chore: format swift/ts and fix gateway lint
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) isn’t listening — restart Clawdis to bring it back."
|
return "The gateway control port (127.0.0.1:18789) isn’t 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 we’ll check Node for you.")
|
Text(
|
||||||
|
"Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and we’ll check Node for you.")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user