feat(bridge): add Bonjour node bridge

This commit is contained in:
Peter Steinberger
2025-12-12 21:18:46 +00:00
parent b9007dc721
commit 0b532579d8
17 changed files with 1002 additions and 13 deletions

View File

@@ -0,0 +1,22 @@
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "ClawdisNodeKit",
platforms: [
.iOS(.v17),
.macOS(.v15),
],
products: [
.library(name: "ClawdisNodeKit", targets: ["ClawdisNodeKit"]),
],
targets: [
.target(
name: "ClawdisNodeKit",
dependencies: [],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
]),
])

View File

@@ -0,0 +1,7 @@
import Foundation
public enum ClawdisBonjour {
// v0: internal-only, subject to rename.
public static let bridgeServiceType = "_clawdis-bridge._tcp"
public static let bridgeServiceDomain = "local."
}

View File

@@ -0,0 +1,156 @@
import Foundation
public struct BridgeBaseFrame: Codable, Sendable {
public let type: String
public init(type: String) {
self.type = type
}
}
public struct BridgeInvokeRequest: Codable, Sendable {
public let type: String
public let id: String
public let command: String
public let paramsJSON: String?
public init(type: String = "invoke", id: String, command: String, paramsJSON: String? = nil) {
self.type = type
self.id = id
self.command = command
self.paramsJSON = paramsJSON
}
}
public struct BridgeInvokeResponse: Codable, Sendable {
public let type: String
public let id: String
public let ok: Bool
public let payloadJSON: String?
public let error: ClawdisNodeError?
public init(
type: String = "invoke-res",
id: String,
ok: Bool,
payloadJSON: String? = nil,
error: ClawdisNodeError? = nil)
{
self.type = type
self.id = id
self.ok = ok
self.payloadJSON = payloadJSON
self.error = error
}
}
public struct BridgeEventFrame: Codable, Sendable {
public let type: String
public let event: String
public let payloadJSON: String?
public init(type: String = "event", event: String, payloadJSON: String? = nil) {
self.type = type
self.event = event
self.payloadJSON = payloadJSON
}
}
public struct BridgeHello: Codable, Sendable {
public let type: String
public let nodeId: String
public let displayName: String?
public let token: String?
public let platform: String?
public let version: String?
public init(
type: String = "hello",
nodeId: String,
displayName: String?,
token: String?,
platform: String?,
version: String?)
{
self.type = type
self.nodeId = nodeId
self.displayName = displayName
self.token = token
self.platform = platform
self.version = version
}
}
public struct BridgeHelloOk: Codable, Sendable {
public let type: String
public let serverName: String
public init(type: String = "hello-ok", serverName: String) {
self.type = type
self.serverName = serverName
}
}
public struct BridgePairRequest: Codable, Sendable {
public let type: String
public let nodeId: String
public let displayName: String?
public let platform: String?
public let version: String?
public init(
type: String = "pair-request",
nodeId: String,
displayName: String?,
platform: String?,
version: String?)
{
self.type = type
self.nodeId = nodeId
self.displayName = displayName
self.platform = platform
self.version = version
}
}
public struct BridgePairOk: Codable, Sendable {
public let type: String
public let token: String
public init(type: String = "pair-ok", token: String) {
self.type = type
self.token = token
}
}
public struct BridgePing: Codable, Sendable {
public let type: String
public let id: String
public init(type: String = "ping", id: String) {
self.type = type
self.id = id
}
}
public struct BridgePong: Codable, Sendable {
public let type: String
public let id: String
public init(type: String = "pong", id: String) {
self.type = type
self.id = id
}
}
public struct BridgeErrorFrame: Codable, Sendable {
public let type: String
public let code: String
public let message: String
public init(type: String = "error", code: String, message: String) {
self.type = type
self.code = code
self.message = message
}
}

View File

@@ -0,0 +1,28 @@
import Foundation
public enum ClawdisNodeErrorCode: String, Codable, Sendable {
case notPaired = "NOT_PAIRED"
case unauthorized = "UNAUTHORIZED"
case backgroundUnavailable = "NODE_BACKGROUND_UNAVAILABLE"
case invalidRequest = "INVALID_REQUEST"
case unavailable = "UNAVAILABLE"
}
public struct ClawdisNodeError: Error, Codable, Sendable, Equatable {
public var code: ClawdisNodeErrorCode
public var message: String
public var retryable: Bool?
public var retryAfterMs: Int?
public init(
code: ClawdisNodeErrorCode,
message: String,
retryable: Bool? = nil,
retryAfterMs: Int? = nil)
{
self.code = code
self.message = message
self.retryable = retryable
self.retryAfterMs = retryAfterMs
}
}

View File

@@ -0,0 +1,56 @@
import Foundation
public enum ClawdisScreenMode: String, Codable, Sendable {
case canvas
case web
}
public enum ClawdisScreenCommand: String, Codable, Sendable {
case show = "screen.show"
case hide = "screen.hide"
case setMode = "screen.setMode"
case navigate = "screen.navigate"
case evalJS = "screen.eval"
case snapshot = "screen.snapshot"
}
public struct ClawdisScreenNavigateParams: Codable, Sendable, Equatable {
public var url: String
public init(url: String) {
self.url = url
}
}
public struct ClawdisScreenSetModeParams: Codable, Sendable, Equatable {
public var mode: ClawdisScreenMode
public init(mode: ClawdisScreenMode) {
self.mode = mode
}
}
public struct ClawdisScreenEvalParams: Codable, Sendable, Equatable {
public var javaScript: String
public init(javaScript: String) {
self.javaScript = javaScript
}
}
public enum ClawdisSnapshotFormat: String, Codable, Sendable {
case png
case jpeg
}
public struct ClawdisScreenSnapshotParams: Codable, Sendable, Equatable {
public var maxWidth: Int?
public var quality: Double?
public var format: ClawdisSnapshotFormat?
public init(maxWidth: Int? = nil, quality: Double? = nil, format: ClawdisSnapshotFormat? = nil) {
self.maxWidth = maxWidth
self.quality = quality
self.format = format
}
}

View File

@@ -0,0 +1,37 @@
import Foundation
public enum ClawdisNodeStorage {
public static func appSupportDir() throws -> URL {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
guard let base else {
throw NSError(domain: "ClawdisNodeStorage", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Application Support directory unavailable",
])
}
return base.appendingPathComponent("Clawdis", isDirectory: true)
}
public static func canvasRoot(sessionKey: String) throws -> URL {
let root = try appSupportDir().appendingPathComponent("canvas", isDirectory: true)
let safe = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let session = safe.isEmpty ? "main" : safe
return root.appendingPathComponent(session, isDirectory: true)
}
public static func cachesDir() throws -> URL {
let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
guard let base else {
throw NSError(domain: "ClawdisNodeStorage", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Caches directory unavailable",
])
}
return base.appendingPathComponent("Clawdis", isDirectory: true)
}
public static func canvasSnapshotsRoot(sessionKey: String) throws -> URL {
let root = try cachesDir().appendingPathComponent("canvas-snapshots", isDirectory: true)
let safe = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let session = safe.isEmpty ? "main" : safe
return root.appendingPathComponent(session, isDirectory: true)
}
}