From d1217e84c76f1b7e98fa0fcdb0364a1e548997e7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 15:06:44 +0100 Subject: [PATCH] CLI: remove relay/heartbeat legacy commands --- .../Sources/Clawdis/GatewayChannel.swift | 2 +- .../Sources/ClawdisProtocol/Protocol.swift | 511 +++++++++--------- src/cli/program.test.ts | 78 +-- src/cli/program.ts | 447 +-------------- 4 files changed, 263 insertions(+), 775 deletions(-) diff --git a/apps/macos/Sources/Clawdis/GatewayChannel.swift b/apps/macos/Sources/Clawdis/GatewayChannel.swift index ca2ae40f9..2349bd400 100644 --- a/apps/macos/Sources/Clawdis/GatewayChannel.swift +++ b/apps/macos/Sources/Clawdis/GatewayChannel.swift @@ -182,7 +182,7 @@ actor GatewayChannel { self.inner = GatewayChannelActor(url: url, token: token) } - func request(method: String, params: [String: Any]?) async throws -> Data { + func request(method: String, params: [String: AnyCodable]?) async throws -> Data { guard let inner else { throw NSError(domain: "Gateway", code: 0, userInfo: [NSLocalizedDescriptionKey: "not configured"]) } diff --git a/apps/macos/Sources/ClawdisProtocol/Protocol.swift b/apps/macos/Sources/ClawdisProtocol/Protocol.swift index 2078e7b1b..3351d96d2 100644 --- a/apps/macos/Sources/ClawdisProtocol/Protocol.swift +++ b/apps/macos/Sources/ClawdisProtocol/Protocol.swift @@ -6,6 +6,7 @@ import Foundation // MARK: - ClawdisGateway + /// Handshake, request/response, and event frames for the Gateway WebSocket. struct ClawdisGateway: Codable { let auth: Auth? @@ -53,7 +54,7 @@ extension ClawdisGateway { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( @@ -81,9 +82,9 @@ extension ClawdisGateway { payload: JSONAny?? = nil, event: String?? = nil, seq: Int?? = nil, - stateVersion: ClawdisGatewayStateVersion?? = nil - ) -> ClawdisGateway { - return ClawdisGateway( + stateVersion: ClawdisGatewayStateVersion?? = nil) -> ClawdisGateway + { + ClawdisGateway( auth: auth ?? self.auth, caps: caps ?? self.caps, client: client ?? self.client, @@ -108,20 +109,20 @@ extension ClawdisGateway { payload: payload ?? self.payload, event: event ?? self.event, seq: seq ?? self.seq, - stateVersion: stateVersion ?? self.stateVersion - ) + stateVersion: stateVersion ?? self.stateVersion) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Auth + struct Auth: Codable { let token: String? } @@ -141,27 +142,27 @@ extension Auth { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( - token: String?? = nil - ) -> Auth { - return Auth( - token: token ?? self.token - ) + token: String?? = nil) -> Auth + { + Auth( + token: token ?? self.token) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Client + struct Client: Codable { let instanceID: String? let mode, name, platform, version: String @@ -187,7 +188,7 @@ extension Client { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( @@ -195,27 +196,27 @@ extension Client { mode: String? = nil, name: String? = nil, platform: String? = nil, - version: String? = nil - ) -> Client { - return Client( + version: String? = nil) -> Client + { + Client( instanceID: instanceID ?? self.instanceID, mode: mode ?? self.mode, name: name ?? self.name, platform: platform ?? self.platform, - version: version ?? self.version - ) + version: version ?? self.version) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Error + struct Error: Codable { let code: String let details: JSONAny? @@ -244,7 +245,7 @@ extension Error { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( @@ -252,27 +253,27 @@ extension Error { details: JSONAny?? = nil, message: String? = nil, retryable: Bool?? = nil, - retryAfterMS: Int?? = nil - ) -> Error { - return Error( + retryAfterMS: Int?? = nil) -> Error + { + Error( code: code ?? self.code, details: details ?? self.details, message: message ?? self.message, retryable: retryable ?? self.retryable, - retryAfterMS: retryAfterMS ?? self.retryAfterMS - ) + retryAfterMS: retryAfterMS ?? self.retryAfterMS) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Features + struct Features: Codable { let events, methods: [String] } @@ -292,29 +293,29 @@ extension Features { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( events: [String]? = nil, - methods: [String]? = nil - ) -> Features { - return Features( + methods: [String]? = nil) -> Features + { + Features( events: events ?? self.events, - methods: methods ?? self.methods - ) + methods: methods ?? self.methods) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Policy + struct Policy: Codable { let maxBufferedBytes, maxPayload, tickIntervalMS: Int @@ -339,31 +340,31 @@ extension Policy { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( maxBufferedBytes: Int? = nil, maxPayload: Int? = nil, - tickIntervalMS: Int? = nil - ) -> Policy { - return Policy( + tickIntervalMS: Int? = nil) -> Policy + { + Policy( maxBufferedBytes: maxBufferedBytes ?? self.maxBufferedBytes, maxPayload: maxPayload ?? self.maxPayload, - tickIntervalMS: tickIntervalMS ?? self.tickIntervalMS - ) + tickIntervalMS: tickIntervalMS ?? self.tickIntervalMS) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Server + struct Server: Codable { let commit: String? let connID: String @@ -392,33 +393,33 @@ extension Server { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( commit: String?? = nil, connID: String? = nil, host: String?? = nil, - version: String? = nil - ) -> Server { - return Server( + version: String? = nil) -> Server + { + Server( commit: commit ?? self.commit, connID: connID ?? self.connID, host: host ?? self.host, - version: version ?? self.version - ) + version: version ?? self.version) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Snapshot + struct Snapshot: Codable { let health: JSONAny let presence: [Presence] @@ -446,33 +447,33 @@ extension Snapshot { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( health: JSONAny? = nil, presence: [Presence]? = nil, stateVersion: SnapshotStateVersion? = nil, - uptimeMS: Int? = nil - ) -> Snapshot { - return Snapshot( + uptimeMS: Int? = nil) -> Snapshot + { + Snapshot( health: health ?? self.health, presence: presence ?? self.presence, stateVersion: stateVersion ?? self.stateVersion, - uptimeMS: uptimeMS ?? self.uptimeMS - ) + uptimeMS: uptimeMS ?? self.uptimeMS) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - Presence + struct Presence: Codable { let host, instanceID, ip: String? let lastInputSeconds: Int? @@ -504,7 +505,7 @@ extension Presence { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( @@ -517,9 +518,9 @@ extension Presence { tags: [String]?? = nil, text: String?? = nil, ts: Int? = nil, - version: String?? = nil - ) -> Presence { - return Presence( + version: String?? = nil) -> Presence + { + Presence( host: host ?? self.host, instanceID: instanceID ?? self.instanceID, ip: ip ?? self.ip, @@ -529,20 +530,20 @@ extension Presence { tags: tags ?? self.tags, text: text ?? self.text, ts: ts ?? self.ts, - version: version ?? self.version - ) + version: version ?? self.version) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - SnapshotStateVersion + struct SnapshotStateVersion: Codable { let health, presence: Int } @@ -562,29 +563,29 @@ extension SnapshotStateVersion { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( health: Int? = nil, - presence: Int? = nil - ) -> SnapshotStateVersion { - return SnapshotStateVersion( + presence: Int? = nil) -> SnapshotStateVersion + { + SnapshotStateVersion( health: health ?? self.health, - presence: presence ?? self.presence - ) + presence: presence ?? self.presence) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } // MARK: - ClawdisGatewayStateVersion + struct ClawdisGatewayStateVersion: Codable { let health, presence: Int } @@ -604,35 +605,34 @@ extension ClawdisGatewayStateVersion { } init(fromURL url: URL) throws { - try self.init(data: try Data(contentsOf: url)) + try self.init(data: Data(contentsOf: url)) } func with( health: Int? = nil, - presence: Int? = nil - ) -> ClawdisGatewayStateVersion { - return ClawdisGatewayStateVersion( + presence: Int? = nil) -> ClawdisGatewayStateVersion + { + ClawdisGatewayStateVersion( health: health ?? self.health, - presence: presence ?? self.presence - ) + presence: presence ?? self.presence) } func jsonData() throws -> Data { - return try newJSONEncoder().encode(self) + try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { - return String(data: try self.jsonData(), encoding: encoding) + try String(data: self.jsonData(), encoding: encoding) } } enum TypeEnum: String, Codable { - case event = "event" - case hello = "hello" + case event + case hello case helloError = "hello-error" case helloOk = "hello-ok" - case req = "req" - case res = "res" + case req + case res } // MARK: - Helper functions for creating encoders and decoders @@ -656,18 +656,17 @@ func newJSONEncoder() -> JSONEncoder { // MARK: - Encode/decode helpers class JSONNull: Codable, Hashable { - - public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool { + static func == (lhs: JSONNull, rhs: JSONNull) -> Bool { true } - public func hash(into hasher: inout Hasher) { + func hash(into hasher: inout Hasher) { hasher.combine(0) } - public init() {} + init() {} - public required init(from decoder: Decoder) throws { + required init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if !container.decodeNil() { throw DecodingError.typeMismatch( @@ -678,7 +677,7 @@ class JSONNull: Codable, Hashable { } } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encodeNil() } @@ -688,215 +687,215 @@ class JSONCodingKey: CodingKey { let key: String required init?(intValue: Int) { - return nil + nil } required init?(stringValue: String) { - key = stringValue + self.key = stringValue } var intValue: Int? { - return nil + nil } var stringValue: String { - return key + self.key } } class JSONAny: Codable { - let value: Any static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { - let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") - return DecodingError.typeMismatch(JSONAny.self, context) + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") + return DecodingError.typeMismatch(JSONAny.self, context) } static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { - let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") - return EncodingError.invalidValue(value, context) + let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") + return EncodingError.invalidValue(value, context) } static func decode(from container: SingleValueDecodingContainer) throws -> Any { - if let value = try? container.decode(Bool.self) { - return value - } - if let value = try? container.decode(Int64.self) { - return value - } - if let value = try? container.decode(Double.self) { - return value - } - if let value = try? container.decode(String.self) { - return value - } - if container.decodeNil() { - return JSONNull() - } - throw decodingError(forCodingPath: container.codingPath) + if let value = try? container.decode(Bool.self) { + return value + } + if let value = try? container.decode(Int64.self) { + return value + } + if let value = try? container.decode(Double.self) { + return value + } + if let value = try? container.decode(String.self) { + return value + } + if container.decodeNil() { + return JSONNull() + } + throw self.decodingError(forCodingPath: container.codingPath) } static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { - if let value = try? container.decode(Bool.self) { - return value + if let value = try? container.decode(Bool.self) { + return value + } + if let value = try? container.decode(Int64.self) { + return value + } + if let value = try? container.decode(Double.self) { + return value + } + if let value = try? container.decode(String.self) { + return value + } + if let value = try? container.decodeNil() { + if value { + return JSONNull() } - if let value = try? container.decode(Int64.self) { - return value - } - if let value = try? container.decode(Double.self) { - return value - } - if let value = try? container.decode(String.self) { - return value - } - if let value = try? container.decodeNil() { - if value { - return JSONNull() - } - } - if var container = try? container.nestedUnkeyedContainer() { - return try decodeArray(from: &container) - } - if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { - return try decodeDictionary(from: &container) - } - throw decodingError(forCodingPath: container.codingPath) + } + if var container = try? container.nestedUnkeyedContainer() { + return try self.decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { + return try self.decodeDictionary(from: &container) + } + throw self.decodingError(forCodingPath: container.codingPath) } static func decode( from container: inout KeyedDecodingContainer, - forKey key: JSONCodingKey) throws -> Any { - if let value = try? container.decode(Bool.self, forKey: key) { - return value + forKey key: JSONCodingKey) throws -> Any + { + if let value = try? container.decode(Bool.self, forKey: key) { + return value + } + if let value = try? container.decode(Int64.self, forKey: key) { + return value + } + if let value = try? container.decode(Double.self, forKey: key) { + return value + } + if let value = try? container.decode(String.self, forKey: key) { + return value + } + if let value = try? container.decodeNil(forKey: key) { + if value { + return JSONNull() } - if let value = try? container.decode(Int64.self, forKey: key) { - return value - } - if let value = try? container.decode(Double.self, forKey: key) { - return value - } - if let value = try? container.decode(String.self, forKey: key) { - return value - } - if let value = try? container.decodeNil(forKey: key) { - if value { - return JSONNull() - } - } - if var container = try? container.nestedUnkeyedContainer(forKey: key) { - return try decodeArray(from: &container) - } - if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { - return try decodeDictionary(from: &container) - } - throw decodingError(forCodingPath: container.codingPath) + } + if var container = try? container.nestedUnkeyedContainer(forKey: key) { + return try self.decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { + return try self.decodeDictionary(from: &container) + } + throw self.decodingError(forCodingPath: container.codingPath) } static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { - var arr: [Any] = [] - while !container.isAtEnd { - let value = try decode(from: &container) - arr.append(value) - } - return arr + var arr: [Any] = [] + while !container.isAtEnd { + let value = try decode(from: &container) + arr.append(value) + } + return arr } static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] { - var dict = [String: Any]() - for key in container.allKeys { - let value = try decode(from: &container, forKey: key) - dict[key.stringValue] = value - } - return dict + var dict = [String: Any]() + for key in container.allKeys { + let value = try decode(from: &container, forKey: key) + dict[key.stringValue] = value + } + return dict } static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { - for value in array { - if let value = value as? Bool { - try container.encode(value) - } else if let value = value as? Int64 { - try container.encode(value) - } else if let value = value as? Double { - try container.encode(value) - } else if let value = value as? String { - try container.encode(value) - } else if value is JSONNull { - try container.encodeNil() - } else if let value = value as? [Any] { - var container = container.nestedUnkeyedContainer() - try encode(to: &container, array: value) - } else if let value = value as? [String: Any] { - var container = container.nestedContainer(keyedBy: JSONCodingKey.self) - try encode(to: &container, dictionary: value) - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } + for value in array { + if let value = value as? Bool { + try container.encode(value) + } else if let value = value as? Int64 { + try container.encode(value) + } else if let value = value as? Double { + try container.encode(value) + } else if let value = value as? String { + try container.encode(value) + } else if value is JSONNull { + try container.encodeNil() + } else if let value = value as? [Any] { + var container = container.nestedUnkeyedContainer() + try self.encode(to: &container, array: value) + } else if let value = value as? [String: Any] { + var container = container.nestedContainer(keyedBy: JSONCodingKey.self) + try self.encode(to: &container, dictionary: value) + } else { + throw self.encodingError(forValue: value, codingPath: container.codingPath) } + } } static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws { - for (key, value) in dictionary { - let key = JSONCodingKey(stringValue: key)! - if let value = value as? Bool { - try container.encode(value, forKey: key) - } else if let value = value as? Int64 { - try container.encode(value, forKey: key) - } else if let value = value as? Double { - try container.encode(value, forKey: key) - } else if let value = value as? String { - try container.encode(value, forKey: key) - } else if value is JSONNull { - try container.encodeNil(forKey: key) - } else if let value = value as? [Any] { - var container = container.nestedUnkeyedContainer(forKey: key) - try encode(to: &container, array: value) - } else if let value = value as? [String: Any] { - var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) - try encode(to: &container, dictionary: value) - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } + for (key, value) in dictionary { + let key = JSONCodingKey(stringValue: key)! + if let value = value as? Bool { + try container.encode(value, forKey: key) + } else if let value = value as? Int64 { + try container.encode(value, forKey: key) + } else if let value = value as? Double { + try container.encode(value, forKey: key) + } else if let value = value as? String { + try container.encode(value, forKey: key) + } else if value is JSONNull { + try container.encodeNil(forKey: key) + } else if let value = value as? [Any] { + var container = container.nestedUnkeyedContainer(forKey: key) + try self.encode(to: &container, array: value) + } else if let value = value as? [String: Any] { + var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) + try self.encode(to: &container, dictionary: value) + } else { + throw self.encodingError(forValue: value, codingPath: container.codingPath) } + } } static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { - if let value = value as? Bool { - try container.encode(value) - } else if let value = value as? Int64 { - try container.encode(value) - } else if let value = value as? Double { - try container.encode(value) - } else if let value = value as? String { - try container.encode(value) - } else if value is JSONNull { - try container.encodeNil() - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } + if let value = value as? Bool { + try container.encode(value) + } else if let value = value as? Int64 { + try container.encode(value) + } else if let value = value as? Double { + try container.encode(value) + } else if let value = value as? String { + try container.encode(value) + } else if value is JSONNull { + try container.encodeNil() + } else { + throw self.encodingError(forValue: value, codingPath: container.codingPath) + } } - public required init(from decoder: Decoder) throws { - if var arrayContainer = try? decoder.unkeyedContainer() { - self.value = try JSONAny.decodeArray(from: &arrayContainer) - } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { - self.value = try JSONAny.decodeDictionary(from: &container) - } else { - let container = try decoder.singleValueContainer() - self.value = try JSONAny.decode(from: container) - } + required init(from decoder: Decoder) throws { + if var arrayContainer = try? decoder.unkeyedContainer() { + self.value = try JSONAny.decodeArray(from: &arrayContainer) + } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { + self.value = try JSONAny.decodeDictionary(from: &container) + } else { + let container = try decoder.singleValueContainer() + self.value = try JSONAny.decode(from: container) + } } - public func encode(to encoder: Encoder) throws { - if let arr = self.value as? [Any] { - var container = encoder.unkeyedContainer() - try JSONAny.encode(to: &container, array: arr) - } else if let dict = self.value as? [String: Any] { - var container = encoder.container(keyedBy: JSONCodingKey.self) - try JSONAny.encode(to: &container, dictionary: dict) - } else { - var container = encoder.singleValueContainer() - try JSONAny.encode(to: &container, value: self.value) - } + func encode(to encoder: Encoder) throws { + if let arr = self.value as? [Any] { + var container = encoder.unkeyedContainer() + try JSONAny.encode(to: &container, array: arr) + } else if let dict = self.value as? [String: Any] { + var container = encoder.container(keyedBy: JSONCodingKey.self) + try JSONAny.encode(to: &container, dictionary: dict) + } else { + var container = encoder.singleValueContainer() + try JSONAny.encode(to: &container, value: self.value) + } } } diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index a8b5b62b4..239a21cf2 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -3,10 +3,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const sendCommand = vi.fn(); const statusCommand = vi.fn(); const loginWeb = vi.fn(); -const monitorWebProvider = vi.fn(); -const logWebSelfId = vi.fn(); -const waitForever = vi.fn(); -const monitorTelegramProvider = vi.fn(); const startWebChatServer = vi.fn(async () => ({ port: 18788 })); const ensureWebChatServerFromConfig = vi.fn(async () => ({ port: 18788 })); @@ -23,10 +19,6 @@ vi.mock("../commands/status.js", () => ({ statusCommand })); vi.mock("../runtime.js", () => ({ defaultRuntime: runtime })); vi.mock("../provider-web.js", () => ({ loginWeb, - monitorWebProvider, -})); -vi.mock("../telegram/monitor.js", () => ({ - monitorTelegramProvider, })); vi.mock("../webchat/server.js", () => ({ startWebChatServer, @@ -34,8 +26,7 @@ vi.mock("../webchat/server.js", () => ({ getWebChatServer: () => null, })); vi.mock("./deps.js", () => ({ - createDefaultDeps: () => ({ waitForever }), - logWebSelfId, + createDefaultDeps: () => ({}), })); const { buildProgram } = await import("./program.js"); @@ -53,73 +44,6 @@ describe("cli program", () => { expect(sendCommand).toHaveBeenCalled(); }); - it("starts relay with heartbeat tuning", async () => { - monitorWebProvider.mockResolvedValue(undefined); - const program = buildProgram(); - await program.parseAsync( - [ - "relay-legacy", - "--web-heartbeat", - "90", - "--heartbeat-now", - "--provider", - "web", - ], - { - from: "user", - }, - ); - expect(logWebSelfId).toHaveBeenCalled(); - expect(monitorWebProvider).toHaveBeenCalledWith( - false, - undefined, - true, - undefined, - runtime, - expect.any(AbortSignal), - { heartbeatSeconds: 90, replyHeartbeatNow: true }, - ); - expect(monitorTelegramProvider).not.toHaveBeenCalled(); - }); - - it("runs telegram relay when token set", async () => { - const program = buildProgram(); - const prev = process.env.TELEGRAM_BOT_TOKEN; - process.env.TELEGRAM_BOT_TOKEN = "token123"; - await program.parseAsync(["relay-legacy", "--provider", "telegram"], { - from: "user", - }); - expect(monitorTelegramProvider).toHaveBeenCalledWith( - expect.objectContaining({ token: "token123" }), - ); - expect(monitorWebProvider).not.toHaveBeenCalled(); - process.env.TELEGRAM_BOT_TOKEN = prev; - }); - - it("errors when telegram provider requested without token", async () => { - const program = buildProgram(); - const prev = process.env.TELEGRAM_BOT_TOKEN; - process.env.TELEGRAM_BOT_TOKEN = ""; - await expect( - program.parseAsync(["relay-legacy", "--provider", "telegram"], { - from: "user", - }), - ).rejects.toThrow(); - expect(runtime.error).toHaveBeenCalled(); - expect(runtime.exit).toHaveBeenCalled(); - process.env.TELEGRAM_BOT_TOKEN = prev; - }); - - it("relay command is deprecated", async () => { - const program = buildProgram(); - await expect( - program.parseAsync(["relay"], { from: "user" }), - ).rejects.toThrow("exit"); - expect(runtime.error).toHaveBeenCalled(); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(monitorWebProvider).not.toHaveBeenCalled(); - }); - it("runs status command", async () => { const program = buildProgram(); await program.parseAsync(["status"], { from: "user" }); diff --git a/src/cli/program.ts b/src/cli/program.ts index 93e8e7022..a8a67c894 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -9,23 +9,10 @@ import { loadConfig } from "../config/config.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { startGatewayServer } from "../gateway/server.js"; import { danger, info, setVerbose } from "../globals.js"; -import { acquireRelayLock, RelayLockError } from "../infra/relay-lock.js"; -import { getResolvedLoggerSettings } from "../logging.js"; -import { - loginWeb, - logoutWeb, - monitorWebProvider, - resolveHeartbeatRecipients, - runWebHeartbeatOnce, - type WebMonitorTuning, -} from "../provider-web.js"; +import { loginWeb, logoutWeb } from "../provider-web.js"; import { runRpcLoop } from "../rpc/loop.js"; import { defaultRuntime } from "../runtime.js"; import { VERSION } from "../version.js"; -import { - resolveHeartbeatSeconds, - resolveReconnectPolicy, -} from "../web/reconnect.js"; import { ensureWebChatServerFromConfig, startWebChatServer, @@ -80,20 +67,16 @@ export function buildProgram() { "Send via your web session and print JSON result.", ], [ - "clawdis relay --verbose", - "Auto-reply loop using your linked web session.", + "clawdis gateway --port 18789", + "Run the WebSocket Gateway locally.", ], [ - "clawdis heartbeat --verbose", - "Send a heartbeat ping to your active session or first allowFrom contact.", - ], - [ - "clawdis status", - "Show web session health and recent session recipients.", + "clawdis gw:status", + "Fetch Gateway status over WS.", ], [ 'clawdis agent --to +15555550123 --message "Run summary" --deliver', - "Talk directly to the agent using the same session handling; optionally send the WhatsApp reply.", + "Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.", ], [ 'clawdis send --provider telegram --to @mychat --message "Hi"', @@ -246,91 +229,6 @@ Examples: await runRpcLoop({ input: process.stdin, output: process.stdout }); await new Promise(() => {}); }); - - program - .command("heartbeat") - .description("Trigger a heartbeat or manual send once (web provider only)") - .option("--to ", "Override target E.164; defaults to allowFrom[0]") - .option( - "--session-id ", - "Force a session id for this heartbeat (resumes a specific Pi session)", - ) - .option( - "--all", - "Send heartbeat to all active sessions (or allowFrom entries when none)", - false, - ) - .option( - "--message ", - "Send a custom message instead of the heartbeat probe", - ) - .option("--body ", "Alias for --message") - .option("--dry-run", "Print the resolved payload without sending", false) - .option("--verbose", "Verbose logging", false) - .addHelpText( - "after", - ` -Examples: - clawdis heartbeat # uses web session + first allowFrom contact - clawdis heartbeat --verbose # prints detailed heartbeat logs - clawdis heartbeat --to +1555123 # override destination - clawdis heartbeat --session-id --to +1555123 # resume a specific session - clawdis heartbeat --message "Ping" - clawdis heartbeat --all # send to every active session recipient or allowFrom entry`, - ) - .action(async (opts) => { - setVerbose(Boolean(opts.verbose)); - const cfg = loadConfig(); - const allowAll = Boolean(opts.all); - const resolution = resolveHeartbeatRecipients(cfg, { - to: opts.to, - all: allowAll, - }); - if ( - !opts.to && - !allowAll && - resolution.source === "session-ambiguous" && - resolution.recipients.length > 1 - ) { - defaultRuntime.error( - danger( - `Multiple active sessions found (${resolution.recipients.join(", ")}). Pass --to or --all to send to all.`, - ), - ); - defaultRuntime.exit(1); - } - const recipients = resolution.recipients; - if (!recipients || recipients.length === 0) { - defaultRuntime.error( - danger( - "No destination found. Add inbound.allowFrom numbers or pass --to .", - ), - ); - defaultRuntime.exit(1); - } - - const overrideBody = - (opts.message as string | undefined) || - (opts.body as string | undefined) || - undefined; - const dryRun = Boolean(opts.dryRun); - - try { - for (const to of recipients) { - await runWebHeartbeatOnce({ - to, - verbose: Boolean(opts.verbose), - runtime: defaultRuntime, - sessionId: opts.sessionId, - overrideBody, - dryRun, - }); - } - } catch { - defaultRuntime.exit(1); - } - }); - program .command("gateway") .description("Run the WebSocket Gateway (replaces relay)") @@ -505,339 +403,6 @@ Examples: } }), ); - - program - .command("relay") - .description( - "Auto-reply to inbound messages across configured providers (web, Telegram)", - ) - .option( - "--provider ", - "Which providers to start: auto (default), web, telegram, or all", - ) - .option( - "--web-heartbeat ", - "Heartbeat interval for web relay health logs (seconds)", - ) - .option( - "--web-retries ", - "Max consecutive web reconnect attempts before exit (0 = unlimited)", - ) - .option( - "--web-retry-initial ", - "Initial reconnect backoff for web relay (ms)", - ) - .option("--web-retry-max ", "Max reconnect backoff for web relay (ms)") - .option( - "--heartbeat-now", - "Run a heartbeat immediately when relay starts", - false, - ) - .option( - "--webhook", - "Run Telegram webhook server instead of long-poll", - false, - ) - .option( - "--webhook-path ", - "Telegram webhook path (default /telegram-webhook when webhook enabled)", - ) - .option( - "--webhook-secret ", - "Secret token to verify Telegram webhook requests", - ) - .option("--port ", "Port for Telegram webhook server (default 8787)") - .option( - "--webhook-url ", - "Public Telegram webhook URL to register (overrides localhost autodetect)", - ) - .option("--verbose", "Verbose logging", false) - .addHelpText( - "after", - ` -Examples: - clawdis relay # starts WhatsApp; also Telegram if bot token set - clawdis relay --provider web # force WhatsApp-only - clawdis relay --provider telegram # Telegram-only (needs TELEGRAM_BOT_TOKEN) - clawdis relay --heartbeat-now # send immediate agent heartbeat on start (web) - clawdis relay --web-heartbeat 60 # override WhatsApp heartbeat interval - # Troubleshooting: docs/refactor/web-relay-troubleshooting.md -`, - ) - .action(async (_opts) => { - defaultRuntime.error( - danger( - "`clawdis relay` is deprecated. Use the WebSocket Gateway (`clawdis gateway`) plus gw:* commands or WebChat/mac app clients.", - ), - ); - defaultRuntime.exit(1); - }); - - // relay is deprecated; gateway is the single entry point. - - program - .command("relay-legacy") - .description( - "(Deprecated) legacy relay for web/telegram; use `gateway` instead", - ) - .option( - "--provider ", - "Which providers to start: auto (default), web, telegram, or all", - ) - .option( - "--web-heartbeat ", - "Heartbeat interval for web relay health logs (seconds)", - ) - .option( - "--web-retries ", - "Max consecutive web reconnect attempts before exit (0 = unlimited)", - ) - .option( - "--web-retry-initial ", - "Initial reconnect backoff for web relay (ms)", - ) - .option("--web-retry-max ", "Max reconnect backoff for web relay (ms)") - .option( - "--heartbeat-now", - "Run a heartbeat immediately when relay starts", - false, - ) - .option( - "--webhook", - "Run Telegram webhook server instead of long-poll", - false, - ) - .option( - "--webhook-path ", - "Telegram webhook path (default /telegram-webhook when webhook enabled)", - ) - .option( - "--webhook-secret ", - "Secret token to verify Telegram webhook requests", - ) - .option("--port ", "Port for Telegram webhook server (default 8787)") - .option( - "--webhook-url ", - "Public Telegram webhook URL to register (overrides localhost autodetect)", - ) - .option("--verbose", "Verbose logging", false) - .addHelpText( - "after", - ` -This command is legacy and will be removed. Prefer the Gateway. -`, - ) - .action(async (opts) => { - setVerbose(Boolean(opts.verbose)); - const { file: logFile, level: logLevel } = getResolvedLoggerSettings(); - defaultRuntime.log(info(`logs: ${logFile} (level ${logLevel})`)); - - let releaseRelayLock: (() => Promise) | null = null; - try { - releaseRelayLock = await acquireRelayLock(); - } catch (err) { - if (err instanceof RelayLockError) { - defaultRuntime.error(danger(`Relay already running: ${err.message}`)); - defaultRuntime.exit(1); - return; - } - throw err; - } - - const providerOpt = (opts.provider ?? "auto").toLowerCase(); - const cfg = loadConfig(); - const telegramToken = - process.env.TELEGRAM_BOT_TOKEN ?? cfg.telegram?.botToken; - - let startWeb = false; - let startTelegram = false; - switch (providerOpt) { - case "web": - startWeb = true; - break; - case "telegram": - startTelegram = true; - break; - case "all": - startWeb = true; - startTelegram = true; - break; - default: - startWeb = true; - startTelegram = Boolean(telegramToken); - break; - } - - if (startTelegram && !telegramToken) { - defaultRuntime.error( - danger( - "Telegram relay requires TELEGRAM_BOT_TOKEN or telegram.botToken in config", - ), - ); - defaultRuntime.exit(1); - return; - } - - if (!startWeb && !startTelegram) { - defaultRuntime.error( - danger("No providers selected. Use --provider web|telegram|all."), - ); - defaultRuntime.exit(1); - return; - } - - const webHeartbeat = - opts.webHeartbeat !== undefined - ? Number.parseInt(String(opts.webHeartbeat), 10) - : undefined; - const webRetries = - opts.webRetries !== undefined - ? Number.parseInt(String(opts.webRetries), 10) - : undefined; - const webRetryInitial = - opts.webRetryInitial !== undefined - ? Number.parseInt(String(opts.webRetryInitial), 10) - : undefined; - const webRetryMax = - opts.webRetryMax !== undefined - ? Number.parseInt(String(opts.webRetryMax), 10) - : undefined; - const heartbeatNow = Boolean(opts.heartbeatNow); - if ( - webHeartbeat !== undefined && - (Number.isNaN(webHeartbeat) || webHeartbeat <= 0) - ) { - defaultRuntime.error("--web-heartbeat must be a positive integer"); - defaultRuntime.exit(1); - } - if ( - webRetries !== undefined && - (Number.isNaN(webRetries) || webRetries < 0) - ) { - defaultRuntime.error("--web-retries must be >= 0"); - defaultRuntime.exit(1); - } - if ( - webRetryInitial !== undefined && - (Number.isNaN(webRetryInitial) || webRetryInitial <= 0) - ) { - defaultRuntime.error("--web-retry-initial must be a positive integer"); - defaultRuntime.exit(1); - } - if ( - webRetryMax !== undefined && - (Number.isNaN(webRetryMax) || webRetryMax <= 0) - ) { - defaultRuntime.error("--web-retry-max must be a positive integer"); - defaultRuntime.exit(1); - } - if ( - webRetryMax !== undefined && - webRetryInitial !== undefined && - webRetryMax < webRetryInitial - ) { - defaultRuntime.error("--web-retry-max must be >= --web-retry-initial"); - defaultRuntime.exit(1); - } - - const controller = new AbortController(); - const stopAll = () => controller.abort(); - process.once("SIGINT", stopAll); - - const runners: Array> = []; - - if (startWeb) { - const webTuning: WebMonitorTuning = {}; - if (webHeartbeat !== undefined) - webTuning.heartbeatSeconds = webHeartbeat; - if (heartbeatNow) webTuning.replyHeartbeatNow = true; - const reconnect: WebMonitorTuning["reconnect"] = {}; - if (webRetries !== undefined) reconnect.maxAttempts = webRetries; - if (webRetryInitial !== undefined) - reconnect.initialMs = webRetryInitial; - if (webRetryMax !== undefined) reconnect.maxMs = webRetryMax; - if (Object.keys(reconnect).length > 0) { - webTuning.reconnect = reconnect; - } - logWebSelfId(defaultRuntime, true); - const effectiveHeartbeat = resolveHeartbeatSeconds( - cfg, - webTuning.heartbeatSeconds, - ); - const effectivePolicy = resolveReconnectPolicy( - cfg, - webTuning.reconnect, - ); - defaultRuntime.log( - info( - `Web relay health: heartbeat ${effectiveHeartbeat}s, retries ${effectivePolicy.maxAttempts || "∞"}, backoff ${effectivePolicy.initialMs}→${effectivePolicy.maxMs}ms x${effectivePolicy.factor} (jitter ${Math.round(effectivePolicy.jitter * 100)}%)`, - ), - ); - - const webchatServer = await ensureWebChatServerFromConfig(); - if (webchatServer) { - defaultRuntime.log( - info( - `webchat listening on http://127.0.0.1:${webchatServer.port}/webchat/`, - ), - ); - } - - runners.push( - monitorWebProvider( - Boolean(opts.verbose), - undefined, - true, - undefined, - defaultRuntime, - controller.signal, - webTuning, - ), - ); - } - - if (startTelegram) { - const useWebhook = Boolean(opts.webhook); - const telegramRunner = (async () => { - const { monitorTelegramProvider } = await import( - "../telegram/monitor.js" - ); - const sharedOpts = { - token: telegramToken, - runtime: defaultRuntime, - abortSignal: controller.signal, - } as const; - if (useWebhook) { - const port = opts.port - ? Number.parseInt(String(opts.port), 10) - : 8787; - const path = opts.webhookPath ?? "/telegram-webhook"; - return monitorTelegramProvider({ - ...sharedOpts, - useWebhook: true, - webhookPath: path, - webhookPort: port, - webhookSecret: opts.webhookSecret ?? cfg.telegram?.webhookSecret, - webhookUrl: opts.webhookUrl ?? cfg.telegram?.webhookUrl, - }); - } - return monitorTelegramProvider(sharedOpts); - })(); - runners.push(telegramRunner); - } - - try { - await Promise.all(runners); - } catch (err) { - defaultRuntime.error(danger(`Relay failed: ${String(err)}`)); - defaultRuntime.exit(1); - } finally { - if (releaseRelayLock) await releaseRelayLock(); - } - }); - - // relay is the single entry point; heartbeat/Telegram helpers removed. - program .command("status") .description("Show web session health and recent session recipients")