chore: format swift/ts and fix gateway lint

This commit is contained in:
Peter Steinberger
2025-12-09 17:11:25 +00:00
parent b6bd39660f
commit 8d888b426f
18 changed files with 205 additions and 121 deletions

View File

@@ -26,11 +26,14 @@ final class AppState: ObservableObject {
} }
@Published var onboardingSeen: Bool { @Published var onboardingSeen: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } } didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") }
}
} }
@Published var debugPaneEnabled: Bool { @Published var debugPaneEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } } didSet {
self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
}
} }
@Published var swabbleEnabled: Bool { @Published var swabbleEnabled: Bool {
@@ -63,7 +66,9 @@ final class AppState: ObservableObject {
} }
@Published var iconAnimationsEnabled: Bool { @Published var iconAnimationsEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } } didSet { self.ifNotPreview { UserDefaults.standard.set(
self.iconAnimationsEnabled,
forKey: iconAnimationsEnabledKey) } }
} }
@Published var showDockIcon: Bool { @Published var showDockIcon: Bool {
@@ -98,19 +103,27 @@ final class AppState: ObservableObject {
} }
@Published var voiceWakeAdditionalLocaleIDs: [String] { @Published var voiceWakeAdditionalLocaleIDs: [String] {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } } didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voiceWakeAdditionalLocaleIDs,
forKey: voiceWakeAdditionalLocalesKey) } }
} }
@Published var voiceWakeForwardEnabled: Bool { @Published var voiceWakeForwardEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } } didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voiceWakeForwardEnabled,
forKey: voiceWakeForwardEnabledKey) } }
} }
@Published var voiceWakeForwardCommand: String { @Published var voiceWakeForwardCommand: String {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } } didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voiceWakeForwardCommand,
forKey: voiceWakeForwardCommandKey) } }
} }
@Published var voicePushToTalkEnabled: Bool { @Published var voicePushToTalkEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } } didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voicePushToTalkEnabled,
forKey: voicePushToTalkEnabledKey) } }
} }
@Published var iconOverride: IconOverrideSelection { @Published var iconOverride: IconOverrideSelection {
@@ -131,7 +144,9 @@ final class AppState: ObservableObject {
} }
@Published var connectionMode: ConnectionMode { @Published var connectionMode: ConnectionMode {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } } didSet {
self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) }
}
} }
@Published var webChatEnabled: Bool { @Published var webChatEnabled: Bool {

View File

@@ -1,7 +1,7 @@
import ClawdisProtocol
import Foundation import Foundation
import OSLog import OSLog
import SwiftUI import SwiftUI
import ClawdisProtocol
struct ControlHeartbeatEvent: Codable { struct ControlHeartbeatEvent: Codable {
let ts: Double let ts: Double
@@ -14,7 +14,7 @@ struct ControlHeartbeatEvent: Codable {
} }
struct ControlAgentEvent: Codable, Sendable, Identifiable { struct ControlAgentEvent: Codable, Sendable, Identifiable {
var id: String { "\(runId)-\(seq)" } var id: String { "\(self.runId)-\(self.seq)" }
let runId: String let runId: String
let seq: Int let seq: Int
let stream: String let stream: String
@@ -173,7 +173,8 @@ final class ControlChannel: ObservableObject {
if let data = evt.payload?.value, if let data = evt.payload?.value,
JSONSerialization.isValidJSONObject(data), JSONSerialization.isValidJSONObject(data),
let blob = try? JSONSerialization.data(withJSONObject: data), let blob = try? JSONSerialization.data(withJSONObject: data),
let agent = try? JSONDecoder().decode(AgentEvent.self, from: blob) { let agent = try? JSONDecoder().decode(AgentEvent.self, from: blob)
{
Task { @MainActor in Task { @MainActor in
AgentEventStore.shared.append(ControlAgentEvent( AgentEventStore.shared.append(ControlAgentEvent(
runId: agent.runid, runId: agent.runid,

View File

@@ -108,7 +108,8 @@ enum DebugActionError: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case let .message(text): text case let .message(text):
text
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import ClawdisProtocol
import Foundation import Foundation
import OSLog import OSLog
import ClawdisProtocol
struct GatewayEvent: Codable { struct GatewayEvent: Codable {
let type: String let type: String
@@ -27,7 +27,7 @@ private actor GatewayChannelActor {
private var shouldReconnect = true private var shouldReconnect = true
private var lastSeq: Int? private var lastSeq: Int?
private var lastTick: Date? private var lastTick: Date?
private var tickIntervalMs: Double = 30_000 private var tickIntervalMs: Double = 30000
private let decoder = JSONDecoder() private let decoder = JSONDecoder()
private let encoder = JSONEncoder() private let encoder = JSONEncoder()
@@ -88,7 +88,8 @@ private actor GatewayChannelActor {
let type = obj["type"] as? String else { return false } let type = obj["type"] as? String else { return false }
if type == "hello-ok" { if type == "hello-ok" {
if let policy = obj["policy"] as? [String: Any], if let policy = obj["policy"] as? [String: Any],
let tick = policy["tickIntervalMs"] as? Double { let tick = policy["tickIntervalMs"] as? Double
{
self.tickIntervalMs = tick self.tickIntervalMs = tick
} }
self.lastTick = Date() self.lastTick = Date()

View File

@@ -296,7 +296,9 @@ struct GeneralSettings: View {
.disabled(self.relayInstalling) .disabled(self.relayInstalling)
} }
Text(self.relayInstallMessage ?? "Installs the global \"clawdis\" package and expects the gateway on port 18789.") Text(self
.relayInstallMessage ??
"Installs the global \"clawdis\" package and expects the gateway on port 18789.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(2) .lineLimit(2)

View File

@@ -151,15 +151,15 @@ final class HealthStore: ObservableObject {
/// Short, human-friendly detail for the last failure, used in the UI. /// Short, human-friendly detail for the last failure, used in the UI.
var detailLine: String? { var detailLine: String? {
if let error = self.lastError, !error.isEmpty { if let error = self.lastError, !error.isEmpty {
let lower = error.lowercased() let lower = error.lowercased()
if lower.contains("connection refused") { if lower.contains("connection refused") {
return "The gateway control port (127.0.0.1:18789) isnt listening — restart Clawdis to bring it back." return "The gateway control port (127.0.0.1:18789) isnt listening — restart Clawdis to bring it back."
} }
if lower.contains("timeout") { if lower.contains("timeout") {
return "Timed out waiting for the control server; the relay may be crashed or still starting." return "Timed out waiting for the control server; the relay may be crashed or still starting."
} }
return error return error
} }
return nil return nil
} }

View File

@@ -1,7 +1,7 @@
import ClawdisProtocol
import Cocoa import Cocoa
import Foundation import Foundation
import OSLog import OSLog
import ClawdisProtocol
struct InstanceInfo: Identifiable, Codable { struct InstanceInfo: Identifiable, Codable {
let id: String let id: String
@@ -79,7 +79,8 @@ final class InstancesStore: ObservableObject {
case let .event(evt) where evt.event == "presence": case let .event(evt) where evt.event == "presence":
if let payload = evt.payload?.value as? [String: Any], if let payload = evt.payload?.value as? [String: Any],
let presence = payload["presence"], let presence = payload["presence"],
let presenceData = try? JSONSerialization.data(withJSONObject: presence) { let presenceData = try? JSONSerialization.data(withJSONObject: presence)
{
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(presenceData) } Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(presenceData) }
} }
default: default:
@@ -104,7 +105,8 @@ final class InstancesStore: ObservableObject {
switch frame { switch frame {
case let .helloOk(hello): case let .helloOk(hello):
if JSONSerialization.isValidJSONObject(hello.snapshot.presence), if JSONSerialization.isValidJSONObject(hello.snapshot.presence),
let data = try? JSONEncoder().encode(hello.snapshot.presence) { let data = try? JSONEncoder().encode(hello.snapshot.presence)
{
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) } Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) }
} }
default: default:
@@ -323,7 +325,7 @@ extension InstancesStore {
mode: "remote", mode: "remote",
reason: "preview", reason: "preview",
text: "Relay node · tunnel ok", text: "Relay node · tunnel ok",
ts: Date().timeIntervalSince1970 * 1000 - 45_000), ts: Date().timeIntervalSince1970 * 1000 - 45000),
]) -> InstancesStore { ]) -> InstancesStore {
let store = InstancesStore(isPreview: true) let store = InstancesStore(isPreview: true)
store.instances = instances store.instances = instances

View File

@@ -181,7 +181,8 @@ struct OnboardingView: View {
self.onboardingPage { self.onboardingPage {
Text("Install the relay") Text("Install the relay")
.font(.largeTitle.weight(.semibold)) .font(.largeTitle.weight(.semibold))
Text("Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and well check Node for you.") Text(
"Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and well check Node for you.")
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -132,9 +132,8 @@ struct PermissionsSettings_Previews: PreviewProvider {
.speechRecognition: false, .speechRecognition: false,
], ],
refresh: {}, refresh: {},
showOnboarding: {} showOnboarding: {})
) .frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
} }
} }
#endif #endif

View File

@@ -1,5 +1,5 @@
import Foundation
import ClawdisIPC import ClawdisIPC
import Foundation
// Lightweight SemVer helper (major.minor.patch only) for relay compatibility checks. // Lightweight SemVer helper (major.minor.patch only) for relay compatibility checks.
struct Semver: Comparable, CustomStringConvertible, Sendable { struct Semver: Comparable, CustomStringConvertible, Sendable {

View File

@@ -126,7 +126,7 @@ extension SessionRow {
id: "global", id: "global",
key: "global", key: "global",
kind: .global, kind: .global,
updatedAt: Date().addingTimeInterval(-86_400), updatedAt: Date().addingTimeInterval(-86400),
sessionId: nil, sessionId: nil,
thinkingLevel: nil, thinkingLevel: nil,
verboseLevel: nil, verboseLevel: nil,

View File

@@ -101,7 +101,14 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
private func loadWebChat(baseEndpoint: URL) { private func loadWebChat(baseEndpoint: URL) {
var comps = URLComponents(url: baseEndpoint.appendingPathComponent("webchat/"), resolvingAgainstBaseURL: false) var comps = URLComponents(url: baseEndpoint.appendingPathComponent("webchat/"), resolvingAgainstBaseURL: false)
comps?.queryItems = [URLQueryItem(name: "session", value: self.sessionKey)] var items = [URLQueryItem(name: "session", value: self.sessionKey)]
if let hostName = Host.current().localizedName ?? Host.current().name {
items.append(URLQueryItem(name: "host", value: hostName))
}
if let ip = Self.primaryIPv4Address() {
items.append(URLQueryItem(name: "ip", value: ip))
}
comps?.queryItems = items
guard let url = comps?.url else { guard let url = comps?.url else {
self.showError("invalid webchat url") self.showError("invalid webchat url")
return return
@@ -197,6 +204,37 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
} }
} }
extension WebChatWindowController {
/// Returns the first non-loopback IPv4 address, skipping link-local (169.254.x.x).
fileprivate static func primaryIPv4Address() -> String? {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0, let first = ifaddr else { return nil }
defer { freeifaddrs(ifaddr) }
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let addrFamily = ptr.pointee.ifa_addr.pointee.sa_family
if (flags & IFF_UP) == 0 || (flags & IFF_LOOPBACK) != 0 { continue }
if addrFamily == UInt8(AF_INET) {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if getnameinfo(
ptr.pointee.ifa_addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&hostname,
socklen_t(hostname.count),
nil,
0,
NI_NUMERICHOST) == 0
{
let ip = String(cString: hostname)
if !ip.hasPrefix("169.254") { return ip }
}
}
}
return nil
}
}
// MARK: - Manager // MARK: - Manager
@MainActor @MainActor

View File

@@ -28,8 +28,8 @@ public struct Hello: Codable {
caps: [String]?, caps: [String]?,
auth: [String: AnyCodable]?, auth: [String: AnyCodable]?,
locale: String?, locale: String?,
useragent: String? useragent: String?)
) { {
self.type = type self.type = type
self.minprotocol = minprotocol self.minprotocol = minprotocol
self.maxprotocol = maxprotocol self.maxprotocol = maxprotocol
@@ -39,6 +39,7 @@ public struct Hello: Codable {
self.locale = locale self.locale = locale
self.useragent = useragent self.useragent = useragent
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case minprotocol = "minProtocol" case minprotocol = "minProtocol"
@@ -65,8 +66,8 @@ public struct HelloOk: Codable {
server: [String: AnyCodable], server: [String: AnyCodable],
features: [String: AnyCodable], features: [String: AnyCodable],
snapshot: Snapshot, snapshot: Snapshot,
policy: [String: AnyCodable] policy: [String: AnyCodable])
) { {
self.type = type self.type = type
self._protocol = _protocol self._protocol = _protocol
self.server = server self.server = server
@@ -74,6 +75,7 @@ public struct HelloOk: Codable {
self.snapshot = snapshot self.snapshot = snapshot
self.policy = policy self.policy = policy
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case _protocol = "protocol" case _protocol = "protocol"
@@ -94,13 +96,14 @@ public struct HelloError: Codable {
type: String, type: String,
reason: String, reason: String,
expectedprotocol: Int?, expectedprotocol: Int?,
minclient: String? minclient: String?)
) { {
self.type = type self.type = type
self.reason = reason self.reason = reason
self.expectedprotocol = expectedprotocol self.expectedprotocol = expectedprotocol
self.minclient = minclient self.minclient = minclient
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case reason case reason
@@ -119,13 +122,14 @@ public struct RequestFrame: Codable {
type: String, type: String,
id: String, id: String,
method: String, method: String,
params: AnyCodable? params: AnyCodable?)
) { {
self.type = type self.type = type
self.id = id self.id = id
self.method = method self.method = method
self.params = params self.params = params
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case id case id
@@ -146,14 +150,15 @@ public struct ResponseFrame: Codable {
id: String, id: String,
ok: Bool, ok: Bool,
payload: AnyCodable?, payload: AnyCodable?,
error: [String: AnyCodable]? error: [String: AnyCodable]?)
) { {
self.type = type self.type = type
self.id = id self.id = id
self.ok = ok self.ok = ok
self.payload = payload self.payload = payload
self.error = error self.error = error
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case id case id
@@ -175,14 +180,15 @@ public struct EventFrame: Codable {
event: String, event: String,
payload: AnyCodable?, payload: AnyCodable?,
seq: Int?, seq: Int?,
stateversion: [String: AnyCodable]? stateversion: [String: AnyCodable]?)
) { {
self.type = type self.type = type
self.event = event self.event = event
self.payload = payload self.payload = payload
self.seq = seq self.seq = seq
self.stateversion = stateversion self.stateversion = stateversion
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case event case event
@@ -214,8 +220,8 @@ public struct PresenceEntry: Codable {
tags: [String]?, tags: [String]?,
text: String?, text: String?,
ts: Int, ts: Int,
instanceid: String? instanceid: String?)
) { {
self.host = host self.host = host
self.ip = ip self.ip = ip
self.version = version self.version = version
@@ -227,6 +233,7 @@ public struct PresenceEntry: Codable {
self.ts = ts self.ts = ts
self.instanceid = instanceid self.instanceid = instanceid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case host case host
case ip case ip
@@ -247,11 +254,12 @@ public struct StateVersion: Codable {
public init( public init(
presence: Int, presence: Int,
health: Int health: Int)
) { {
self.presence = presence self.presence = presence
self.health = health self.health = health
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case presence case presence
case health case health
@@ -268,13 +276,14 @@ public struct Snapshot: Codable {
presence: [PresenceEntry], presence: [PresenceEntry],
health: AnyCodable, health: AnyCodable,
stateversion: StateVersion, stateversion: StateVersion,
uptimems: Int uptimems: Int)
) { {
self.presence = presence self.presence = presence
self.health = health self.health = health
self.stateversion = stateversion self.stateversion = stateversion
self.uptimems = uptimems self.uptimems = uptimems
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case presence case presence
case health case health
@@ -295,14 +304,15 @@ public struct ErrorShape: Codable {
message: String, message: String,
details: AnyCodable?, details: AnyCodable?,
retryable: Bool?, retryable: Bool?,
retryafterms: Int? retryafterms: Int?)
) { {
self.code = code self.code = code
self.message = message self.message = message
self.details = details self.details = details
self.retryable = retryable self.retryable = retryable
self.retryafterms = retryafterms self.retryafterms = retryafterms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case code case code
case message case message
@@ -324,14 +334,15 @@ public struct AgentEvent: Codable {
seq: Int, seq: Int,
stream: String, stream: String,
ts: Int, ts: Int,
data: [String: AnyCodable] data: [String: AnyCodable])
) { {
self.runid = runid self.runid = runid
self.seq = seq self.seq = seq
self.stream = stream self.stream = stream
self.ts = ts self.ts = ts
self.data = data self.data = data
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case runid = "runId" case runid = "runId"
case seq case seq
@@ -353,14 +364,15 @@ public struct SendParams: Codable {
message: String, message: String,
mediaurl: String?, mediaurl: String?,
provider: String?, provider: String?,
idempotencykey: String idempotencykey: String)
) { {
self.to = to self.to = to
self.message = message self.message = message
self.mediaurl = mediaurl self.mediaurl = mediaurl
self.provider = provider self.provider = provider
self.idempotencykey = idempotencykey self.idempotencykey = idempotencykey
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case to case to
case message case message
@@ -386,8 +398,8 @@ public struct AgentParams: Codable {
thinking: String?, thinking: String?,
deliver: Bool?, deliver: Bool?,
timeout: Int?, timeout: Int?,
idempotencykey: String idempotencykey: String)
) { {
self.message = message self.message = message
self.to = to self.to = to
self.sessionid = sessionid self.sessionid = sessionid
@@ -396,6 +408,7 @@ public struct AgentParams: Codable {
self.timeout = timeout self.timeout = timeout
self.idempotencykey = idempotencykey self.idempotencykey = idempotencykey
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case message case message
case to case to
@@ -411,10 +424,11 @@ public struct TickEvent: Codable {
public let ts: Int public let ts: Int
public init( public init(
ts: Int ts: Int)
) { {
self.ts = ts self.ts = ts
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case ts case ts
} }
@@ -426,11 +440,12 @@ public struct ShutdownEvent: Codable {
public init( public init(
reason: String, reason: String,
restartexpectedms: Int? restartexpectedms: Int?)
) { {
self.reason = reason self.reason = reason
self.restartexpectedms = restartexpectedms self.restartexpectedms = restartexpectedms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case reason case reason
case restartexpectedms = "restartExpectedMs" case restartexpectedms = "restartExpectedMs"
@@ -454,17 +469,17 @@ public enum GatewayFrame: Codable {
} }
switch type { switch type {
case "hello": case "hello":
self = .hello(try Self.decodePayload(Hello.self, from: raw)) self = try .hello(Self.decodePayload(Hello.self, from: raw))
case "hello-ok": case "hello-ok":
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw)) self = try .helloOk(Self.decodePayload(HelloOk.self, from: raw))
case "hello-error": case "hello-error":
self = .helloError(try Self.decodePayload(HelloError.self, from: raw)) self = try .helloError(Self.decodePayload(HelloError.self, from: raw))
case "req": case "req":
self = .req(try Self.decodePayload(RequestFrame.self, from: raw)) self = try .req(Self.decodePayload(RequestFrame.self, from: raw))
case "res": case "res":
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw)) self = try .res(Self.decodePayload(ResponseFrame.self, from: raw))
case "event": case "event":
self = .event(try Self.decodePayload(EventFrame.self, from: raw)) self = try .event(Self.decodePayload(EventFrame.self, from: raw))
default: default:
self = .unknown(type: type, raw: raw) self = .unknown(type: type, raw: raw)
} }
@@ -472,23 +487,21 @@ public enum GatewayFrame: Codable {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
switch self { switch self {
case .hello(let v): try v.encode(to: encoder) case let .hello(v): try v.encode(to: encoder)
case .helloOk(let v): try v.encode(to: encoder) case let .helloOk(v): try v.encode(to: encoder)
case .helloError(let v): try v.encode(to: encoder) case let .helloError(v): try v.encode(to: encoder)
case .req(let v): try v.encode(to: encoder) case let .req(v): try v.encode(to: encoder)
case .res(let v): try v.encode(to: encoder) case let .res(v): try v.encode(to: encoder)
case .event(let v): try v.encode(to: encoder) case let .event(v): try v.encode(to: encoder)
case .unknown(_, let raw): case let .unknown(_, raw):
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(raw) try container.encode(raw)
} }
} }
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T { private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
let data = try JSONSerialization.data(withJSONObject: raw) let data = try JSONSerialization.data(withJSONObject: raw)
let decoder = JSONDecoder() let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data) return try decoder.decode(T.self, from: data)
} }
} }

View File

@@ -624,7 +624,7 @@ export async function getReplyFromConfig(
systemLines.push(...queued); systemLines.push(...queued);
if (isNewSession) { if (isNewSession) {
const summary = await buildProviderSummary(cfg); const summary = await buildProviderSummary(cfg);
if (summary) systemLines.unshift(summary); if (summary.length > 0) systemLines.unshift(...summary);
} }
if (systemLines.length > 0) { if (systemLines.length > 0) {
const block = systemLines.map((l) => `System: ${l}`).join("\n"); const block = systemLines.map((l) => `System: ${l}`).join("\n");

View File

@@ -1,9 +1,10 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("node:child_process", async () => { vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>( const actual =
"node:child_process", await vi.importActual<typeof import("node:child_process")>(
); "node:child_process",
);
return { return {
...actual, ...actual,
execFileSync: vi.fn(), execFileSync: vi.fn(),
@@ -14,8 +15,8 @@ import { execFileSync } from "node:child_process";
import { import {
forceFreePort, forceFreePort,
listPortListeners, listPortListeners,
parseLsofOutput,
type PortProcess, type PortProcess,
parseLsofOutput,
} from "./program.js"; } from "./program.js";
describe("gateway --force helpers", () => { describe("gateway --force helpers", () => {

View File

@@ -116,10 +116,7 @@ export function buildProgram() {
'clawdis send --to +15555550123 --message "Hi" --json', 'clawdis send --to +15555550123 --message "Hi" --json',
"Send via your web session and print JSON result.", "Send via your web session and print JSON result.",
], ],
[ ["clawdis gateway --port 18789", "Run the WebSocket Gateway locally."],
"clawdis gateway --port 18789",
"Run the WebSocket Gateway locally.",
],
[ [
"clawdis gateway --force", "clawdis gateway --force",
"Kill anything bound to the default gateway port, then start it.", "Kill anything bound to the default gateway port, then start it.",

View File

@@ -15,11 +15,11 @@ import {
} from "../infra/system-presence.js"; } from "../infra/system-presence.js";
import { logError } from "../logger.js"; import { logError } from "../logger.js";
import { getResolvedLoggerSettings } from "../logging.js"; import { getResolvedLoggerSettings } from "../logging.js";
import { defaultRuntime } from "../runtime.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js"; import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import { sendMessageWhatsApp } from "../web/outbound.js"; import { defaultRuntime } from "../runtime.js";
import { monitorTelegramProvider } from "../telegram/monitor.js"; import { monitorTelegramProvider } from "../telegram/monitor.js";
import { sendMessageTelegram } from "../telegram/send.js"; import { sendMessageTelegram } from "../telegram/send.js";
import { sendMessageWhatsApp } from "../web/outbound.js";
import { import {
ErrorCodes, ErrorCodes,
type ErrorShape, type ErrorShape,
@@ -450,32 +450,41 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
const message = params.message.trim(); const message = params.message.trim();
const provider = (params.provider ?? "whatsapp").toLowerCase(); const provider = (params.provider ?? "whatsapp").toLowerCase();
try { try {
const result = if (provider === "telegram") {
provider === "telegram" const result = await sendMessageTelegram(to, message, {
? await sendMessageTelegram(to, message, { mediaUrl: params.mediaUrl,
mediaUrl: params.mediaUrl, verbose: isVerbose(),
verbose: isVerbose(), });
}) const payload = {
: await sendMessageWhatsApp(to, message, { runId: idem,
mediaUrl: params.mediaUrl, messageId: result.messageId,
verbose: isVerbose(), chatId: result.chatId,
}); provider,
const payload = };
provider === "telegram" dedupe.set(`send:${idem}`, {
? { ts: Date.now(),
runId: idem, ok: true,
messageId: result.messageId, payload,
chatId: result.chatId, });
provider, respond(true, payload, undefined);
} } else {
: { const result = await sendMessageWhatsApp(to, message, {
runId: idem, mediaUrl: params.mediaUrl,
messageId: result.messageId, verbose: isVerbose(),
toJid: result.toJid ?? `${to}@s.whatsapp.net`, });
provider, const payload = {
}; runId: idem,
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: true, payload }); messageId: result.messageId,
respond(true, payload, undefined); toJid: result.toJid ?? `${to}@s.whatsapp.net`,
provider,
};
dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined);
}
} catch (err) { } catch (err) {
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err)); const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: false, error }); dedupe.set(`send:${idem}`, { ts: Date.now(), ok: false, error });
@@ -594,7 +603,9 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") { if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
void startProviders(); void startProviders();
} else { } else {
defaultRuntime.log("gateway: skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)"); defaultRuntime.log(
"gateway: skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)",
);
} }
return { return {

View File

@@ -21,7 +21,9 @@ export async function buildProviderSummary(
const { e164 } = readWebSelfId(); const { e164 } = readWebSelfId();
lines.push( lines.push(
webLinked webLinked
? chalk.green(`WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`) ? chalk.green(
`WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`,
)
: chalk.red("WhatsApp: not linked"), : chalk.red("WhatsApp: not linked"),
); );