refactor: consolidate mac debug CLI
This commit is contained in:
@@ -12,8 +12,7 @@ let package = Package(
|
|||||||
.library(name: "ClawdbotIPC", targets: ["ClawdbotIPC"]),
|
.library(name: "ClawdbotIPC", targets: ["ClawdbotIPC"]),
|
||||||
.library(name: "ClawdbotDiscovery", targets: ["ClawdbotDiscovery"]),
|
.library(name: "ClawdbotDiscovery", targets: ["ClawdbotDiscovery"]),
|
||||||
.executable(name: "Clawdbot", targets: ["Clawdbot"]),
|
.executable(name: "Clawdbot", targets: ["Clawdbot"]),
|
||||||
.executable(name: "clawdbot-mac-discovery", targets: ["ClawdbotDiscoveryCLI"]),
|
.executable(name: "clawdbot-mac", targets: ["ClawdbotMacCLI"]),
|
||||||
.executable(name: "clawdbot-mac-wizard", targets: ["ClawdbotWizardCLI"]),
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/orchetect/MenuBarExtraAccess", exact: "1.2.2"),
|
.package(url: "https://github.com/orchetect/MenuBarExtraAccess", exact: "1.2.2"),
|
||||||
@@ -67,21 +66,13 @@ let package = Package(
|
|||||||
.enableUpcomingFeature("StrictConcurrency"),
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
]),
|
]),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "ClawdbotDiscoveryCLI",
|
name: "ClawdbotMacCLI",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"ClawdbotDiscovery",
|
"ClawdbotDiscovery",
|
||||||
],
|
|
||||||
path: "Sources/ClawdbotDiscoveryCLI",
|
|
||||||
swiftSettings: [
|
|
||||||
.enableUpcomingFeature("StrictConcurrency"),
|
|
||||||
]),
|
|
||||||
.executableTarget(
|
|
||||||
name: "ClawdbotWizardCLI",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "ClawdbotKit", package: "ClawdbotKit"),
|
.product(name: "ClawdbotKit", package: "ClawdbotKit"),
|
||||||
.product(name: "ClawdbotProtocol", package: "ClawdbotKit"),
|
.product(name: "ClawdbotProtocol", package: "ClawdbotKit"),
|
||||||
],
|
],
|
||||||
path: "Sources/ClawdbotWizardCLI",
|
path: "Sources/ClawdbotMacCLI",
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
.enableUpcomingFeature("StrictConcurrency"),
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import ClawdbotDiscovery
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct DiscoveryOptions {
|
|
||||||
var timeoutMs: Int = 2000
|
|
||||||
var json: Bool = false
|
|
||||||
var includeLocal: Bool = false
|
|
||||||
var help: Bool = false
|
|
||||||
|
|
||||||
static func parse(_ args: [String]) -> DiscoveryOptions {
|
|
||||||
var opts = DiscoveryOptions()
|
|
||||||
var i = 0
|
|
||||||
while i < args.count {
|
|
||||||
let arg = args[i]
|
|
||||||
switch arg {
|
|
||||||
case "-h", "--help":
|
|
||||||
opts.help = true
|
|
||||||
case "--json":
|
|
||||||
opts.json = true
|
|
||||||
case "--include-local":
|
|
||||||
opts.includeLocal = true
|
|
||||||
case "--timeout":
|
|
||||||
let next = (i + 1 < args.count) ? args[i + 1] : nil
|
|
||||||
if let next, let parsed = Int(next.trimmingCharacters(in: .whitespacesAndNewlines)) {
|
|
||||||
opts.timeoutMs = max(100, parsed)
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiscoveryOutput: Encodable {
|
|
||||||
struct Gateway: Encodable {
|
|
||||||
var displayName: String
|
|
||||||
var lanHost: String?
|
|
||||||
var tailnetDns: String?
|
|
||||||
var sshPort: Int
|
|
||||||
var gatewayPort: Int?
|
|
||||||
var cliPath: String?
|
|
||||||
var stableID: String
|
|
||||||
var debugID: String
|
|
||||||
var isLocal: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var status: String
|
|
||||||
var timeoutMs: Int
|
|
||||||
var includeLocal: Bool
|
|
||||||
var count: Int
|
|
||||||
var gateways: [Gateway]
|
|
||||||
}
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct ClawdbotDiscoveryCLI {
|
|
||||||
static func main() async {
|
|
||||||
let opts = DiscoveryOptions.parse(Array(CommandLine.arguments.dropFirst()))
|
|
||||||
if opts.help {
|
|
||||||
print("""
|
|
||||||
clawdbot-mac-discovery
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
clawdbot-mac-discovery [--timeout <ms>] [--json] [--include-local]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--timeout <ms> Discovery window in milliseconds (default: 2000)
|
|
||||||
--json Emit JSON
|
|
||||||
--include-local Include gateways considered local
|
|
||||||
-h, --help Show help
|
|
||||||
""")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let displayName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
|
|
||||||
let model = GatewayDiscoveryModel(
|
|
||||||
localDisplayName: displayName,
|
|
||||||
filterLocalGateways: !opts.includeLocal)
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
model.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
let nanos = UInt64(max(100, opts.timeoutMs)) * 1_000_000
|
|
||||||
try? await Task.sleep(nanoseconds: nanos)
|
|
||||||
|
|
||||||
let gateways = await MainActor.run { model.gateways }
|
|
||||||
let status = await MainActor.run { model.statusText }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
model.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.json {
|
|
||||||
let payload = DiscoveryOutput(
|
|
||||||
status: status,
|
|
||||||
timeoutMs: opts.timeoutMs,
|
|
||||||
includeLocal: opts.includeLocal,
|
|
||||||
count: gateways.count,
|
|
||||||
gateways: gateways.map {
|
|
||||||
DiscoveryOutput.Gateway(
|
|
||||||
displayName: $0.displayName,
|
|
||||||
lanHost: $0.lanHost,
|
|
||||||
tailnetDns: $0.tailnetDns,
|
|
||||||
sshPort: $0.sshPort,
|
|
||||||
gatewayPort: $0.gatewayPort,
|
|
||||||
cliPath: $0.cliPath,
|
|
||||||
stableID: $0.stableID,
|
|
||||||
debugID: $0.debugID,
|
|
||||||
isLocal: $0.isLocal)
|
|
||||||
})
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
||||||
if let data = try? encoder.encode(payload),
|
|
||||||
let json = String(data: data, encoding: .utf8)
|
|
||||||
{
|
|
||||||
print(json)
|
|
||||||
} else {
|
|
||||||
print("{\"error\":\"failed to encode JSON\"}")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Gateway Discovery (macOS NWBrowser)")
|
|
||||||
print("Status: \(status)")
|
|
||||||
print("Found \(gateways.count) gateway(s)\(opts.includeLocal ? "" : " (local filtered)")")
|
|
||||||
if gateways.isEmpty { return }
|
|
||||||
|
|
||||||
for gateway in gateways {
|
|
||||||
let hosts = [gateway.tailnetDns, gateway.lanHost]
|
|
||||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
.joined(separator: ", ")
|
|
||||||
print("- \(gateway.displayName)")
|
|
||||||
print(" hosts: \(hosts.isEmpty ? "(none)" : hosts)")
|
|
||||||
print(" ssh: \(gateway.sshPort)")
|
|
||||||
if let port = gateway.gatewayPort {
|
|
||||||
print(" gatewayPort: \(port)")
|
|
||||||
}
|
|
||||||
if let cliPath = gateway.cliPath {
|
|
||||||
print(" cliPath: \(cliPath)")
|
|
||||||
}
|
|
||||||
print(" isLocal: \(gateway.isLocal)")
|
|
||||||
print(" stableID: \(gateway.stableID)")
|
|
||||||
print(" debugID: \(gateway.debugID)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
306
apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift
Normal file
306
apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
import ClawdbotKit
|
||||||
|
import ClawdbotProtocol
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ConnectOptions {
|
||||||
|
var url: String?
|
||||||
|
var token: String?
|
||||||
|
var password: String?
|
||||||
|
var mode: String?
|
||||||
|
var timeoutMs: Int = 15_000
|
||||||
|
var json: Bool = false
|
||||||
|
var probe: Bool = false
|
||||||
|
var clientId: String = "clawdbot-macos"
|
||||||
|
var clientMode: String = "ui"
|
||||||
|
var displayName: String?
|
||||||
|
var role: String = "operator"
|
||||||
|
var scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"]
|
||||||
|
var help: Bool = false
|
||||||
|
|
||||||
|
static func parse(_ args: [String]) -> ConnectOptions {
|
||||||
|
var opts = ConnectOptions()
|
||||||
|
var i = 0
|
||||||
|
while i < args.count {
|
||||||
|
let arg = args[i]
|
||||||
|
switch arg {
|
||||||
|
case "-h", "--help":
|
||||||
|
opts.help = true
|
||||||
|
case "--json":
|
||||||
|
opts.json = true
|
||||||
|
case "--probe":
|
||||||
|
opts.probe = true
|
||||||
|
case "--url":
|
||||||
|
opts.url = self.nextValue(args, index: &i)
|
||||||
|
case "--token":
|
||||||
|
opts.token = self.nextValue(args, index: &i)
|
||||||
|
case "--password":
|
||||||
|
opts.password = self.nextValue(args, index: &i)
|
||||||
|
case "--mode":
|
||||||
|
if let value = self.nextValue(args, index: &i) {
|
||||||
|
opts.mode = value
|
||||||
|
}
|
||||||
|
case "--timeout":
|
||||||
|
if let raw = self.nextValue(args, index: &i),
|
||||||
|
let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
{
|
||||||
|
opts.timeoutMs = max(250, parsed)
|
||||||
|
}
|
||||||
|
case "--client-id":
|
||||||
|
if let value = self.nextValue(args, index: &i) {
|
||||||
|
opts.clientId = value
|
||||||
|
}
|
||||||
|
case "--client-mode":
|
||||||
|
if let value = self.nextValue(args, index: &i) {
|
||||||
|
opts.clientMode = value
|
||||||
|
}
|
||||||
|
case "--display-name":
|
||||||
|
opts.displayName = self.nextValue(args, index: &i)
|
||||||
|
case "--role":
|
||||||
|
if let value = self.nextValue(args, index: &i) {
|
||||||
|
opts.role = value
|
||||||
|
}
|
||||||
|
case "--scopes":
|
||||||
|
if let value = self.nextValue(args, index: &i) {
|
||||||
|
opts.scopes = value.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func nextValue(_ args: [String], index: inout Int) -> String? {
|
||||||
|
guard index + 1 < args.count else { return nil }
|
||||||
|
index += 1
|
||||||
|
return args[index].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConnectOutput: Encodable {
|
||||||
|
var status: String
|
||||||
|
var url: String
|
||||||
|
var mode: String
|
||||||
|
var role: String
|
||||||
|
var clientId: String
|
||||||
|
var clientMode: String
|
||||||
|
var scopes: [String]
|
||||||
|
var snapshot: HelloOk?
|
||||||
|
var health: ProtoAnyCodable?
|
||||||
|
var error: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
actor SnapshotStore {
|
||||||
|
private var value: HelloOk?
|
||||||
|
|
||||||
|
func set(_ snapshot: HelloOk) {
|
||||||
|
self.value = snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
func get() -> HelloOk? {
|
||||||
|
self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConnect(_ args: [String]) async {
|
||||||
|
let opts = ConnectOptions.parse(args)
|
||||||
|
if opts.help {
|
||||||
|
print("""
|
||||||
|
clawdbot-mac connect
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
clawdbot-mac connect [--url <ws://host:port>] [--token <token>] [--password <password>]
|
||||||
|
[--mode <local|remote>] [--timeout <ms>] [--probe] [--json]
|
||||||
|
[--client-id <id>] [--client-mode <mode>] [--display-name <name>]
|
||||||
|
[--role <role>] [--scopes <a,b,c>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--url <url> Gateway WebSocket URL (overrides config)
|
||||||
|
--token <token> Gateway token (if required)
|
||||||
|
--password <pw> Gateway password (if required)
|
||||||
|
--mode <mode> Resolve from config: local|remote (default: config or local)
|
||||||
|
--timeout <ms> Request timeout (default: 15000)
|
||||||
|
--probe Force a fresh health probe
|
||||||
|
--json Emit JSON
|
||||||
|
--client-id <id> Override client id (default: clawdbot-macos)
|
||||||
|
--client-mode <m> Override client mode (default: ui)
|
||||||
|
--display-name <n> Override display name
|
||||||
|
--role <role> Override role (default: operator)
|
||||||
|
--scopes <a,b,c> Override scopes list
|
||||||
|
-h, --help Show help
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = loadGatewayConfig()
|
||||||
|
do {
|
||||||
|
let endpoint = try resolveGatewayEndpoint(opts: opts, config: config)
|
||||||
|
let displayName = opts.displayName ?? Host.current().localizedName ?? "Clawdbot macOS Debug CLI"
|
||||||
|
let connectOptions = GatewayConnectOptions(
|
||||||
|
role: opts.role,
|
||||||
|
scopes: opts.scopes,
|
||||||
|
caps: [],
|
||||||
|
commands: [],
|
||||||
|
permissions: [:],
|
||||||
|
clientId: opts.clientId,
|
||||||
|
clientMode: opts.clientMode,
|
||||||
|
clientDisplayName: displayName)
|
||||||
|
|
||||||
|
let snapshotStore = SnapshotStore()
|
||||||
|
let channel = GatewayChannelActor(
|
||||||
|
url: endpoint.url,
|
||||||
|
token: endpoint.token,
|
||||||
|
password: endpoint.password,
|
||||||
|
pushHandler: { push in
|
||||||
|
if case let .snapshot(ok) = push {
|
||||||
|
await snapshotStore.set(ok)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
connectOptions: connectOptions)
|
||||||
|
|
||||||
|
let params: [String: KitAnyCodable]? = opts.probe ? ["probe": KitAnyCodable(true)] : nil
|
||||||
|
let data = try await channel.request(
|
||||||
|
method: "health",
|
||||||
|
params: params,
|
||||||
|
timeoutMs: Double(opts.timeoutMs))
|
||||||
|
let health = try? JSONDecoder().decode(ProtoAnyCodable.self, from: data)
|
||||||
|
let snapshot = await snapshotStore.get()
|
||||||
|
await channel.shutdown()
|
||||||
|
|
||||||
|
let output = ConnectOutput(
|
||||||
|
status: "ok",
|
||||||
|
url: endpoint.url.absoluteString,
|
||||||
|
mode: endpoint.mode,
|
||||||
|
role: opts.role,
|
||||||
|
clientId: opts.clientId,
|
||||||
|
clientMode: opts.clientMode,
|
||||||
|
scopes: opts.scopes,
|
||||||
|
snapshot: snapshot,
|
||||||
|
health: health,
|
||||||
|
error: nil)
|
||||||
|
printConnectOutput(output, json: opts.json)
|
||||||
|
} catch {
|
||||||
|
let endpoint = bestEffortEndpoint(opts: opts, config: config)
|
||||||
|
let fallbackMode = (opts.mode ?? config.mode ?? "local").lowercased()
|
||||||
|
let output = ConnectOutput(
|
||||||
|
status: "error",
|
||||||
|
url: endpoint?.url.absoluteString ?? "unknown",
|
||||||
|
mode: endpoint?.mode ?? fallbackMode,
|
||||||
|
role: opts.role,
|
||||||
|
clientId: opts.clientId,
|
||||||
|
clientMode: opts.clientMode,
|
||||||
|
scopes: opts.scopes,
|
||||||
|
snapshot: nil,
|
||||||
|
health: nil,
|
||||||
|
error: error.localizedDescription)
|
||||||
|
printConnectOutput(output, json: opts.json)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func printConnectOutput(_ output: ConnectOutput, json: Bool) {
|
||||||
|
if json {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
|
if let data = try? encoder.encode(output),
|
||||||
|
let text = String(data: data, encoding: .utf8)
|
||||||
|
{
|
||||||
|
print(text)
|
||||||
|
} else {
|
||||||
|
print("{\"error\":\"failed to encode JSON\"}")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Clawdbot macOS Gateway Connect")
|
||||||
|
print("Status: \(output.status)")
|
||||||
|
print("URL: \(output.url)")
|
||||||
|
print("Mode: \(output.mode)")
|
||||||
|
print("Client: \(output.clientId) (\(output.clientMode))")
|
||||||
|
print("Role: \(output.role)")
|
||||||
|
print("Scopes: \(output.scopes.joined(separator: ", "))")
|
||||||
|
if let snapshot = output.snapshot {
|
||||||
|
print("Protocol: \(snapshot._protocol)")
|
||||||
|
if let version = snapshot.server["version"]?.value as? String {
|
||||||
|
print("Server: \(version)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let health = output.health,
|
||||||
|
let ok = (health.value as? [String: ProtoAnyCodable])?["ok"]?.value as? Bool
|
||||||
|
{
|
||||||
|
print("Health: \(ok ? "ok" : "error")")
|
||||||
|
} else if output.health != nil {
|
||||||
|
print("Health: received")
|
||||||
|
}
|
||||||
|
if let error = output.error {
|
||||||
|
print("Error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolveGatewayEndpoint(opts: ConnectOptions, config: GatewayConfig) throws -> GatewayEndpoint {
|
||||||
|
let resolvedMode = (opts.mode ?? config.mode ?? "local").lowercased()
|
||||||
|
if let raw = opts.url, !raw.isEmpty {
|
||||||
|
guard let url = URL(string: raw) else {
|
||||||
|
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: \(raw)"])
|
||||||
|
}
|
||||||
|
return GatewayEndpoint(
|
||||||
|
url: url,
|
||||||
|
token: resolvedToken(opts: opts, mode: resolvedMode, config: config),
|
||||||
|
password: resolvedPassword(opts: opts, mode: resolvedMode, config: config),
|
||||||
|
mode: resolvedMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolvedMode == "remote" {
|
||||||
|
guard let raw = config.remoteUrl?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!raw.isEmpty else {
|
||||||
|
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url is missing"])
|
||||||
|
}
|
||||||
|
guard let url = URL(string: raw) else {
|
||||||
|
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: \(raw)"])
|
||||||
|
}
|
||||||
|
return GatewayEndpoint(
|
||||||
|
url: url,
|
||||||
|
token: resolvedToken(opts: opts, mode: resolvedMode, config: config),
|
||||||
|
password: resolvedPassword(opts: opts, mode: resolvedMode, config: config),
|
||||||
|
mode: resolvedMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = config.port ?? 18789
|
||||||
|
let host = "127.0.0.1"
|
||||||
|
guard let url = URL(string: "ws://\(host):\(port)") else {
|
||||||
|
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: ws://\(host):\(port)"])
|
||||||
|
}
|
||||||
|
return GatewayEndpoint(
|
||||||
|
url: url,
|
||||||
|
token: resolvedToken(opts: opts, mode: resolvedMode, config: config),
|
||||||
|
password: resolvedPassword(opts: opts, mode: resolvedMode, config: config),
|
||||||
|
mode: resolvedMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bestEffortEndpoint(opts: ConnectOptions, config: GatewayConfig) -> GatewayEndpoint? {
|
||||||
|
return try? resolveGatewayEndpoint(opts: opts, config: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedToken(opts: ConnectOptions, mode: String, config: GatewayConfig) -> String? {
|
||||||
|
if let token = opts.token, !token.isEmpty { return token }
|
||||||
|
if let token = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_TOKEN"], !token.isEmpty {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
if mode == "remote" {
|
||||||
|
return config.remoteToken
|
||||||
|
}
|
||||||
|
return config.token
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedPassword(opts: ConnectOptions, mode: String, config: GatewayConfig) -> String? {
|
||||||
|
if let password = opts.password, !password.isEmpty { return password }
|
||||||
|
if let password = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PASSWORD"], !password.isEmpty {
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
if mode == "remote" {
|
||||||
|
return config.remotePassword
|
||||||
|
}
|
||||||
|
return config.password
|
||||||
|
}
|
||||||
149
apps/macos/Sources/ClawdbotMacCLI/DiscoverCommand.swift
Normal file
149
apps/macos/Sources/ClawdbotMacCLI/DiscoverCommand.swift
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import ClawdbotDiscovery
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DiscoveryOptions {
|
||||||
|
var timeoutMs: Int = 2000
|
||||||
|
var json: Bool = false
|
||||||
|
var includeLocal: Bool = false
|
||||||
|
var help: Bool = false
|
||||||
|
|
||||||
|
static func parse(_ args: [String]) -> DiscoveryOptions {
|
||||||
|
var opts = DiscoveryOptions()
|
||||||
|
var i = 0
|
||||||
|
while i < args.count {
|
||||||
|
let arg = args[i]
|
||||||
|
switch arg {
|
||||||
|
case "-h", "--help":
|
||||||
|
opts.help = true
|
||||||
|
case "--json":
|
||||||
|
opts.json = true
|
||||||
|
case "--include-local":
|
||||||
|
opts.includeLocal = true
|
||||||
|
case "--timeout":
|
||||||
|
let next = (i + 1 < args.count) ? args[i + 1] : nil
|
||||||
|
if let next, let parsed = Int(next.trimmingCharacters(in: .whitespacesAndNewlines)) {
|
||||||
|
opts.timeoutMs = max(100, parsed)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiscoveryOutput: Encodable {
|
||||||
|
struct Gateway: Encodable {
|
||||||
|
var displayName: String
|
||||||
|
var lanHost: String?
|
||||||
|
var tailnetDns: String?
|
||||||
|
var sshPort: Int
|
||||||
|
var gatewayPort: Int?
|
||||||
|
var cliPath: String?
|
||||||
|
var stableID: String
|
||||||
|
var debugID: String
|
||||||
|
var isLocal: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var status: String
|
||||||
|
var timeoutMs: Int
|
||||||
|
var includeLocal: Bool
|
||||||
|
var count: Int
|
||||||
|
var gateways: [Gateway]
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDiscover(_ args: [String]) async {
|
||||||
|
let opts = DiscoveryOptions.parse(args)
|
||||||
|
if opts.help {
|
||||||
|
print("""
|
||||||
|
clawdbot-mac discover
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
clawdbot-mac discover [--timeout <ms>] [--json] [--include-local]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--timeout <ms> Discovery window in milliseconds (default: 2000)
|
||||||
|
--json Emit JSON
|
||||||
|
--include-local Include gateways considered local
|
||||||
|
-h, --help Show help
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
|
||||||
|
let model = await MainActor.run {
|
||||||
|
GatewayDiscoveryModel(
|
||||||
|
localDisplayName: displayName,
|
||||||
|
filterLocalGateways: !opts.includeLocal)
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
model.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
let nanos = UInt64(max(100, opts.timeoutMs)) * 1_000_000
|
||||||
|
try? await Task.sleep(nanoseconds: nanos)
|
||||||
|
|
||||||
|
let gateways = await MainActor.run { model.gateways }
|
||||||
|
let status = await MainActor.run { model.statusText }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
model.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.json {
|
||||||
|
let payload = DiscoveryOutput(
|
||||||
|
status: status,
|
||||||
|
timeoutMs: opts.timeoutMs,
|
||||||
|
includeLocal: opts.includeLocal,
|
||||||
|
count: gateways.count,
|
||||||
|
gateways: gateways.map {
|
||||||
|
DiscoveryOutput.Gateway(
|
||||||
|
displayName: $0.displayName,
|
||||||
|
lanHost: $0.lanHost,
|
||||||
|
tailnetDns: $0.tailnetDns,
|
||||||
|
sshPort: $0.sshPort,
|
||||||
|
gatewayPort: $0.gatewayPort,
|
||||||
|
cliPath: $0.cliPath,
|
||||||
|
stableID: $0.stableID,
|
||||||
|
debugID: $0.debugID,
|
||||||
|
isLocal: $0.isLocal)
|
||||||
|
})
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
|
if let data = try? encoder.encode(payload),
|
||||||
|
let json = String(data: data, encoding: .utf8)
|
||||||
|
{
|
||||||
|
print(json)
|
||||||
|
} else {
|
||||||
|
print("{\"error\":\"failed to encode JSON\"}")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Gateway Discovery (macOS NWBrowser)")
|
||||||
|
print("Status: \(status)")
|
||||||
|
print("Found \(gateways.count) gateway(s)\(opts.includeLocal ? "" : " (local filtered)")")
|
||||||
|
if gateways.isEmpty { return }
|
||||||
|
|
||||||
|
for gateway in gateways {
|
||||||
|
let hosts = [gateway.tailnetDns, gateway.lanHost]
|
||||||
|
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.joined(separator: ", ")
|
||||||
|
print("- \(gateway.displayName)")
|
||||||
|
print(" hosts: \(hosts.isEmpty ? "(none)" : hosts)")
|
||||||
|
print(" ssh: \(gateway.sshPort)")
|
||||||
|
if let port = gateway.gatewayPort {
|
||||||
|
print(" gatewayPort: \(port)")
|
||||||
|
}
|
||||||
|
if let cliPath = gateway.cliPath {
|
||||||
|
print(" cliPath: \(cliPath)")
|
||||||
|
}
|
||||||
|
print(" isLocal: \(gateway.isLocal)")
|
||||||
|
print(" stableID: \(gateway.stableID)")
|
||||||
|
print(" debugID: \(gateway.debugID)")
|
||||||
|
}
|
||||||
|
}
|
||||||
56
apps/macos/Sources/ClawdbotMacCLI/EntryPoint.swift
Normal file
56
apps/macos/Sources/ClawdbotMacCLI/EntryPoint.swift
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
private struct RootCommand {
|
||||||
|
var name: String
|
||||||
|
var args: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct ClawdbotMacCLI {
|
||||||
|
static func main() async {
|
||||||
|
let args = Array(CommandLine.arguments.dropFirst())
|
||||||
|
let command = parseRootCommand(args)
|
||||||
|
switch command?.name {
|
||||||
|
case nil:
|
||||||
|
printUsage()
|
||||||
|
case "-h", "--help", "help":
|
||||||
|
printUsage()
|
||||||
|
case "connect":
|
||||||
|
await runConnect(command?.args ?? [])
|
||||||
|
case "discover":
|
||||||
|
await runDiscover(command?.args ?? [])
|
||||||
|
case "wizard":
|
||||||
|
await runWizardCommand(command?.args ?? [])
|
||||||
|
default:
|
||||||
|
fputs("clawdbot-mac: unknown command\n", stderr)
|
||||||
|
printUsage()
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parseRootCommand(_ args: [String]) -> RootCommand? {
|
||||||
|
guard let first = args.first else { return nil }
|
||||||
|
return RootCommand(name: first, args: Array(args.dropFirst()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func printUsage() {
|
||||||
|
print("""
|
||||||
|
clawdbot-mac
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
clawdbot-mac connect [--url <ws://host:port>] [--token <token>] [--password <password>]
|
||||||
|
[--mode <local|remote>] [--timeout <ms>] [--probe] [--json]
|
||||||
|
[--client-id <id>] [--client-mode <mode>] [--display-name <name>]
|
||||||
|
[--role <role>] [--scopes <a,b,c>]
|
||||||
|
clawdbot-mac discover [--timeout <ms>] [--json] [--include-local]
|
||||||
|
clawdbot-mac wizard [--url <ws://host:port>] [--token <token>] [--password <password>]
|
||||||
|
[--mode <local|remote>] [--workspace <path>] [--json]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
clawdbot-mac connect
|
||||||
|
clawdbot-mac connect --url ws://127.0.0.1:18789 --json
|
||||||
|
clawdbot-mac discover --timeout 3000 --json
|
||||||
|
clawdbot-mac wizard --mode local
|
||||||
|
""")
|
||||||
|
}
|
||||||
60
apps/macos/Sources/ClawdbotMacCLI/GatewayConfig.swift
Normal file
60
apps/macos/Sources/ClawdbotMacCLI/GatewayConfig.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GatewayConfig {
|
||||||
|
var mode: String?
|
||||||
|
var bind: String?
|
||||||
|
var port: Int?
|
||||||
|
var remoteUrl: String?
|
||||||
|
var token: String?
|
||||||
|
var password: String?
|
||||||
|
var remoteToken: String?
|
||||||
|
var remotePassword: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GatewayEndpoint {
|
||||||
|
let url: URL
|
||||||
|
let token: String?
|
||||||
|
let password: String?
|
||||||
|
let mode: String
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGatewayConfig() -> GatewayConfig {
|
||||||
|
let url = FileManager().homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".clawdbot")
|
||||||
|
.appendingPathComponent("clawdbot.json")
|
||||||
|
guard let data = try? Data(contentsOf: url) else { return GatewayConfig() }
|
||||||
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
return GatewayConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg = GatewayConfig()
|
||||||
|
if let gateway = json["gateway"] as? [String: Any] {
|
||||||
|
cfg.mode = gateway["mode"] as? String
|
||||||
|
cfg.bind = gateway["bind"] as? String
|
||||||
|
cfg.port = gateway["port"] as? Int ?? parseInt(gateway["port"])
|
||||||
|
|
||||||
|
if let auth = gateway["auth"] as? [String: Any] {
|
||||||
|
cfg.token = auth["token"] as? String
|
||||||
|
cfg.password = auth["password"] as? String
|
||||||
|
}
|
||||||
|
if let remote = gateway["remote"] as? [String: Any] {
|
||||||
|
cfg.remoteUrl = remote["url"] as? String
|
||||||
|
cfg.remoteToken = remote["token"] as? String
|
||||||
|
cfg.remotePassword = remote["password"] as? String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt(_ value: Any?) -> Int? {
|
||||||
|
switch value {
|
||||||
|
case let number as Int:
|
||||||
|
number
|
||||||
|
case let number as Double:
|
||||||
|
Int(number)
|
||||||
|
case let raw as String:
|
||||||
|
Int(raw.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
default:
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/macos/Sources/ClawdbotMacCLI/TypeAliases.swift
Normal file
5
apps/macos/Sources/ClawdbotMacCLI/TypeAliases.swift
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import ClawdbotKit
|
||||||
|
import ClawdbotProtocol
|
||||||
|
|
||||||
|
typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable
|
||||||
|
typealias KitAnyCodable = ClawdbotKit.AnyCodable
|
||||||
@@ -3,8 +3,6 @@ import ClawdbotProtocol
|
|||||||
import Darwin
|
import Darwin
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable
|
|
||||||
|
|
||||||
struct WizardCliOptions {
|
struct WizardCliOptions {
|
||||||
var url: String?
|
var url: String?
|
||||||
var token: String?
|
var token: String?
|
||||||
@@ -51,17 +49,6 @@ struct WizardCliOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GatewayConfig {
|
|
||||||
var mode: String?
|
|
||||||
var bind: String?
|
|
||||||
var port: Int?
|
|
||||||
var remoteUrl: String?
|
|
||||||
var token: String?
|
|
||||||
var password: String?
|
|
||||||
var remoteToken: String?
|
|
||||||
var remotePassword: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
enum WizardCliError: Error, CustomStringConvertible {
|
enum WizardCliError: Error, CustomStringConvertible {
|
||||||
case invalidUrl(String)
|
case invalidUrl(String)
|
||||||
case missingRemoteUrl
|
case missingRemoteUrl
|
||||||
@@ -80,68 +67,56 @@ enum WizardCliError: Error, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@main
|
func runWizardCommand(_ args: [String]) async {
|
||||||
struct ClawdbotWizardCLI {
|
let opts = WizardCliOptions.parse(args)
|
||||||
static func main() async {
|
if opts.help {
|
||||||
let opts = WizardCliOptions.parse(Array(CommandLine.arguments.dropFirst()))
|
print("""
|
||||||
if opts.help {
|
clawdbot-mac wizard
|
||||||
printUsage()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = loadGatewayConfig()
|
Usage:
|
||||||
do {
|
clawdbot-mac wizard [--url <ws://host:port>] [--token <token>] [--password <password>]
|
||||||
guard isatty(STDIN_FILENO) != 0 else {
|
[--mode <local|remote>] [--workspace <path>] [--json]
|
||||||
throw WizardCliError.gatewayError("Wizard requires an interactive TTY.")
|
|
||||||
}
|
Options:
|
||||||
let endpoint = try resolveGatewayEndpoint(opts: opts, config: config)
|
--url <url> Gateway WebSocket URL (overrides config)
|
||||||
let client = GatewayWizardClient(
|
--token <token> Gateway token (if required)
|
||||||
url: endpoint.url,
|
--password <pw> Gateway password (if required)
|
||||||
token: endpoint.token,
|
--mode <mode> Wizard mode (local|remote). Default: local
|
||||||
password: endpoint.password,
|
--workspace <path> Wizard workspace override
|
||||||
json: opts.json)
|
--json Print raw wizard responses
|
||||||
try await client.connect()
|
-h, --help Show help
|
||||||
defer { Task { await client.close() } }
|
""")
|
||||||
try await runWizard(client: client, opts: opts)
|
return
|
||||||
} catch {
|
}
|
||||||
fputs("wizard: \(error)\n", stderr)
|
|
||||||
exit(1)
|
let config = loadGatewayConfig()
|
||||||
|
do {
|
||||||
|
guard isatty(STDIN_FILENO) != 0 else {
|
||||||
|
throw WizardCliError.gatewayError("Wizard requires an interactive TTY.")
|
||||||
}
|
}
|
||||||
|
let endpoint = try resolveWizardGatewayEndpoint(opts: opts, config: config)
|
||||||
|
let client = GatewayWizardClient(
|
||||||
|
url: endpoint.url,
|
||||||
|
token: endpoint.token,
|
||||||
|
password: endpoint.password,
|
||||||
|
json: opts.json)
|
||||||
|
try await client.connect()
|
||||||
|
defer { Task { await client.close() } }
|
||||||
|
try await runWizard(client: client, opts: opts)
|
||||||
|
} catch {
|
||||||
|
fputs("wizard: \(error)\n", stderr)
|
||||||
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct GatewayEndpoint {
|
private func resolveWizardGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfig) throws -> GatewayEndpoint {
|
||||||
let url: URL
|
|
||||||
let token: String?
|
|
||||||
let password: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
private func printUsage() {
|
|
||||||
print("""
|
|
||||||
clawdbot-mac-wizard
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
clawdbot-mac-wizard [--url <ws://host:port>] [--token <token>] [--password <password>]
|
|
||||||
[--mode <local|remote>] [--workspace <path>] [--json]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--url <url> Gateway WebSocket URL (overrides config)
|
|
||||||
--token <token> Gateway token (if required)
|
|
||||||
--password <pw> Gateway password (if required)
|
|
||||||
--mode <mode> Wizard mode (local|remote). Default: local
|
|
||||||
--workspace <path> Wizard workspace override
|
|
||||||
--json Print raw wizard responses
|
|
||||||
-h, --help Show help
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resolveGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfig) throws -> GatewayEndpoint {
|
|
||||||
if let raw = opts.url, !raw.isEmpty {
|
if let raw = opts.url, !raw.isEmpty {
|
||||||
guard let url = URL(string: raw) else { throw WizardCliError.invalidUrl(raw) }
|
guard let url = URL(string: raw) else { throw WizardCliError.invalidUrl(raw) }
|
||||||
return GatewayEndpoint(
|
return GatewayEndpoint(
|
||||||
url: url,
|
url: url,
|
||||||
token: resolvedToken(opts: opts, config: config),
|
token: resolvedToken(opts: opts, config: config),
|
||||||
password: resolvedPassword(opts: opts, config: config))
|
password: resolvedPassword(opts: opts, config: config),
|
||||||
|
mode: (config.mode ?? "local").lowercased())
|
||||||
}
|
}
|
||||||
|
|
||||||
let mode = (config.mode ?? "local").lowercased()
|
let mode = (config.mode ?? "local").lowercased()
|
||||||
@@ -153,7 +128,8 @@ private func resolveGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfi
|
|||||||
return GatewayEndpoint(
|
return GatewayEndpoint(
|
||||||
url: url,
|
url: url,
|
||||||
token: resolvedToken(opts: opts, config: config),
|
token: resolvedToken(opts: opts, config: config),
|
||||||
password: resolvedPassword(opts: opts, config: config))
|
password: resolvedPassword(opts: opts, config: config),
|
||||||
|
mode: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = config.port ?? 18789
|
let port = config.port ?? 18789
|
||||||
@@ -164,7 +140,8 @@ private func resolveGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfi
|
|||||||
return GatewayEndpoint(
|
return GatewayEndpoint(
|
||||||
url: url,
|
url: url,
|
||||||
token: resolvedToken(opts: opts, config: config),
|
token: resolvedToken(opts: opts, config: config),
|
||||||
password: resolvedPassword(opts: opts, config: config))
|
password: resolvedPassword(opts: opts, config: config),
|
||||||
|
mode: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolvedToken(opts: WizardCliOptions, config: GatewayConfig) -> String? {
|
private func resolvedToken(opts: WizardCliOptions, config: GatewayConfig) -> String? {
|
||||||
@@ -189,47 +166,6 @@ private func resolvedPassword(opts: WizardCliOptions, config: GatewayConfig) ->
|
|||||||
return config.password
|
return config.password
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadGatewayConfig() -> GatewayConfig {
|
|
||||||
let url = FileManager().homeDirectoryForCurrentUser
|
|
||||||
.appendingPathComponent(".clawdbot")
|
|
||||||
.appendingPathComponent("clawdbot.json")
|
|
||||||
guard let data = try? Data(contentsOf: url) else { return GatewayConfig() }
|
|
||||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
||||||
return GatewayConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg = GatewayConfig()
|
|
||||||
if let gateway = json["gateway"] as? [String: Any] {
|
|
||||||
cfg.mode = gateway["mode"] as? String
|
|
||||||
cfg.bind = gateway["bind"] as? String
|
|
||||||
cfg.port = gateway["port"] as? Int ?? parseInt(gateway["port"])
|
|
||||||
|
|
||||||
if let auth = gateway["auth"] as? [String: Any] {
|
|
||||||
cfg.token = auth["token"] as? String
|
|
||||||
cfg.password = auth["password"] as? String
|
|
||||||
}
|
|
||||||
if let remote = gateway["remote"] as? [String: Any] {
|
|
||||||
cfg.remoteUrl = remote["url"] as? String
|
|
||||||
cfg.remoteToken = remote["token"] as? String
|
|
||||||
cfg.remotePassword = remote["password"] as? String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseInt(_ value: Any?) -> Int? {
|
|
||||||
switch value {
|
|
||||||
case let number as Int:
|
|
||||||
number
|
|
||||||
case let number as Double:
|
|
||||||
Int(number)
|
|
||||||
case let raw as String:
|
|
||||||
Int(raw.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
||||||
default:
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actor GatewayWizardClient {
|
actor GatewayWizardClient {
|
||||||
private enum ConnectChallengeError: Error {
|
private enum ConnectChallengeError: Error {
|
||||||
case timeout
|
case timeout
|
||||||
@@ -411,7 +347,7 @@ actor GatewayWizardClient {
|
|||||||
operation: {
|
operation: {
|
||||||
while true {
|
while true {
|
||||||
let message = try await task.receive()
|
let message = try await task.receive()
|
||||||
let frame = try decodeFrame(message)
|
let frame = try await self.decodeFrame(message)
|
||||||
if case let .event(evt) = frame, evt.event == "connect.challenge" {
|
if case let .event(evt) = frame, evt.event == "connect.challenge" {
|
||||||
if let payload = evt.payload?.value as? [String: ProtoAnyCodable],
|
if let payload = evt.payload?.value as? [String: ProtoAnyCodable],
|
||||||
let nonce = payload["nonce"]?.value as? String
|
let nonce = payload["nonce"]?.value as? String
|
||||||
@@ -5,7 +5,7 @@ read_when:
|
|||||||
---
|
---
|
||||||
# Clawdbot macOS IPC architecture
|
# Clawdbot macOS IPC architecture
|
||||||
|
|
||||||
**Current model:** a local Unix socket connects the **node service** to the **macOS app** for exec approvals + `system.run`. There is no `clawdbot-mac` CLI; agent actions still flow through the Gateway WebSocket and `node.invoke`. UI automation uses PeekabooBridge.
|
**Current model:** a local Unix socket connects the **node service** to the **macOS app** for exec approvals + `system.run`. A `clawdbot-mac` debug CLI exists for discovery/connect checks; agent actions still flow through the Gateway WebSocket and `node.invoke`. UI automation uses PeekabooBridge.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
- Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript).
|
- Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript).
|
||||||
|
|||||||
@@ -140,19 +140,27 @@ Safety:
|
|||||||
- `swift run Clawdbot` (or Xcode)
|
- `swift run Clawdbot` (or Xcode)
|
||||||
- Package app: `scripts/package-mac-app.sh`
|
- Package app: `scripts/package-mac-app.sh`
|
||||||
|
|
||||||
## Debug gateway discovery (macOS CLI)
|
## Debug gateway connectivity (macOS CLI)
|
||||||
|
|
||||||
Use the debug CLI to exercise the same Bonjour + wide‑area discovery code that the
|
Use the debug CLI to exercise the same Gateway WebSocket handshake and discovery
|
||||||
macOS app uses, without launching the app.
|
logic that the macOS app uses, without launching the app.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/macos
|
cd apps/macos
|
||||||
swift run clawdbot-mac-discovery --timeout 3000 --json
|
swift run clawdbot-mac connect --json
|
||||||
|
swift run clawdbot-mac discover --timeout 3000 --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Connect options:
|
||||||
|
- `--url <ws://host:port>`: override config
|
||||||
|
- `--mode <local|remote>`: resolve from config (default: config or local)
|
||||||
|
- `--probe`: force a fresh health probe
|
||||||
|
- `--timeout <ms>`: request timeout (default: `15000`)
|
||||||
|
- `--json`: structured output for diffing
|
||||||
|
|
||||||
|
Discovery options:
|
||||||
- `--include-local`: include gateways that would be filtered as “local”
|
- `--include-local`: include gateways that would be filtered as “local”
|
||||||
- `--timeout <ms>`: overall discovery window (default `2000`)
|
- `--timeout <ms>`: overall discovery window (default: `2000`)
|
||||||
- `--json`: structured output for diffing
|
- `--json`: structured output for diffing
|
||||||
|
|
||||||
Tip: compare against `clawdbot gateway discover --json` to see whether the
|
Tip: compare against `clawdbot gateway discover --json` to see whether the
|
||||||
|
|||||||
Reference in New Issue
Block a user