@lattices/cli 0.4.2 → 0.4.5

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.
Files changed (70) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +90 -34
  8. package/app/Sources/AppShellView.swift +2 -0
  9. package/app/Sources/AppTypeClassifier.swift +36 -0
  10. package/app/Sources/AppUpdater.swift +92 -0
  11. package/app/Sources/CheatSheetHUD.swift +1 -0
  12. package/app/Sources/CliActionLauncher.swift +50 -0
  13. package/app/Sources/CommandModeView.swift +4 -24
  14. package/app/Sources/CompanionActivityLog.swift +70 -0
  15. package/app/Sources/CompanionKeyboardController.swift +141 -0
  16. package/app/Sources/DesktopModel.swift +4 -0
  17. package/app/Sources/HandsOffSession.swift +15 -4
  18. package/app/Sources/HomeDashboardView.swift +18 -10
  19. package/app/Sources/HotkeyStore.swift +8 -5
  20. package/app/Sources/IntentEngine.swift +7 -1
  21. package/app/Sources/LatticesApi.swift +125 -4
  22. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  23. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  24. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  25. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  26. package/app/Sources/LatticesDeckHost.swift +1463 -0
  27. package/app/Sources/LatticesRuntime.swift +61 -0
  28. package/app/Sources/MainView.swift +351 -191
  29. package/app/Sources/MouseFinder.swift +335 -30
  30. package/app/Sources/MouseGestureConfig.swift +364 -0
  31. package/app/Sources/MouseGestureController.swift +1203 -0
  32. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  33. package/app/Sources/MouseInputEventViewer.swift +272 -0
  34. package/app/Sources/MouseShortcutStore.swift +107 -0
  35. package/app/Sources/OmniSearchView.swift +136 -2
  36. package/app/Sources/OmniSearchWindow.swift +65 -5
  37. package/app/Sources/OnboardingView.swift +30 -16
  38. package/app/Sources/PaletteCommand.swift +26 -6
  39. package/app/Sources/PermissionChecker.swift +76 -2
  40. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  41. package/app/Sources/PiAuthPromptCard.swift +90 -0
  42. package/app/Sources/PiChatDock.swift +137 -74
  43. package/app/Sources/PiChatSession.swift +608 -108
  44. package/app/Sources/PiInstallCallout.swift +86 -0
  45. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  46. package/app/Sources/PiWorkspaceView.swift +174 -77
  47. package/app/Sources/Preferences.swift +78 -0
  48. package/app/Sources/ScreenMapState.swift +91 -31
  49. package/app/Sources/ScreenMapView.swift +510 -524
  50. package/app/Sources/ScreenMapWindowController.swift +12 -4
  51. package/app/Sources/SettingsView.swift +869 -152
  52. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  53. package/app/Sources/VoiceCommandWindow.swift +23 -2
  54. package/app/Sources/WindowDragSnapController.swift +628 -0
  55. package/app/Sources/WindowTiler.swift +328 -65
  56. package/app/Sources/WorkspaceManager.swift +288 -0
  57. package/bin/assistant-intelligence.ts +874 -0
  58. package/bin/handsoff-infer.ts +16 -209
  59. package/bin/handsoff-worker.ts +45 -258
  60. package/bin/lattices-app.ts +62 -0
  61. package/bin/lattices-dev +4 -0
  62. package/bin/lattices.ts +125 -14
  63. package/docs/agents.md +14 -0
  64. package/docs/api.md +55 -0
  65. package/docs/app.md +3 -0
  66. package/docs/companion-deck.md +180 -0
  67. package/docs/config.md +25 -0
  68. package/docs/tiling-reference.md +55 -0
  69. package/docs/voice-error-model.md +73 -0
  70. package/package.json +2 -1
