@lattices/cli 0.4.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/apps/mac/Info.plist +4 -4
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +5 -5
- package/docs/voice.md +11 -27
- package/package.json +11 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
// MARK: - Wire Format
|
|
4
|
-
|
|
5
|
-
struct DaemonRequest: Codable {
|
|
6
|
-
let id: String
|
|
7
|
-
let method: String
|
|
8
|
-
let params: JSON?
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
struct DaemonResponse: Codable {
|
|
12
|
-
let id: String
|
|
13
|
-
let result: JSON?
|
|
14
|
-
let error: String?
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
struct DaemonEvent: Codable {
|
|
18
|
-
let event: String
|
|
19
|
-
let data: JSON
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// MARK: - Dynamic JSON
|
|
23
|
-
|
|
24
|
-
enum JSON: Codable, Equatable {
|
|
25
|
-
case string(String)
|
|
26
|
-
case int(Int)
|
|
27
|
-
case double(Double)
|
|
28
|
-
case bool(Bool)
|
|
29
|
-
case array([JSON])
|
|
30
|
-
case object([String: JSON])
|
|
31
|
-
case null
|
|
32
|
-
|
|
33
|
-
// MARK: Codable
|
|
34
|
-
|
|
35
|
-
init(from decoder: Decoder) throws {
|
|
36
|
-
let container = try decoder.singleValueContainer()
|
|
37
|
-
|
|
38
|
-
if container.decodeNil() {
|
|
39
|
-
self = .null
|
|
40
|
-
} else if let b = try? container.decode(Bool.self) {
|
|
41
|
-
self = .bool(b)
|
|
42
|
-
} else if let i = try? container.decode(Int.self) {
|
|
43
|
-
self = .int(i)
|
|
44
|
-
} else if let d = try? container.decode(Double.self) {
|
|
45
|
-
self = .double(d)
|
|
46
|
-
} else if let s = try? container.decode(String.self) {
|
|
47
|
-
self = .string(s)
|
|
48
|
-
} else if let arr = try? container.decode([JSON].self) {
|
|
49
|
-
self = .array(arr)
|
|
50
|
-
} else if let obj = try? container.decode([String: JSON].self) {
|
|
51
|
-
self = .object(obj)
|
|
52
|
-
} else {
|
|
53
|
-
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode JSON value")
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
func encode(to encoder: Encoder) throws {
|
|
58
|
-
var container = encoder.singleValueContainer()
|
|
59
|
-
switch self {
|
|
60
|
-
case .string(let s): try container.encode(s)
|
|
61
|
-
case .int(let i): try container.encode(i)
|
|
62
|
-
case .double(let d): try container.encode(d)
|
|
63
|
-
case .bool(let b): try container.encode(b)
|
|
64
|
-
case .array(let a): try container.encode(a)
|
|
65
|
-
case .object(let o): try container.encode(o)
|
|
66
|
-
case .null: try container.encodeNil()
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// MARK: Subscript helpers
|
|
71
|
-
|
|
72
|
-
subscript(key: String) -> JSON? {
|
|
73
|
-
guard case .object(let dict) = self else { return nil }
|
|
74
|
-
return dict[key]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
subscript(index: Int) -> JSON? {
|
|
78
|
-
guard case .array(let arr) = self, index >= 0, index < arr.count else { return nil }
|
|
79
|
-
return arr[index]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
var stringValue: String? {
|
|
83
|
-
guard case .string(let s) = self else { return nil }
|
|
84
|
-
return s
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
var intValue: Int? {
|
|
88
|
-
guard case .int(let i) = self else { return nil }
|
|
89
|
-
return i
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
var uint32Value: UInt32? {
|
|
93
|
-
guard case .int(let i) = self else { return nil }
|
|
94
|
-
return UInt32(i)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
var boolValue: Bool? {
|
|
98
|
-
guard case .bool(let b) = self else { return nil }
|
|
99
|
-
return b
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
var arrayValue: [JSON]? {
|
|
103
|
-
guard case .array(let a) = self else { return nil }
|
|
104
|
-
return a
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
var numericDouble: Double? {
|
|
108
|
-
switch self {
|
|
109
|
-
case .double(let d): return d
|
|
110
|
-
case .int(let i): return Double(i)
|
|
111
|
-
default: return nil
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import CommonCrypto
|
|
3
|
-
|
|
4
|
-
// MARK: - POSIX WebSocket Server
|
|
5
|
-
// NWListener is broken on macOS 26 (Tahoe) — EINVAL on any listener creation.
|
|
6
|
-
// This is a minimal POSIX-socket WebSocket server on 127.0.0.1:9399.
|
|
7
|
-
|
|
8
|
-
final class DaemonServer: ObservableObject {
|
|
9
|
-
static let shared = DaemonServer()
|
|
10
|
-
|
|
11
|
-
@Published var clientCount: Int = 0
|
|
12
|
-
@Published var isListening: Bool = false
|
|
13
|
-
|
|
14
|
-
private var serverFd: Int32 = -1
|
|
15
|
-
private var clients: [UUID: WebSocketClient] = [:]
|
|
16
|
-
private let lock = NSLock()
|
|
17
|
-
private let queue = DispatchQueue(label: "lattices.daemon", qos: .userInitiated)
|
|
18
|
-
private let encoder = JSONEncoder()
|
|
19
|
-
private let decoder = JSONDecoder()
|
|
20
|
-
private var acceptSource: DispatchSourceRead?
|
|
21
|
-
|
|
22
|
-
func start() {
|
|
23
|
-
let diag = DiagnosticLog.shared
|
|
24
|
-
|
|
25
|
-
// 1. Create TCP socket
|
|
26
|
-
serverFd = socket(AF_INET, SOCK_STREAM, 0)
|
|
27
|
-
guard serverFd >= 0 else {
|
|
28
|
-
diag.error("DaemonServer: socket() failed — errno \(errno)")
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// SO_REUSEADDR so we can restart quickly
|
|
33
|
-
var yes: Int32 = 1
|
|
34
|
-
setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout<Int32>.size))
|
|
35
|
-
|
|
36
|
-
// 2. Bind to 127.0.0.1:9399
|
|
37
|
-
var addr = sockaddr_in()
|
|
38
|
-
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
39
|
-
addr.sin_family = sa_family_t(AF_INET)
|
|
40
|
-
addr.sin_port = UInt16(9399).bigEndian
|
|
41
|
-
addr.sin_addr.s_addr = UInt32(0x7f000001).bigEndian // 127.0.0.1
|
|
42
|
-
|
|
43
|
-
let bindResult = withUnsafePointer(to: &addr) {
|
|
44
|
-
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
45
|
-
bind(serverFd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
guard bindResult == 0 else {
|
|
49
|
-
diag.error("DaemonServer: bind() failed — errno \(errno)")
|
|
50
|
-
close(serverFd)
|
|
51
|
-
serverFd = -1
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 3. Listen
|
|
56
|
-
guard listen(serverFd, 8) == 0 else {
|
|
57
|
-
diag.error("DaemonServer: listen() failed — errno \(errno)")
|
|
58
|
-
close(serverFd)
|
|
59
|
-
serverFd = -1
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Non-blocking
|
|
64
|
-
let flags = fcntl(serverFd, F_GETFL)
|
|
65
|
-
_ = fcntl(serverFd, F_SETFL, flags | O_NONBLOCK)
|
|
66
|
-
|
|
67
|
-
// 4. GCD dispatch source for accepting connections
|
|
68
|
-
let source = DispatchSource.makeReadSource(fileDescriptor: serverFd, queue: queue)
|
|
69
|
-
source.setEventHandler { [weak self] in self?.acceptConnection() }
|
|
70
|
-
source.setCancelHandler { [weak self] in
|
|
71
|
-
if let fd = self?.serverFd, fd >= 0 { close(fd) }
|
|
72
|
-
self?.serverFd = -1
|
|
73
|
-
}
|
|
74
|
-
source.resume()
|
|
75
|
-
acceptSource = source
|
|
76
|
-
|
|
77
|
-
DispatchQueue.main.async { self.isListening = true }
|
|
78
|
-
diag.success("DaemonServer: listening on ws://127.0.0.1:9399")
|
|
79
|
-
|
|
80
|
-
// Subscribe to EventBus for broadcasting
|
|
81
|
-
EventBus.shared.subscribe { [weak self] event in
|
|
82
|
-
self?.broadcastEvent(event)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
func stop() {
|
|
87
|
-
acceptSource?.cancel()
|
|
88
|
-
acceptSource = nil
|
|
89
|
-
lock.lock()
|
|
90
|
-
for (_, client) in clients {
|
|
91
|
-
close(client.fd)
|
|
92
|
-
}
|
|
93
|
-
clients.removeAll()
|
|
94
|
-
lock.unlock()
|
|
95
|
-
DispatchQueue.main.async {
|
|
96
|
-
self.clientCount = 0
|
|
97
|
-
self.isListening = false
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
func broadcast(_ event: DaemonEvent) {
|
|
102
|
-
guard let data = try? encoder.encode(event),
|
|
103
|
-
let text = String(data: data, encoding: .utf8) else { return }
|
|
104
|
-
lock.lock()
|
|
105
|
-
let snapshot = clients
|
|
106
|
-
lock.unlock()
|
|
107
|
-
for (_, client) in snapshot {
|
|
108
|
-
sendWebSocketText(text, to: client)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// MARK: - Accept
|
|
113
|
-
|
|
114
|
-
private func acceptConnection() {
|
|
115
|
-
var clientAddr = sockaddr_in()
|
|
116
|
-
var addrLen = socklen_t(MemoryLayout<sockaddr_in>.size)
|
|
117
|
-
let clientFd = withUnsafeMutablePointer(to: &clientAddr) {
|
|
118
|
-
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
119
|
-
accept(serverFd, $0, &addrLen)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
guard clientFd >= 0 else { return }
|
|
123
|
-
|
|
124
|
-
// A client can disconnect immediately after a large response. On Darwin,
|
|
125
|
-
// writing to that socket can otherwise raise SIGPIPE and terminate the app.
|
|
126
|
-
var noSigPipe: Int32 = 1
|
|
127
|
-
setsockopt(clientFd, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, socklen_t(MemoryLayout<Int32>.size))
|
|
128
|
-
|
|
129
|
-
let id = UUID()
|
|
130
|
-
let client = WebSocketClient(id: id, fd: clientFd)
|
|
131
|
-
|
|
132
|
-
// Read the HTTP upgrade request
|
|
133
|
-
queue.async { [weak self] in
|
|
134
|
-
self?.performHandshake(client: client)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// MARK: - WebSocket Handshake
|
|
139
|
-
|
|
140
|
-
private func performHandshake(client: WebSocketClient) {
|
|
141
|
-
let diag = DiagnosticLog.shared
|
|
142
|
-
|
|
143
|
-
// Ensure blocking mode for handshake read
|
|
144
|
-
let curFlags = fcntl(client.fd, F_GETFL)
|
|
145
|
-
if curFlags & O_NONBLOCK != 0 {
|
|
146
|
-
_ = fcntl(client.fd, F_SETFL, curFlags & ~O_NONBLOCK)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Read HTTP request (up to 4KB)
|
|
150
|
-
var buf = [UInt8](repeating: 0, count: 4096)
|
|
151
|
-
let n = read(client.fd, &buf, buf.count)
|
|
152
|
-
guard n > 0 else {
|
|
153
|
-
close(client.fd)
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let request = String(bytes: buf[..<n], encoding: .utf8) ?? ""
|
|
158
|
-
|
|
159
|
-
// Extract Sec-WebSocket-Key
|
|
160
|
-
guard let keyLine = request.split(separator: "\r\n").first(where: {
|
|
161
|
-
$0.lowercased().hasPrefix("sec-websocket-key:")
|
|
162
|
-
}) else {
|
|
163
|
-
close(client.fd)
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
let key = keyLine.split(separator: ":", maxSplits: 1)[1].trimmingCharacters(in: .whitespaces)
|
|
167
|
-
|
|
168
|
-
// Compute accept key: Base64(SHA1(key + magic))
|
|
169
|
-
let magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
170
|
-
let combined = key + magic
|
|
171
|
-
let acceptKey = sha1Base64(combined)
|
|
172
|
-
|
|
173
|
-
// Send HTTP 101 response
|
|
174
|
-
let response = "HTTP/1.1 101 Switching Protocols\r\n" +
|
|
175
|
-
"Upgrade: websocket\r\n" +
|
|
176
|
-
"Connection: Upgrade\r\n" +
|
|
177
|
-
"Sec-WebSocket-Accept: \(acceptKey)\r\n\r\n"
|
|
178
|
-
let responseBytes = Array(response.utf8)
|
|
179
|
-
responseBytes.withUnsafeBufferPointer { ptr in
|
|
180
|
-
_ = write(client.fd, ptr.baseAddress!, ptr.count)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Register client
|
|
184
|
-
lock.lock()
|
|
185
|
-
clients[client.id] = client
|
|
186
|
-
let count = clients.count
|
|
187
|
-
lock.unlock()
|
|
188
|
-
DispatchQueue.main.async { self.clientCount = count }
|
|
189
|
-
diag.info("DaemonServer: client connected (\(count) total)")
|
|
190
|
-
|
|
191
|
-
// Start read loop
|
|
192
|
-
readLoop(client: client)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// MARK: - WebSocket Frame I/O
|
|
196
|
-
|
|
197
|
-
private func readLoop(client: WebSocketClient) {
|
|
198
|
-
// Make non-blocking and use a dispatch source
|
|
199
|
-
let flags = fcntl(client.fd, F_GETFL)
|
|
200
|
-
_ = fcntl(client.fd, F_SETFL, flags | O_NONBLOCK)
|
|
201
|
-
|
|
202
|
-
let source = DispatchSource.makeReadSource(fileDescriptor: client.fd, queue: queue)
|
|
203
|
-
client.readSource = source
|
|
204
|
-
source.setEventHandler { [weak self] in
|
|
205
|
-
self?.readFrame(client: client)
|
|
206
|
-
}
|
|
207
|
-
source.setCancelHandler { [weak self] in
|
|
208
|
-
self?.removeClient(client)
|
|
209
|
-
}
|
|
210
|
-
source.resume()
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private func readFrame(client: WebSocketClient) {
|
|
214
|
-
// Read available data into client buffer
|
|
215
|
-
var buf = [UInt8](repeating: 0, count: 65536)
|
|
216
|
-
let n = read(client.fd, &buf, buf.count)
|
|
217
|
-
if n <= 0 {
|
|
218
|
-
client.readSource?.cancel()
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
client.buffer.append(contentsOf: buf[..<n])
|
|
222
|
-
|
|
223
|
-
// Process complete frames
|
|
224
|
-
while let frame = parseFrame(&client.buffer) {
|
|
225
|
-
switch frame.opcode {
|
|
226
|
-
case 0x1: // Text
|
|
227
|
-
if let text = String(bytes: frame.payload, encoding: .utf8),
|
|
228
|
-
let data = text.data(using: .utf8) {
|
|
229
|
-
handleMessage(data, client: client)
|
|
230
|
-
}
|
|
231
|
-
case 0x8: // Close
|
|
232
|
-
// Send close frame back
|
|
233
|
-
sendFrame(opcode: 0x8, payload: [], to: client)
|
|
234
|
-
client.readSource?.cancel()
|
|
235
|
-
return
|
|
236
|
-
case 0x9: // Ping → Pong
|
|
237
|
-
sendFrame(opcode: 0xA, payload: frame.payload, to: client)
|
|
238
|
-
case 0xA: // Pong — ignore
|
|
239
|
-
break
|
|
240
|
-
default:
|
|
241
|
-
break
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private struct WSFrame {
|
|
247
|
-
let opcode: UInt8
|
|
248
|
-
let payload: [UInt8]
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private func parseFrame(_ buffer: inout [UInt8]) -> WSFrame? {
|
|
252
|
-
guard buffer.count >= 2 else { return nil }
|
|
253
|
-
|
|
254
|
-
let byte0 = buffer[0]
|
|
255
|
-
let byte1 = buffer[1]
|
|
256
|
-
let opcode = byte0 & 0x0F
|
|
257
|
-
let masked = (byte1 & 0x80) != 0
|
|
258
|
-
var payloadLen = UInt64(byte1 & 0x7F)
|
|
259
|
-
var offset = 2
|
|
260
|
-
|
|
261
|
-
if payloadLen == 126 {
|
|
262
|
-
guard buffer.count >= 4 else { return nil }
|
|
263
|
-
payloadLen = UInt64(buffer[2]) << 8 | UInt64(buffer[3])
|
|
264
|
-
offset = 4
|
|
265
|
-
} else if payloadLen == 127 {
|
|
266
|
-
guard buffer.count >= 10 else { return nil }
|
|
267
|
-
payloadLen = 0
|
|
268
|
-
for i in 0..<8 { payloadLen = payloadLen << 8 | UInt64(buffer[2 + i]) }
|
|
269
|
-
offset = 10
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
let maskSize = masked ? 4 : 0
|
|
273
|
-
let totalNeeded = offset + maskSize + Int(payloadLen)
|
|
274
|
-
guard buffer.count >= totalNeeded else { return nil }
|
|
275
|
-
|
|
276
|
-
var payload: [UInt8]
|
|
277
|
-
if masked {
|
|
278
|
-
let mask = Array(buffer[offset..<(offset + 4)])
|
|
279
|
-
let dataStart = offset + 4
|
|
280
|
-
payload = Array(buffer[dataStart..<(dataStart + Int(payloadLen))])
|
|
281
|
-
for i in 0..<payload.count {
|
|
282
|
-
payload[i] ^= mask[i % 4]
|
|
283
|
-
}
|
|
284
|
-
} else {
|
|
285
|
-
payload = Array(buffer[offset..<(offset + Int(payloadLen))])
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
buffer.removeFirst(totalNeeded)
|
|
289
|
-
return WSFrame(opcode: opcode, payload: payload)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private func sendFrame(opcode: UInt8, payload: [UInt8], to client: WebSocketClient) {
|
|
293
|
-
var frame: [UInt8] = [0x80 | opcode] // FIN + opcode
|
|
294
|
-
|
|
295
|
-
if payload.count < 126 {
|
|
296
|
-
frame.append(UInt8(payload.count))
|
|
297
|
-
} else if payload.count < 65536 {
|
|
298
|
-
frame.append(126)
|
|
299
|
-
frame.append(UInt8((payload.count >> 8) & 0xFF))
|
|
300
|
-
frame.append(UInt8(payload.count & 0xFF))
|
|
301
|
-
} else {
|
|
302
|
-
frame.append(127)
|
|
303
|
-
for i in (0..<8).reversed() {
|
|
304
|
-
frame.append(UInt8((payload.count >> (i * 8)) & 0xFF))
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
frame.append(contentsOf: payload)
|
|
308
|
-
|
|
309
|
-
frame.withUnsafeBufferPointer { ptr in
|
|
310
|
-
_ = write(client.fd, ptr.baseAddress!, ptr.count)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private func sendWebSocketText(_ text: String, to client: WebSocketClient) {
|
|
315
|
-
let payload = Array(text.utf8)
|
|
316
|
-
sendFrame(opcode: 0x1, payload: payload, to: client)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// MARK: - Message Handling
|
|
320
|
-
|
|
321
|
-
private func handleMessage(_ data: Data, client: WebSocketClient) {
|
|
322
|
-
guard let request = try? decoder.decode(DaemonRequest.self, from: data) else {
|
|
323
|
-
let errResponse = DaemonResponse(id: "?", result: nil, error: "Invalid request JSON")
|
|
324
|
-
sendResponse(errResponse, to: client)
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
let response = LatticesApi.shared.handle(request)
|
|
329
|
-
sendResponse(response, to: client)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private func sendResponse(_ response: DaemonResponse, to client: WebSocketClient) {
|
|
333
|
-
guard let data = try? encoder.encode(response),
|
|
334
|
-
let text = String(data: data, encoding: .utf8) else { return }
|
|
335
|
-
sendWebSocketText(text, to: client)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// MARK: - Client Management
|
|
339
|
-
|
|
340
|
-
private func removeClient(_ client: WebSocketClient) {
|
|
341
|
-
close(client.fd)
|
|
342
|
-
lock.lock()
|
|
343
|
-
clients.removeValue(forKey: client.id)
|
|
344
|
-
let count = clients.count
|
|
345
|
-
lock.unlock()
|
|
346
|
-
DispatchQueue.main.async { self.clientCount = count }
|
|
347
|
-
DiagnosticLog.shared.info("DaemonServer: client disconnected (\(count) total)")
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// MARK: - Crypto Helper
|
|
351
|
-
|
|
352
|
-
private func sha1Base64(_ string: String) -> String {
|
|
353
|
-
let data = Array(string.utf8)
|
|
354
|
-
var hash = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
|
|
355
|
-
CC_SHA1(data, CC_LONG(data.count), &hash)
|
|
356
|
-
return Data(hash).base64EncodedString()
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// MARK: - Event Broadcasting
|
|
360
|
-
|
|
361
|
-
private func broadcastEvent(_ event: ModelEvent) {
|
|
362
|
-
let daemonEvent: DaemonEvent
|
|
363
|
-
switch event {
|
|
364
|
-
case .windowsChanged(let windows, let added, let removed):
|
|
365
|
-
daemonEvent = DaemonEvent(
|
|
366
|
-
event: "windows.changed",
|
|
367
|
-
data: .object([
|
|
368
|
-
"windowCount": .int(windows.count),
|
|
369
|
-
"added": .array(added.map { .int(Int($0)) }),
|
|
370
|
-
"removed": .array(removed.map { .int(Int($0)) })
|
|
371
|
-
])
|
|
372
|
-
)
|
|
373
|
-
case .tmuxChanged(let sessions):
|
|
374
|
-
daemonEvent = DaemonEvent(
|
|
375
|
-
event: "tmux.changed",
|
|
376
|
-
data: .object([
|
|
377
|
-
"sessionCount": .int(sessions.count),
|
|
378
|
-
"sessions": .array(sessions.map { .string($0.name) })
|
|
379
|
-
])
|
|
380
|
-
)
|
|
381
|
-
case .layerSwitched(let index):
|
|
382
|
-
daemonEvent = DaemonEvent(
|
|
383
|
-
event: "layer.switched",
|
|
384
|
-
data: .object(["index": .int(index)])
|
|
385
|
-
)
|
|
386
|
-
case .processesChanged(let interesting):
|
|
387
|
-
daemonEvent = DaemonEvent(
|
|
388
|
-
event: "processes.changed",
|
|
389
|
-
data: .object([
|
|
390
|
-
"interestingCount": .int(interesting.count),
|
|
391
|
-
"pids": .array(interesting.map { .int($0) })
|
|
392
|
-
])
|
|
393
|
-
)
|
|
394
|
-
case .ocrScanComplete(let windowCount, let totalBlocks):
|
|
395
|
-
daemonEvent = DaemonEvent(
|
|
396
|
-
event: "ocr.scanComplete",
|
|
397
|
-
data: .object([
|
|
398
|
-
"windowCount": .int(windowCount),
|
|
399
|
-
"totalBlocks": .int(totalBlocks)
|
|
400
|
-
])
|
|
401
|
-
)
|
|
402
|
-
case .voiceCommand(let text, let confidence):
|
|
403
|
-
daemonEvent = DaemonEvent(
|
|
404
|
-
event: "voice.command",
|
|
405
|
-
data: .object([
|
|
406
|
-
"text": .string(text),
|
|
407
|
-
"confidence": .double(confidence)
|
|
408
|
-
])
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
broadcast(daemonEvent)
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// MARK: - Client State
|
|
416
|
-
|
|
417
|
-
final class WebSocketClient {
|
|
418
|
-
let id: UUID
|
|
419
|
-
let fd: Int32
|
|
420
|
-
var buffer: [UInt8] = []
|
|
421
|
-
var readSource: DispatchSourceRead?
|
|
422
|
-
|
|
423
|
-
init(id: UUID, fd: Int32) {
|
|
424
|
-
self.id = id
|
|
425
|
-
self.fd = fd
|
|
426
|
-
}
|
|
427
|
-
}
|