From 5c705ab6755ec0c70800314f9832494e28ae4d82 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 18 Dec 2025 08:55:47 +0100 Subject: [PATCH] ci: fix swiftformat and bun CI --- .../Clawdis/AnthropicAuthControls.swift | 1 - .../Sources/Clawdis/AnthropicOAuth.swift | 2 +- .../Sources/Clawdis/Bridge/BridgeServer.swift | 2 +- .../macos/Sources/Clawdis/CanvasManager.swift | 9 +- .../Sources/Clawdis/CanvasSchemeHandler.swift | 7 +- apps/macos/Sources/Clawdis/CanvasWindow.swift | 47 +++++----- .../Sources/Clawdis/ConfigSettings.swift | 28 +++--- .../Clawdis/ControlRequestHandler.swift | 28 ++++-- .../Sources/Clawdis/DeviceModelCatalog.swift | 22 +++-- .../Sources/Clawdis/GatewayConnection.swift | 6 +- .../macos/Sources/Clawdis/GatewayErrors.swift | 1 - .../Sources/Clawdis/String+NonEmpty.swift | 1 - .../Sources/Clawdis/VoicePushToTalk.swift | 4 +- .../Sources/Clawdis/WebChatManager.swift | 1 - .../Sources/Clawdis/WebChatSwiftUI.swift | 4 +- .../macos/Sources/ClawdisCLI/ClawdisCLI.swift | 16 +++- .../ClawdisProtocol/GatewayModels.swift | 46 ++++++++++ dist/protocol.schema.json | 91 ++++++++++++++++++- package.json | 1 + src/gateway/server.test.ts | 35 +++---- 20 files changed, 259 insertions(+), 93 deletions(-) diff --git a/apps/macos/Sources/Clawdis/AnthropicAuthControls.swift b/apps/macos/Sources/Clawdis/AnthropicAuthControls.swift index 62722dcf8..ede230625 100644 --- a/apps/macos/Sources/Clawdis/AnthropicAuthControls.swift +++ b/apps/macos/Sources/Clawdis/AnthropicAuthControls.swift @@ -149,4 +149,3 @@ struct AnthropicAuthControls: View { } } } - diff --git a/apps/macos/Sources/Clawdis/AnthropicOAuth.swift b/apps/macos/Sources/Clawdis/AnthropicOAuth.swift index b9e0919fe..52ba5ca5d 100644 --- a/apps/macos/Sources/Clawdis/AnthropicOAuth.swift +++ b/apps/macos/Sources/Clawdis/AnthropicOAuth.swift @@ -183,7 +183,7 @@ enum PiOAuthStore { static func oauthDir() -> URL { if let override = ProcessInfo.processInfo.environment[self.piAgentDirEnv]? .trimmingCharacters(in: .whitespacesAndNewlines), - !override.isEmpty + !override.isEmpty { let expanded = NSString(string: override).expandingTildeInPath return URL(fileURLWithPath: expanded, isDirectory: true) diff --git a/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift b/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift index 9d331f61d..89a7dbc3c 100644 --- a/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift +++ b/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift @@ -74,7 +74,7 @@ actor BridgeServer { } private func handle(connection: NWConnection) async { - let handler = BridgeConnectionHandler(connection: connection, logger: self.logger) + let handler = BridgeConnectionHandler(connection: connection, logger: self.logger) await handler.run( resolveAuth: { [weak self] hello in await self?.authorize(hello: hello) ?? .error(code: "UNAVAILABLE", message: "bridge unavailable") diff --git a/apps/macos/Sources/Clawdis/CanvasManager.swift b/apps/macos/Sources/Clawdis/CanvasManager.swift index 3e35eeb25..7fb8a2b2c 100644 --- a/apps/macos/Sources/Clawdis/CanvasManager.swift +++ b/apps/macos/Sources/Clawdis/CanvasManager.swift @@ -26,7 +26,11 @@ final class CanvasManager { try self.showDetailed(sessionKey: sessionKey, target: path, placement: placement).directory } - func showDetailed(sessionKey: String, target: String? = nil, placement: CanvasPlacement? = nil) throws -> CanvasShowResult { + func showDetailed( + sessionKey: String, + target: String? = nil, + placement: CanvasPlacement? = nil) throws -> CanvasShowResult + { Self.logger.debug( "showDetailed start session=\(sessionKey, privacy: .public) target=\(target ?? "", privacy: .public) placement=\(placement != nil)") let anchorProvider = self.defaultAnchorProvider ?? Self.mouseAnchorProvider @@ -177,7 +181,8 @@ final class CanvasManager { private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus { let fm = FileManager.default let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines) - let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first.map(String.init) ?? trimmed + let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first + .map(String.init) ?? trimmed var path = withoutQuery if path.hasPrefix("/") { path.removeFirst() } path = path.removingPercentEncoding ?? path diff --git a/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift b/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift index a924a79ea..ef9c5ab84 100644 --- a/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift +++ b/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift @@ -222,11 +222,10 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { || trimmed.hasPrefix(Self.builtinPrefix + "/") else { return nil } - let relative: String - if trimmed == Self.builtinPrefix || trimmed == Self.builtinPrefix + "/" { - relative = "index.html" + let relative = if trimmed == Self.builtinPrefix || trimmed == Self.builtinPrefix + "/" { + "index.html" } else { - relative = String(trimmed.dropFirst((Self.builtinPrefix + "/").count)) + String(trimmed.dropFirst((Self.builtinPrefix + "/").count)) } if relative.isEmpty { return self.html("Not Found", title: "Canvas: 404") } diff --git a/apps/macos/Sources/Clawdis/CanvasWindow.swift b/apps/macos/Sources/Clawdis/CanvasWindow.swift index d8825ff55..cb1021c12 100644 --- a/apps/macos/Sources/Clawdis/CanvasWindow.swift +++ b/apps/macos/Sources/Clawdis/CanvasWindow.swift @@ -160,7 +160,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS // Only auto-reload when we are showing local canvas content. guard webView.url?.scheme == CanvasScheme.scheme else { return } - // Avoid reloading the built-in A2UI shell due to filesystem noise (it does not depend on session files). + // Avoid reloading the built-in A2UI shell due to filesystem noise (it does not depend on session + // files). let path = webView.url?.path ?? "" if path.hasPrefix("/__clawdis__/a2ui") { return } if path == "/" || path.isEmpty { @@ -200,7 +201,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } @MainActor deinit { - self.webView.configuration.userContentController.removeScriptMessageHandler(forName: CanvasA2UIActionMessageHandler.messageName) + self.webView.configuration.userContentController + .removeScriptMessageHandler(forName: CanvasA2UIActionMessageHandler.messageName) self.watcher.stop() } @@ -255,10 +257,10 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS if trimmed.hasPrefix("/") { var isDir: ObjCBool = false if FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue { - let url = URL(fileURLWithPath: trimmed) - canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)") - self.loadFile(url) - return + let url = URL(fileURLWithPath: trimmed) + canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)") + self.loadFile(url) + return } } @@ -344,17 +346,17 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS private static func makeWindow(for presentation: CanvasPresentation, contentView: NSView) -> NSWindow { switch presentation { case .window: - let window = NSWindow( - contentRect: NSRect(origin: .zero, size: CanvasLayout.windowSize), - styleMask: [.titled, .closable, .resizable, .miniaturizable], - backing: .buffered, - defer: false) - window.title = "Clawdis Canvas" - window.isReleasedWhenClosed = false - window.contentView = contentView - window.center() - window.minSize = NSSize(width: 880, height: 680) - return window + let window = NSWindow( + contentRect: NSRect(origin: .zero, size: CanvasLayout.windowSize), + styleMask: [.titled, .closable, .resizable, .miniaturizable], + backing: .buffered, + defer: false) + window.title = "Clawdis Canvas" + window.isReleasedWhenClosed = false + window.contentView = contentView + window.center() + window.minSize = NSSize(width: 880, height: 680) + return window case .panel: let panel = CanvasPanel( @@ -501,7 +503,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS if self.webView.url?.scheme == CanvasScheme.scheme { Task { await DeepLinkHandler.shared.handle(url: url) } } else { - canvasWindowLogger.debug("ignoring deep link from non-canvas page \(url.absoluteString, privacy: .public)") + canvasWindowLogger + .debug("ignoring deep link from non-canvas page \(url.absoluteString, privacy: .public)") } decisionHandler(.cancel) return @@ -635,12 +638,14 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan guard let name = userAction["name"] as? String, !name.isEmpty else { return } let actionId = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - ?? UUID().uuidString + ?? UUID().uuidString canvasWindowLogger.info("A2UI action \(name, privacy: .public) session=\(self.sessionKey, privacy: .public)") - let surfaceId = (userAction["surfaceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main" - let sourceComponentId = (userAction["sourceComponentId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "-" + let surfaceId = (userAction["surfaceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + .nonEmpty ?? "main" + let sourceComponentId = (userAction["sourceComponentId"] as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "-" let host = Self.sanitizeTagValue(InstanceIdentity.displayName) let instanceId = InstanceIdentity.instanceId.lowercased() let contextJSON = Self.compactJSON(userAction["context"]) diff --git a/apps/macos/Sources/Clawdis/ConfigSettings.swift b/apps/macos/Sources/Clawdis/ConfigSettings.swift index 1f5eea981..1d3d20691 100644 --- a/apps/macos/Sources/Clawdis/ConfigSettings.swift +++ b/apps/macos/Sources/Clawdis/ConfigSettings.swift @@ -31,20 +31,20 @@ struct ConfigSettings: View { var body: some View { ScrollView { self.content } - .onChange(of: self.modelCatalogPath) { _, _ in - Task { await self.loadModels() } - } - .onChange(of: self.modelCatalogReloadBump) { _, _ in - Task { await self.loadModels() } - } - .task { - guard !self.hasLoaded else { return } - guard !self.isPreview else { return } - self.hasLoaded = true - self.loadConfig() - await self.loadModels() - self.allowAutosave = true - } + .onChange(of: self.modelCatalogPath) { _, _ in + Task { await self.loadModels() } + } + .onChange(of: self.modelCatalogReloadBump) { _, _ in + Task { await self.loadModels() } + } + .task { + guard !self.hasLoaded else { return } + guard !self.isPreview else { return } + self.hasLoaded = true + self.loadConfig() + await self.loadModels() + self.allowAutosave = true + } } private var content: some View { diff --git a/apps/macos/Sources/Clawdis/ControlRequestHandler.swift b/apps/macos/Sources/Clawdis/ControlRequestHandler.swift index ab2c62813..88ca77c1c 100644 --- a/apps/macos/Sources/Clawdis/ControlRequestHandler.swift +++ b/apps/macos/Sources/Clawdis/ControlRequestHandler.swift @@ -237,8 +237,13 @@ enum ControlRequestHandler { logger.info("canvas show start session=\(session, privacy: .public) path=\(path ?? "", privacy: .public)") do { logger.info("canvas show awaiting CanvasManager") - let res = try await CanvasManager.shared.showDetailed(sessionKey: session, target: path, placement: placement) - logger.info("canvas show done dir=\(res.directory, privacy: .public) status=\(String(describing: res.status), privacy: .public)") + let res = try await CanvasManager.shared.showDetailed( + sessionKey: session, + target: path, + placement: placement) + logger + .info( + "canvas show done dir=\(res.directory, privacy: .public) status=\(String(describing: res.status), privacy: .public)") let payload = try? JSONEncoder().encode(res) return Response(ok: true, message: res.directory, payload: payload) } catch { @@ -277,17 +282,21 @@ enum ControlRequestHandler { } } - private static func handleCanvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?) async -> Response { + private static func handleCanvasA2UI( + session: String, + command: CanvasA2UICommand, + jsonl: String?) async -> Response + { guard self.canvasEnabled() else { return Response(ok: false, message: "Canvas disabled by user") } do { // Ensure the Canvas is visible without forcing a navigation/reload. _ = try await CanvasManager.shared.show(sessionKey: session, path: nil) // Wait for the in-page A2UI bridge. If it doesn't appear, force-load the bundled A2UI shell once. - var ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: false, timeoutMs: 2_000) + var ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: false, timeoutMs: 2000) if !ready { _ = try await CanvasManager.shared.show(sessionKey: session, path: "/__clawdis__/a2ui/") - ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: true, timeoutMs: 5_000) + ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: true, timeoutMs: 5000) } guard ready else { return Response(ok: false, message: "A2UI not ready") } @@ -397,7 +406,8 @@ enum ControlRequestHandler { let found = dict.keys.sorted().joined(separator: ", ") throw NSError(domain: "A2UI", code: 3, userInfo: [ NSLocalizedDescriptionKey: """ - A2UI JSONL line \(item.lineNumber): expected exactly one of \(allowed.sorted().joined(separator: ", ")); found: \(found) + A2UI JSONL line \(item.lineNumber): expected exactly one of \(allowed.sorted() + .joined(separator: ", ")); found: \(found) """, ]) } @@ -441,7 +451,7 @@ enum ControlRequestHandler { let data = try await GatewayConnection.shared.request( method: "node.list", params: [:], - timeoutMs: 10_000) + timeoutMs: 10000) let payload = try JSONDecoder().decode(GatewayNodeListPayload.self, from: data) let result = self.buildNodeListResult(payload: payload) @@ -460,7 +470,7 @@ enum ControlRequestHandler { let data = try await GatewayConnection.shared.request( method: "node.describe", params: ["nodeId": AnyCodable(nodeId)], - timeoutMs: 10_000) + timeoutMs: 10000) return Response(ok: true, payload: data) } catch { return Response(ok: false, message: error.localizedDescription) @@ -526,7 +536,7 @@ enum ControlRequestHandler { let data = try await GatewayConnection.shared.request( method: "node.invoke", params: params, - timeoutMs: 30_000) + timeoutMs: 30000) return Response(ok: true, payload: data) } catch { logger.error("node invoke failed: \(error.localizedDescription, privacy: .public)") diff --git a/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift b/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift index d563a194a..a093a85d1 100644 --- a/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift +++ b/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift @@ -12,9 +12,9 @@ enum DeviceModelCatalog { let family = (deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - let friendlyName = model.isEmpty ? nil : modelIdentifierToName[model] - let symbol = symbolFor(modelIdentifier: model, friendlyName: friendlyName) - ?? fallbackSymbol(for: family, modelIdentifier: model) + let friendlyName = model.isEmpty ? nil : self.modelIdentifierToName[model] + let symbol = self.symbolFor(modelIdentifier: model, friendlyName: friendlyName) + ?? self.fallbackSymbol(for: family, modelIdentifier: model) let title = if let friendlyName, !friendlyName.isEmpty { friendlyName @@ -47,7 +47,9 @@ enum DeviceModelCatalog { if lower.hasPrefix("macbook") || lower.hasPrefix("macbookpro") || lower.hasPrefix("macbookair") { return "laptopcomputer" } - if lower.hasPrefix("imac") || lower.hasPrefix("macmini") || lower.hasPrefix("macpro") || lower.hasPrefix("macstudio") { + if lower.hasPrefix("imac") || lower.hasPrefix("macmini") || lower.hasPrefix("macpro") || lower + .hasPrefix("macstudio") + { return "desktopcomputer" } @@ -84,8 +86,12 @@ enum DeviceModelCatalog { private static func loadModelIdentifierToName() -> [String: String] { var combined: [String: String] = [:] - combined.merge(loadMapping(resourceName: "ios-device-identifiers"), uniquingKeysWith: { current, _ in current }) - combined.merge(loadMapping(resourceName: "mac-device-identifiers"), uniquingKeysWith: { current, _ in current }) + combined.merge( + self.loadMapping(resourceName: "ios-device-identifiers"), + uniquingKeysWith: { current, _ in current }) + combined.merge( + self.loadMapping(resourceName: "mac-device-identifiers"), + uniquingKeysWith: { current, _ in current }) return combined } @@ -128,10 +134,10 @@ enum DeviceModelCatalog { var normalizedName: String? { switch self { - case .string(let s): + case let .string(s): let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? nil : trimmed - case .stringArray(let arr): + case let .stringArray(arr): let values = arr .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } diff --git a/apps/macos/Sources/Clawdis/GatewayConnection.swift b/apps/macos/Sources/Clawdis/GatewayConnection.swift index 9138ac40f..46ab20afa 100644 --- a/apps/macos/Sources/Clawdis/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdis/GatewayConnection.swift @@ -42,11 +42,11 @@ actor GatewayConnection { typealias Config = (url: URL, token: String?) enum Method: String, Sendable { - case agent = "agent" - case status = "status" + case agent + case status case setHeartbeats = "set-heartbeats" case systemEvent = "system-event" - case health = "health" + case health case chatHistory = "chat.history" case chatSend = "chat.send" case chatAbort = "chat.abort" diff --git a/apps/macos/Sources/Clawdis/GatewayErrors.swift b/apps/macos/Sources/Clawdis/GatewayErrors.swift index 50ba05b01..73c1f4736 100644 --- a/apps/macos/Sources/Clawdis/GatewayErrors.swift +++ b/apps/macos/Sources/Clawdis/GatewayErrors.swift @@ -31,4 +31,3 @@ struct GatewayDecodingError: LocalizedError, Sendable { var errorDescription: String? { "\(self.method): \(self.message)" } } - diff --git a/apps/macos/Sources/Clawdis/String+NonEmpty.swift b/apps/macos/Sources/Clawdis/String+NonEmpty.swift index a5d208e2e..402e4c2db 100644 --- a/apps/macos/Sources/Clawdis/String+NonEmpty.swift +++ b/apps/macos/Sources/Clawdis/String+NonEmpty.swift @@ -6,4 +6,3 @@ extension String { return trimmed.isEmpty ? nil : trimmed } } - diff --git a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift index 8f5fbde16..795eeace0 100644 --- a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift +++ b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift @@ -18,8 +18,8 @@ final class VoicePushToTalkHotkey: @unchecked Sendable { init( beginAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.begin() }, - endAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.end() } - ) { + endAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.end() }) + { self.beginAction = beginAction self.endAction = endAction } diff --git a/apps/macos/Sources/Clawdis/WebChatManager.swift b/apps/macos/Sources/Clawdis/WebChatManager.swift index bc3fe4bc0..c4dc7ab94 100644 --- a/apps/macos/Sources/Clawdis/WebChatManager.swift +++ b/apps/macos/Sources/Clawdis/WebChatManager.swift @@ -102,4 +102,3 @@ final class WebChatManager { // Keep panel controller cached so reopening doesn't re-bootstrap. } } - diff --git a/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift b/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift index d9ae7876d..6bd5e5eb8 100644 --- a/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift +++ b/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift @@ -25,7 +25,7 @@ struct MacGatewayChatTransport: ClawdisChatTransport, Sendable { "sessionKey": AnyCodable(sessionKey), "runId": AnyCodable(runId), ], - timeoutMs: 10_000) + timeoutMs: 10000) } func listSessions(limit: Int?) async throws -> ClawdisChatSessionsListResponse { @@ -39,7 +39,7 @@ struct MacGatewayChatTransport: ClawdisChatTransport, Sendable { let data = try await GatewayConnection.shared.request( method: "sessions.list", params: params, - timeoutMs: 15_000) + timeoutMs: 15000) return try JSONDecoder().decode(ClawdisChatSessionsListResponse.self, from: data) } diff --git a/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift b/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift index 20d138814..92ce15687 100644 --- a/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift +++ b/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift @@ -259,6 +259,7 @@ struct ClawdisCLI { return ParsedCLIRequest( request: .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON), kind: .generic) + default: throw CLIError.help } @@ -450,10 +451,13 @@ struct ClawdisCLI { if let message = response.message, !message.isEmpty { FileHandle.standardOutput.write(Data((message + "\n").utf8)) } - if let payload = response.payload, let info = try? JSONDecoder().decode(CanvasShowResult.self, from: payload) { - FileHandle.standardOutput.write(Data(("STATUS:\(info.status.rawValue)\n").utf8)) + if let payload = response.payload, let info = try? JSONDecoder().decode( + CanvasShowResult.self, + from: payload) + { + FileHandle.standardOutput.write(Data("STATUS:\(info.status.rawValue)\n".utf8)) if let url = info.url, !url.isEmpty { - FileHandle.standardOutput.write(Data(("URL:\(url)\n").utf8)) + FileHandle.standardOutput.write(Data("URL:\(url)\n".utf8)) } } return @@ -515,7 +519,7 @@ struct ClawdisCLI { let version = (n.version ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if !platform.isEmpty || !version.isEmpty { let pv = [platform.isEmpty ? nil : platform, version.isEmpty ? nil : version] - .compactMap { $0 } + .compactMap(\.self) .joined(separator: " ") if !pv.isEmpty { print(" platform: \(pv)") } } @@ -571,7 +575,9 @@ struct ClawdisCLI { print(parts.joined(separator: " ยท ")) if !commands.isEmpty { print("Commands:") - for c in commands { print("- \(c)") } + for c in commands { + print("- \(c)") + } } return } diff --git a/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift index 97ffa4cd8..f77f1f4fc 100644 --- a/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift +++ b/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift @@ -420,6 +420,10 @@ public struct NodePairRequestParams: Codable { public let displayname: String? public let platform: String? public let version: String? + public let devicefamily: String? + public let modelidentifier: String? + public let caps: [String]? + public let commands: [String]? public let remoteip: String? public init( @@ -427,12 +431,20 @@ public struct NodePairRequestParams: Codable { displayname: String?, platform: String?, version: String?, + devicefamily: String?, + modelidentifier: String?, + caps: [String]?, + commands: [String]?, remoteip: String? ) { self.nodeid = nodeid self.displayname = displayname self.platform = platform self.version = version + self.devicefamily = devicefamily + self.modelidentifier = modelidentifier + self.caps = caps + self.commands = commands self.remoteip = remoteip } private enum CodingKeys: String, CodingKey { @@ -440,6 +452,10 @@ public struct NodePairRequestParams: Codable { case displayname = "displayName" case platform case version + case devicefamily = "deviceFamily" + case modelidentifier = "modelIdentifier" + case caps + case commands case remoteip = "remoteIp" } } @@ -493,6 +509,19 @@ public struct NodePairVerifyParams: Codable { public struct NodeListParams: Codable { } +public struct NodeDescribeParams: Codable { + public let nodeid: String + + public init( + nodeid: String + ) { + self.nodeid = nodeid + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + } +} + public struct NodeInvokeParams: Codable { public let nodeid: String public let command: String @@ -837,6 +866,23 @@ public struct ChatSendParams: Codable { } } +public struct ChatAbortParams: Codable { + public let sessionkey: String + public let runid: String + + public init( + sessionkey: String, + runid: String + ) { + self.sessionkey = sessionkey + self.runid = runid + } + private enum CodingKeys: String, CodingKey { + case sessionkey = "sessionKey" + case runid = "runId" + } +} + public struct ChatEvent: Codable { public let runid: String public let sessionkey: String diff --git a/dist/protocol.schema.json b/dist/protocol.schema.json index 0cc51fc1e..22657ad73 100644 --- a/dist/protocol.schema.json +++ b/dist/protocol.schema.json @@ -51,6 +51,14 @@ "minLength": 1, "type": "string" }, + "deviceFamily": { + "minLength": 1, + "type": "string" + }, + "modelIdentifier": { + "minLength": 1, + "type": "string" + }, "mode": { "minLength": 1, "type": "string" @@ -185,6 +193,14 @@ "minLength": 1, "type": "string" }, + "deviceFamily": { + "minLength": 1, + "type": "string" + }, + "modelIdentifier": { + "minLength": 1, + "type": "string" + }, "mode": { "minLength": 1, "type": "string" @@ -538,6 +554,14 @@ "minLength": 1, "type": "string" }, + "deviceFamily": { + "minLength": 1, + "type": "string" + }, + "modelIdentifier": { + "minLength": 1, + "type": "string" + }, "mode": { "minLength": 1, "type": "string" @@ -617,6 +641,14 @@ "minLength": 1, "type": "string" }, + "deviceFamily": { + "minLength": 1, + "type": "string" + }, + "modelIdentifier": { + "minLength": 1, + "type": "string" + }, "mode": { "minLength": 1, "type": "string" @@ -860,6 +892,28 @@ "minLength": 1, "type": "string" }, + "deviceFamily": { + "minLength": 1, + "type": "string" + }, + "modelIdentifier": { + "minLength": 1, + "type": "string" + }, + "caps": { + "type": "array", + "items": { + "minLength": 1, + "type": "string" + } + }, + "commands": { + "type": "array", + "items": { + "minLength": 1, + "type": "string" + } + }, "remoteIp": { "minLength": 1, "type": "string" @@ -923,6 +977,19 @@ "type": "object", "properties": {} }, + "NodeDescribeParams": { + "additionalProperties": false, + "type": "object", + "properties": { + "nodeId": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "nodeId" + ] + }, "NodeInvokeParams": { "additionalProperties": false, "type": "object", @@ -1773,7 +1840,7 @@ }, "limit": { "minimum": 1, - "maximum": 500, + "maximum": 1000, "type": "integer" } }, @@ -1818,6 +1885,24 @@ "idempotencyKey" ] }, + "ChatAbortParams": { + "additionalProperties": false, + "type": "object", + "properties": { + "sessionKey": { + "minLength": 1, + "type": "string" + }, + "runId": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "sessionKey", + "runId" + ] + }, "ChatEvent": { "additionalProperties": false, "type": "object", @@ -1844,6 +1929,10 @@ "const": "final", "type": "string" }, + { + "const": "aborted", + "type": "string" + }, { "const": "error", "type": "string" diff --git a/package.json b/package.json index 0ec7a981d..e7f72ef82 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@grammyjs/transformer-throttler": "^1.2.1", "@homebridge/ciao": "^1.3.4", + "@mariozechner/pi-agent-core": "^0.21.0", "@mariozechner/pi-ai": "^0.21.0", "@mariozechner/pi-coding-agent": "^0.21.0", "@sinclair/typebox": "^0.34.41", diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index ba8aa0932..ccdf0b34b 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -2419,6 +2419,11 @@ describe("gateway server", () => { }); }); + const sendResP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "send-mismatch-1", + 10_000, + ); ws.send( JSON.stringify({ type: "req", @@ -2435,6 +2440,11 @@ describe("gateway server", () => { await agentStartedP; + const abortResP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "abort-mismatch-1", + 10_000, + ); ws.send( JSON.stringify({ type: "req", @@ -2444,14 +2454,15 @@ describe("gateway server", () => { }), ); - const abortRes = await onceMessage( - ws, - (o) => o.type === "res" && o.id === "abort-mismatch-1", - 10_000, - ); + const abortRes = await abortResP; expect(abortRes.ok).toBe(false); expect(abortRes.error?.code).toBe("INVALID_REQUEST"); + const abortRes2P = onceMessage( + ws, + (o) => o.type === "res" && o.id === "abort-mismatch-2", + 10_000, + ); ws.send( JSON.stringify({ type: "req", @@ -2461,23 +2472,15 @@ describe("gateway server", () => { }), ); - const abortRes2 = await onceMessage( - ws, - (o) => o.type === "res" && o.id === "abort-mismatch-2", - 10_000, - ); + const abortRes2 = await abortRes2P; expect(abortRes2.ok).toBe(true); - const sendRes = await onceMessage( - ws, - (o) => o.type === "res" && o.id === "send-mismatch-1", - 10_000, - ); + const sendRes = await sendResP; expect(sendRes.ok).toBe(true); ws.close(); await server.close(); - }); + }, 15_000); test("chat.abort is a no-op after chat.send completes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-gw-"));