Files
byre/hands/Sources/byre-hands/Server.swift
T

113 lines
3.5 KiB
Swift
Raw Normal View History

import AppKit
struct ToolError: Error {
let message: String
init(_ message: String) {
self.message = message
}
}
enum Content {
case text(String)
case png(Data)
var json: [String: Any] {
switch self {
case .text(let s):
return ["type": "text", "text": s]
case .png(let d):
return ["type": "image", "data": d.base64EncodedString(), "mimeType": "image/png"]
}
}
}
struct Tool {
let name: String
let description: String
let schema: String
2026-10-17 23:03:33 -07:00
let run: ([String: Any]) async throws -> [Content]
}
2026-10-17 23:03:33 -07:00
let stdoutLock = NSLock()
2026-10-18 00:39:04 -07:00
let mediaDir: String? = {
let args = CommandLine.arguments
guard let i = args.firstIndex(of: "--media-dir"), i + 1 < args.count else {
return nil
}
return args[i + 1]
}()
@main
struct Server {
static func main() {
_ = NSApplication.shared
2026-10-18 00:05:26 -07:00
NSApp.setActivationPolicy(.prohibited)
2026-10-17 23:03:33 -07:00
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), 3)
Thread.detachNewThread {
2026-10-17 23:03:33 -07:00
let inFlight = DispatchGroup()
while let line = readLine() {
2026-10-17 23:03:33 -07:00
inFlight.enter()
Task.detached {
await handle(line)
2026-10-17 23:03:33 -07:00
inFlight.leave()
}
}
2026-10-17 23:03:33 -07:00
inFlight.wait()
exit(0)
}
RunLoop.main.run()
}
static func handle(_ line: String) async {
guard let data = line.data(using: .utf8),
let req = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let id = req["id"]
else {
return
}
var resp: [String: Any] = ["jsonrpc": "2.0", "id": id]
let params = req["params"] as? [String: Any] ?? [:]
switch req["method"] as? String ?? "" {
case "initialize":
resp["result"] = [
"protocolVersion": params["protocolVersion"] as? String ?? "2025-06-18",
"capabilities": ["tools": [:]],
"serverInfo": ["name": "hands", "version": "0.0.0"],
]
case "ping":
resp["result"] = [:] as [String: Any]
case "tools/list":
resp["result"] = ["tools": tools.map { t in
["name": t.name, "description": t.description, "inputSchema": try! JSONSerialization.jsonObject(with: Data(t.schema.utf8))]
}]
case "tools/call":
resp["result"] = await call(params)
default:
resp["error"] = ["code": -32601, "message": "method not found: \(req["method"] ?? "")"]
}
2026-10-17 23:03:33 -07:00
var out = try! JSONSerialization.data(withJSONObject: resp, options: .withoutEscapingSlashes)
out.append(Data("\n".utf8))
stdoutLock.withLock {
FileHandle.standardOutput.write(out)
}
}
static func call(_ params: [String: Any]) async -> [String: Any] {
let name = params["name"] as? String ?? ""
let args = params["arguments"] as? [String: Any] ?? [:]
guard let tool = tools.first(where: { $0.name == name }) else {
return ["content": [Content.text("unknown tool: \(name)").json], "isError": true]
}
do {
let content = try await withDisplayAwake { try await tool.run(args) }
return ["content": content.map { $0.json }]
} catch let e as ToolError {
return ["content": [Content.text(e.message).json], "isError": true]
} catch {
return ["content": [Content.text("\(error)").json], "isError": true]
}
}
}