@@ -0,0 +1,438 @@
1
+ import Darwin
2
+ import DeckKit
3
+ import Foundation
4
+
5
+ final class LatticesCompanionBridgeServer: NSObject {
6
+ static let shared = LatticesCompanionBridgeServer()
7
+
8
+ static let bonjourType = "_lattices-companion._tcp."
9
+ static let defaultPort: UInt16 = 5287
10
+
11
+ private let queue = DispatchQueue(label: "lattices.companion.bridge", qos: .userInitiated)
12
+ private let encoder = JSONEncoder()
13
+ private let decoder = JSONDecoder()
14
+
15
+ private var serverFd: Int32 = -1
16
+ private var acceptSource: DispatchSourceRead?
17
+ private var service: NetService?
18
+
19
+ private override init() {
20
+ super.init()
21
+ }
22
+
23
+ func start() {
24
+ guard acceptSource == nil else { return }
25
+
26
+ let diag = DiagnosticLog.shared
27
+ serverFd = socket(AF_INET, SOCK_STREAM, 0)
28
+ guard serverFd >= 0 else {
29
+ diag.error("CompanionBridge: socket() failed — errno \(errno)")
30
+ return
31
+ }
32
+
33
+ var yes: Int32 = 1
34
+ setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout<Int32>.size))
35
+
36
+ var addr = sockaddr_in()
37
+ addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
38
+ addr.sin_family = sa_family_t(AF_INET)
39
+ addr.sin_port = Self.defaultPort.bigEndian
40
+ addr.sin_addr.s_addr = INADDR_ANY.bigEndian
41
+
42
+ let bindResult = withUnsafePointer(to: &addr) {
43
+ $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
44
+ Darwin.bind(serverFd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
45
+ }
46
+ }
47
+ guard bindResult == 0 else {
48
+ diag.error("CompanionBridge: bind() failed — errno \(errno)")
49
+ close(serverFd)
50
+ serverFd = -1
51
+ return
52
+ }
53
+
54
+ guard listen(serverFd, 8) == 0 else {
55
+ diag.error("CompanionBridge: listen() failed — errno \(errno)")
56
+ close(serverFd)
57
+ serverFd = -1
58
+ return
59
+ }
60
+
61
+ let flags = fcntl(serverFd, F_GETFL)
62
+ _ = fcntl(serverFd, F_SETFL, flags | O_NONBLOCK)
63
+
64
+ let source = DispatchSource.makeReadSource(fileDescriptor: serverFd, queue: queue)
65
+ source.setEventHandler { [weak self] in
66
+ self?.acceptConnection()
67
+ }
68
+ source.setCancelHandler { [weak self] in
69
+ guard let self else { return }
70
+ if self.serverFd >= 0 {
71
+ close(self.serverFd)
72
+ self.serverFd = -1
73
+ }
74
+ }
75
+ source.resume()
76
+ acceptSource = source
77
+
78
+ publishBonjour()
79
+ diag.success("CompanionBridge: listening on http://0.0.0.0:\(Self.defaultPort)")
80
+ }
81
+
82
+ func stop() {
83
+ acceptSource?.cancel()
84
+ acceptSource = nil
85
+ service?.stop()
86
+ service = nil
87
+ }
88
+ }
89
+
90
+ private extension LatticesCompanionBridgeServer {
91
+ struct HTTPRequest {
92
+ let method: String
93
+ let path: String
94
+ let headers: [String: String]
95
+ let body: Data
96
+ }
97
+
98
+ struct HealthResponse: Codable {
99
+ let ok: Bool
100
+ let name: String
101
+ let serviceType: String
102
+ let hostName: String
103
+ let port: UInt16
104
+ let version: String
105
+ let mode: String
106
+ let bridgePublicKey: String
107
+ let bridgeFingerprint: String
108
+ let requestSigningRequired: Bool
109
+ let payloadEncryptionRequired: Bool
110
+ }
111
+
112
+ func publishBonjour() {
113
+ let advertisedName = Host.current().localizedName ?? "Lattices Companion"
114
+ let service = NetService(
115
+ domain: "local.",
116
+ type: Self.bonjourType,
117
+ name: advertisedName,
118
+ port: Int32(Self.defaultPort)
119
+ )
120
+ service.includesPeerToPeer = true
121
+ service.publish()
122
+ self.service = service
123
+ }
124
+
125
+ func acceptConnection() {
126
+ while true {
127
+ var clientAddr = sockaddr_in()
128
+ var addrLen = socklen_t(MemoryLayout<sockaddr_in>.size)
129
+
130
+ let clientFd = withUnsafeMutablePointer(to: &clientAddr) {
131
+ $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
132
+ accept(serverFd, $0, &addrLen)
133
+ }
134
+ }
135
+
136
+ if clientFd < 0 {
137
+ if errno != EAGAIN && errno != EWOULDBLOCK {
138
+ DiagnosticLog.shared.error("CompanionBridge: accept() failed — errno \(errno)")
139
+ }
140
+ return
141
+ }
142
+
143
+ let clientFlags = fcntl(clientFd, F_GETFL)
144
+ if clientFlags >= 0 {
145
+ _ = fcntl(clientFd, F_SETFL, clientFlags & ~O_NONBLOCK)
146
+ }
147
+
148
+ queue.async { [weak self] in
149
+ self?.handleClient(fd: clientFd)
150
+ }
151
+ }
152
+ }
153
+
154
+ func handleClient(fd: Int32) {
155
+ defer { close(fd) }
156
+
157
+ guard let request = readRequest(from: fd) else {
158
+ sendError(status: 400, message: "Invalid HTTP request", to: fd)
159
+ return
160
+ }
161
+
162
+ do {
163
+ try route(request, to: fd)
164
+ } catch let error as LatticesCompanionSecurityError {
165
+ let status: Int
166
+ switch error {
167
+ case .untrustedDevice:
168
+ status = 403
169
+ case .missingHeader, .staleRequest, .replayedRequest, .invalidSignature, .invalidEnvelope, .invalidDeviceKey:
170
+ status = 401
171
+ }
172
+ sendError(status: status, message: error.localizedDescription, to: fd)
173
+ } catch {
174
+ sendError(status: 500, message: error.localizedDescription, to: fd)
175
+ }
176
+ }
177
+
178
+ func route(_ request: HTTPRequest, to fd: Int32) throws {
179
+ switch (request.method, request.path) {
180
+ case ("GET", "/health"):
181
+ let security = LatticesDeckHost.shared.securityConfiguration
182
+ let response = HealthResponse(
183
+ ok: true,
184
+ name: Host.current().localizedName ?? "Lattices Companion",
185
+ serviceType: Self.bonjourType,
186
+ hostName: localHostName(),
187
+ port: Self.defaultPort,
188
+ version: LatticesRuntime.appVersion,
189
+ mode: "local-network-secure",
190
+ bridgePublicKey: LatticesCompanionSecurityCoordinator.shared.bridgePublicKeyBase64,
191
+ bridgeFingerprint: LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint,
192
+ requestSigningRequired: security.requestSigningRequired,
193
+ payloadEncryptionRequired: security.payloadEncryptionRequired
194
+ )
195
+ try sendJSON(status: 200, value: response, to: fd)
196
+
197
+ case ("GET", "/deck/manifest"):
198
+ try sendJSON(status: 200, value: LatticesDeckHost.shared.manifestSync(), to: fd)
199
+
200
+ case ("POST", "/pairing/request"):
201
+ let pairingRequest = try decoder.decode(DeckPairingRequest.self, from: request.body)
202
+ let response = LatticesCompanionSecurityCoordinator.shared.handlePairingRequest(pairingRequest)
203
+ let status = response.disposition == .denied ? 403 : 200
204
+ try sendJSON(status: status, value: response, to: fd)
205
+
206
+ case ("GET", "/deck/snapshot"):
207
+ let auth = try authorizeProtectedRequest(request)
208
+ let snapshot = try LatticesDeckHost.shared.runtimeSnapshotSync()
209
+ let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
210
+ snapshot,
211
+ auth: auth,
212
+ status: 200,
213
+ path: request.path
214
+ )
215
+ try sendJSON(status: 200, value: response, to: fd)
216
+
217
+ case ("POST", "/deck/perform"):
218
+ let auth = try authorizeProtectedRequest(request)
219
+ let action = try LatticesCompanionSecurityCoordinator.shared.decodeProtectedBody(
220
+ DeckActionRequest.self,
221
+ body: request.body,
222
+ auth: auth,
223
+ method: request.method,
224
+ path: request.path
225
+ )
226
+ let result = try LatticesDeckHost.shared.performSync(action)
227
+ let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
228
+ result,
229
+ auth: auth,
230
+ status: 200,
231
+ path: request.path
232
+ )
233
+ try sendJSON(status: 200, value: response, to: fd)
234
+
235
+ case ("POST", "/deck/trackpad"):
236
+ let auth = try authorizeProtectedRequest(request)
237
+ let eventRequest = try LatticesCompanionSecurityCoordinator.shared.decodeProtectedBody(
238
+ DeckTrackpadEventRequest.self,
239
+ body: request.body,
240
+ auth: auth,
241
+ method: request.method,
242
+ path: request.path
243
+ )
244
+ let result = LatticesCompanionTrackpadController.shared.perform(eventRequest)
245
+ let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
246
+ result,
247
+ auth: auth,
248
+ status: 200,
249
+ path: request.path
250
+ )
251
+ try sendJSON(status: 200, value: response, to: fd)
252
+
253
+ default:
254
+ sendError(status: 404, message: "Unknown route", to: fd)
255
+ }
256
+ }
257
+
258
+ func authorizeProtectedRequest(_ request: HTTPRequest) throws -> AuthorizedBridgeRequest {
259
+ let security = LatticesDeckHost.shared.securityConfiguration
260
+ guard security.requestSigningRequired else {
261
+ throw LatticesCompanionSecurityError.untrustedDevice
262
+ }
263
+ return try LatticesCompanionSecurityCoordinator.shared.authorize(
264
+ method: request.method,
265
+ path: request.path,
266
+ headers: request.headers,
267
+ body: request.body
268
+ )
269
+ }
270
+
271
+ func readRequest(from fd: Int32) -> HTTPRequest? {
272
+ var buffer = Data()
273
+ let deadline = DispatchTime.now().uptimeNanoseconds + 2_000_000_000
274
+ let delimiterCRLF = Data([13, 10, 13, 10])
275
+ let delimiterLF = Data([10, 10])
276
+
277
+ var headerRange = buffer.range(of: delimiterCRLF) ?? buffer.range(of: delimiterLF)
278
+ while headerRange == nil {
279
+ guard let count = readChunk(from: fd, deadline: deadline, into: &buffer) else {
280
+ return nil
281
+ }
282
+ guard count > 0 else { return nil }
283
+ if buffer.count > 128 * 1024 {
284
+ return nil
285
+ }
286
+ headerRange = buffer.range(of: delimiterCRLF) ?? buffer.range(of: delimiterLF)
287
+ }
288
+
289
+ guard let headerRange else { return nil }
290
+ let headerData = buffer.subdata(in: 0..<headerRange.lowerBound)
291
+ guard let headerText = String(data: headerData, encoding: .utf8) else {
292
+ return nil
293
+ }
294
+
295
+ let lines = headerText
296
+ .replacingOccurrences(of: "\r\n", with: "\n")
297
+ .components(separatedBy: "\n")
298
+ guard let requestLine = lines.first else { return nil }
299
+ let requestParts = requestLine.split(separator: " ", omittingEmptySubsequences: true)
300
+ guard requestParts.count >= 2 else { return nil }
301
+
302
+ let method = String(requestParts[0]).uppercased()
303
+ let rawPath = String(requestParts[1])
304
+ let path = rawPath.split(separator: "?", maxSplits: 1).first.map(String.init) ?? rawPath
305
+
306
+ var headers: [String: String] = [:]
307
+ for line in lines.dropFirst() {
308
+ guard let separator = line.firstIndex(of: ":") else { continue }
309
+ let name = line[..<separator].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
310
+ let value = line[line.index(after: separator)...].trimmingCharacters(in: .whitespacesAndNewlines)
311
+ headers[name] = value
312
+ }
313
+
314
+ let contentLength = Int(headers["content-length"] ?? "") ?? 0
315
+ var body = Data(buffer[headerRange.upperBound...])
316
+ while body.count < contentLength {
317
+ guard let count = readChunk(
318
+ from: fd,
319
+ deadline: deadline,
320
+ chunkSize: min(4096, contentLength - body.count),
321
+ into: &body
322
+ ) else {
323
+ return nil
324
+ }
325
+ guard count > 0 else { return nil }
326
+ }
327
+ if body.count > contentLength {
328
+ body = body.prefix(contentLength)
329
+ }
330
+
331
+ return HTTPRequest(method: method, path: path, headers: headers, body: body)
332
+ }
333
+
334
+ func readChunk(
335
+ from fd: Int32,
336
+ deadline: UInt64,
337
+ chunkSize: Int = 4096,
338
+ into buffer: inout Data
339
+ ) -> Int? {
340
+ var chunk = [UInt8](repeating: 0, count: chunkSize)
341
+
342
+ while true {
343
+ let count = Darwin.read(fd, &chunk, chunk.count)
344
+ if count > 0 {
345
+ buffer.append(contentsOf: chunk[..<count])
346
+ return count
347
+ }
348
+ if count == 0 {
349
+ return 0
350
+ }
351
+
352
+ if errno == EINTR {
353
+ continue
354
+ }
355
+ if (errno == EAGAIN || errno == EWOULDBLOCK) &&
356
+ DispatchTime.now().uptimeNanoseconds < deadline {
357
+ usleep(10_000)
358
+ continue
359
+ }
360
+
361
+ DiagnosticLog.shared.error("CompanionBridge: read() failed — errno \(errno)")
362
+ return nil
363
+ }
364
+ }
365
+
366
+ func sendJSON<T: Encodable>(status: Int, value: T, to fd: Int32) throws {
367
+ let body = try encoder.encode(value)
368
+ sendResponse(status: status, contentType: "application/json; charset=utf-8", body: body, to: fd)
369
+ }
370
+
371
+ func sendError(status: Int, message: String, to fd: Int32) {
372
+ let payload: [String: Any] = ["ok": false, "error": message]
373
+ guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return }
374
+ sendResponse(status: status, contentType: "application/json; charset=utf-8", body: body, to: fd)
375
+ }
376
+
377
+ func sendResponse(status: Int, contentType: String, body: Data, to fd: Int32) {
378
+ let reason = reasonPhrase(for: status)
379
+ let header = [
380
+ "HTTP/1.1 \(status) \(reason)",
381
+ "Content-Type: \(contentType)",
382
+ "Content-Length: \(body.count)",
383
+ "Connection: close",
384
+ "",
385
+ ""
386
+ ].joined(separator: "\r\n")
387
+ writeAll(Data(header.utf8), to: fd)
388
+ writeAll(body, to: fd)
389
+ _ = shutdown(fd, SHUT_WR)
390
+ }
391
+
392
+ func writeAll(_ data: Data, to fd: Int32) {
393
+ data.withUnsafeBytes { rawBuffer in
394
+ guard var pointer = rawBuffer.bindMemory(to: UInt8.self).baseAddress else { return }
395
+ var remaining = data.count
396
+ while remaining > 0 {
397
+ let written = write(fd, pointer, remaining)
398
+ guard written > 0 else { return }
399
+ remaining -= written
400
+ pointer = pointer.advanced(by: written)
401
+ }
402
+ }
403
+ }
404
+
405
+ func reasonPhrase(for status: Int) -> String {
406
+ switch status {
407
+ case 200: return "OK"
408
+ case 400: return "Bad Request"
409
+ case 401: return "Unauthorized"
410
+ case 403: return "Forbidden"
411
+ case 404: return "Not Found"
412
+ default: return "Internal Server Error"
413
+ }
414
+ }
415
+
416
+ func localHostName() -> String {
417
+ let process = Process()
418
+ process.executableURL = URL(fileURLWithPath: "/usr/sbin/scutil")
419
+ process.arguments = ["--get", "LocalHostName"]
420
+
421
+ let pipe = Pipe()
422
+ process.standardOutput = pipe
423
+ process.standardError = Pipe()
424
+
425
+ do {
426
+ try process.run()
427
+ process.waitUntilExit()
428
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
429
+ if let name = String(data: data, encoding: .utf8)?
430
+ .trimmingCharacters(in: .whitespacesAndNewlines),
431
+ !name.isEmpty {
432
+ return "\(name).local"
433
+ }
434
+ } catch { }
435
+
436
+ return Host.current().localizedName ?? "localhost"
437
+ }
438
+ }