@arach/lattices 0.1.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.
Files changed (64) hide show
  1. package/README.md +157 -0
  2. package/app/Lattices.app/Contents/Info.plist +24 -0
  3. package/app/Package.swift +13 -0
  4. package/app/Sources/App.swift +49 -0
  5. package/app/Sources/AppDelegate.swift +104 -0
  6. package/app/Sources/AppShellView.swift +62 -0
  7. package/app/Sources/AppTypeClassifier.swift +70 -0
  8. package/app/Sources/AppWindowShell.swift +63 -0
  9. package/app/Sources/CheatSheetHUD.swift +331 -0
  10. package/app/Sources/CommandModeState.swift +1341 -0
  11. package/app/Sources/CommandModeView.swift +1380 -0
  12. package/app/Sources/CommandModeWindow.swift +192 -0
  13. package/app/Sources/CommandPaletteView.swift +307 -0
  14. package/app/Sources/CommandPaletteWindow.swift +134 -0
  15. package/app/Sources/DaemonProtocol.swift +101 -0
  16. package/app/Sources/DaemonServer.swift +406 -0
  17. package/app/Sources/DesktopModel.swift +121 -0
  18. package/app/Sources/DesktopModelTypes.swift +71 -0
  19. package/app/Sources/DiagnosticLog.swift +253 -0
  20. package/app/Sources/EventBus.swift +29 -0
  21. package/app/Sources/HotkeyManager.swift +249 -0
  22. package/app/Sources/HotkeyStore.swift +330 -0
  23. package/app/Sources/InventoryManager.swift +35 -0
  24. package/app/Sources/InventoryPath.swift +43 -0
  25. package/app/Sources/KeyRecorderView.swift +210 -0
  26. package/app/Sources/LatticesApi.swift +915 -0
  27. package/app/Sources/MainView.swift +507 -0
  28. package/app/Sources/MainWindow.swift +70 -0
  29. package/app/Sources/OrphanRow.swift +129 -0
  30. package/app/Sources/PaletteCommand.swift +409 -0
  31. package/app/Sources/PermissionChecker.swift +115 -0
  32. package/app/Sources/Preferences.swift +48 -0
  33. package/app/Sources/ProcessModel.swift +199 -0
  34. package/app/Sources/ProcessQuery.swift +151 -0
  35. package/app/Sources/Project.swift +28 -0
  36. package/app/Sources/ProjectRow.swift +368 -0
  37. package/app/Sources/ProjectScanner.swift +121 -0
  38. package/app/Sources/ScreenMapState.swift +2397 -0
  39. package/app/Sources/ScreenMapView.swift +2817 -0
  40. package/app/Sources/ScreenMapWindowController.swift +89 -0
  41. package/app/Sources/SessionManager.swift +72 -0
  42. package/app/Sources/SettingsView.swift +641 -0
  43. package/app/Sources/SettingsWindow.swift +20 -0
  44. package/app/Sources/TabGroupRow.swift +178 -0
  45. package/app/Sources/Terminal.swift +259 -0
  46. package/app/Sources/TerminalQuery.swift +156 -0
  47. package/app/Sources/TerminalSynthesizer.swift +200 -0
  48. package/app/Sources/Theme.swift +124 -0
  49. package/app/Sources/TilePickerView.swift +209 -0
  50. package/app/Sources/TmuxModel.swift +53 -0
  51. package/app/Sources/TmuxQuery.swift +81 -0
  52. package/app/Sources/WindowTiler.swift +1752 -0
  53. package/app/Sources/WorkspaceManager.swift +434 -0
  54. package/bin/daemon-client.js +187 -0
  55. package/bin/lattices-app.js +205 -0
  56. package/bin/lattices.js +1295 -0
  57. package/docs/api.md +707 -0
  58. package/docs/app.md +250 -0
  59. package/docs/concepts.md +225 -0
  60. package/docs/config.md +234 -0
  61. package/docs/layers.md +317 -0
  62. package/docs/overview.md +74 -0
  63. package/docs/quickstart.md +82 -0
  64. package/package.json +38 -0
package/docs/api.md ADDED
@@ -0,0 +1,707 @@
1
+ ---
2
+ title: Daemon API
3
+ description: WebSocket API reference for programmatic control of lattices
4
+ order: 5
5
+ ---
6
+
7
+ # Daemon API
8
+
9
+ The lattices menu bar app runs a WebSocket daemon on `ws://127.0.0.1:9399`.
10
+ It exposes 20 RPC methods and 3 real-time events — everything the app
11
+ can do, agents and scripts can do too.
12
+
13
+ ## Who this is for
14
+
15
+ - **AI coding agents** that need to discover projects, launch sessions,
16
+ tile windows, and switch contexts without human interaction
17
+ - **Scripts and automation** — CI, dotfile bootstraps, workspace setup
18
+ - **Custom tools** — build your own launcher, dashboard, or orchestrator
19
+
20
+ > New to lattices? Start with the [Overview](/docs/overview) and
21
+ > [Quickstart](/docs/quickstart). For the `.lattices.json` config format
22
+ > and CLI commands, see [Configuration](/docs/config). For architecture
23
+ > details, see [Concepts](/docs/concepts).
24
+
25
+ ## Quick start
26
+
27
+ 1. Launch the daemon (it starts with the menu bar app):
28
+
29
+ ```bash
30
+ lattices app
31
+ ```
32
+
33
+ 2. Check that it's running:
34
+
35
+ ```bash
36
+ lattices daemon status
37
+ ```
38
+
39
+ 3. Call a method from Node.js:
40
+
41
+ ```js
42
+ import { daemonCall } from 'lattices/daemon-client'
43
+
44
+ const windows = await daemonCall('windows.list')
45
+ console.log(windows) // [{ wid, app, title, frame, ... }, ...]
46
+ ```
47
+
48
+ Or from any language — it's a standard WebSocket:
49
+
50
+ ```bash
51
+ # Plain websocat example
52
+ echo '{"id":"1","method":"daemon.status"}' | websocat ws://127.0.0.1:9399
53
+ ```
54
+
55
+ ## Wire protocol
56
+
57
+ lattices uses a JSON-RPC-style protocol over WebSocket on port **9399**.
58
+
59
+ ### Request
60
+
61
+ ```json
62
+ {
63
+ "id": "unique-string",
64
+ "method": "windows.list",
65
+ "params": { "wid": 1234 }
66
+ }
67
+ ```
68
+
69
+ | Field | Type | Required | Description |
70
+ |----------|---------|----------|--------------------------------------|
71
+ | `id` | string | yes | Caller-chosen ID, echoed in response |
72
+ | `method` | string | yes | Method name (see below) |
73
+ | `params` | object | no | Method-specific parameters |
74
+
75
+ ### Response
76
+
77
+ ```json
78
+ {
79
+ "id": "unique-string",
80
+ "result": [ ... ],
81
+ "error": null
82
+ }
83
+ ```
84
+
85
+ | Field | Type | Description |
86
+ |----------|----------------|----------------------------------------------|
87
+ | `id` | string | Echoed from request |
88
+ | `result` | any \| null | Method return value (null on error) |
89
+ | `error` | string \| null | Error message (null on success) |
90
+
91
+ ### Event (server-pushed)
92
+
93
+ ```json
94
+ {
95
+ "event": "windows.changed",
96
+ "data": { ... }
97
+ }
98
+ ```
99
+
100
+ Events have no `id` — they are broadcast to all connected clients
101
+ whenever state changes.
102
+
103
+ ### Errors
104
+
105
+ Three error types:
106
+
107
+ | Error | Meaning |
108
+ |-----------------|--------------------------------------|
109
+ | Unknown method | The `method` string is not recognized |
110
+ | Missing parameter | A required param was not provided |
111
+ | Not found | The referenced resource doesn't exist |
112
+
113
+ ## Node.js client
114
+
115
+ lattices ships a zero-dependency WebSocket client that works with
116
+ Node.js 18+. It handles connection, framing, and request/response
117
+ matching internally.
118
+
119
+ ### `daemonCall(method, params?, timeoutMs?)`
120
+
121
+ Send an RPC call and await the response.
122
+
123
+ ```js
124
+ import { daemonCall } from 'lattices/daemon-client'
125
+
126
+ // Read-only
127
+ const status = await daemonCall('daemon.status')
128
+ const windows = await daemonCall('windows.list')
129
+ const win = await daemonCall('windows.get', { wid: 1234 })
130
+
131
+ // Mutations
132
+ await daemonCall('session.launch', { path: '/Users/you/dev/myapp' })
133
+ await daemonCall('window.tile', { session: 'myapp-a1b2c3', position: 'left' })
134
+
135
+ // Custom timeout (default: 5000ms)
136
+ await daemonCall('projects.scan', null, 10000)
137
+ ```
138
+
139
+ **Returns** the `result` field from the response.
140
+ **Throws** if the daemon returns an error, the connection fails, or the timeout is reached.
141
+
142
+ ### `isDaemonRunning()`
143
+
144
+ Check if the daemon is reachable.
145
+
146
+ ```js
147
+ import { isDaemonRunning } from 'lattices/daemon-client'
148
+
149
+ if (await isDaemonRunning()) {
150
+ console.log('daemon is up')
151
+ }
152
+ ```
153
+
154
+ Returns `true` if `daemon.status` responds within 1 second.
155
+
156
+ ---
157
+
158
+ ## Read methods
159
+
160
+ ### `daemon.status`
161
+
162
+ Health check and basic stats.
163
+
164
+ **Params**: none
165
+
166
+ **Returns**:
167
+
168
+ ```json
169
+ {
170
+ "uptime": 3600.5,
171
+ "clientCount": 2,
172
+ "version": "1.0.0",
173
+ "windowCount": 12,
174
+ "tmuxSessionCount": 3
175
+ }
176
+ ```
177
+
178
+ ---
179
+
180
+ ### `windows.list`
181
+
182
+ List all visible windows tracked by the desktop model.
183
+
184
+ **Params**: none
185
+
186
+ **Returns**: array of window objects:
187
+
188
+ ```json
189
+ [
190
+ {
191
+ "wid": 1234,
192
+ "app": "Terminal",
193
+ "pid": 5678,
194
+ "title": "[lattices:myapp-a1b2c3] zsh",
195
+ "frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
196
+ "spaceIds": [1],
197
+ "isOnScreen": true,
198
+ "latticesSession": "myapp-a1b2c3"
199
+ }
200
+ ]
201
+ ```
202
+
203
+ The `latticesSession` field is present only on windows that belong to
204
+ a lattices session (matched via the `[lattices:name]` title tag).
205
+
206
+ ---
207
+
208
+ ### `windows.get`
209
+
210
+ Get a single window by its CGWindowID.
211
+
212
+ **Params**:
213
+
214
+ | Field | Type | Required | Description |
215
+ |-------|--------|----------|-------------------|
216
+ | `wid` | number | yes | CGWindowID |
217
+
218
+ **Returns**: a single window object (same shape as `windows.list` items).
219
+
220
+ **Errors**: `Not found` if the window ID doesn't exist.
221
+
222
+ ---
223
+
224
+ ### `tmux.sessions`
225
+
226
+ List tmux sessions that belong to lattices.
227
+
228
+ **Params**: none
229
+
230
+ **Returns**: array of session objects:
231
+
232
+ ```json
233
+ [
234
+ {
235
+ "name": "myapp-a1b2c3",
236
+ "windowCount": 1,
237
+ "attached": true,
238
+ "panes": [
239
+ {
240
+ "id": "%0",
241
+ "windowIndex": 0,
242
+ "windowName": "main",
243
+ "title": "claude",
244
+ "currentCommand": "claude",
245
+ "pid": 9876,
246
+ "isActive": true
247
+ }
248
+ ]
249
+ }
250
+ ]
251
+ ```
252
+
253
+ ---
254
+
255
+ ### `tmux.inventory`
256
+
257
+ List all tmux sessions including orphans (sessions not tracked by lattices).
258
+
259
+ **Params**: none
260
+
261
+ **Returns**:
262
+
263
+ ```json
264
+ {
265
+ "all": [ ... ],
266
+ "orphans": [ ... ]
267
+ }
268
+ ```
269
+
270
+ Both arrays contain session objects (same shape as `tmux.sessions`).
271
+
272
+ ---
273
+
274
+ ### `projects.list`
275
+
276
+ List all discovered projects.
277
+
278
+ **Params**: none
279
+
280
+ **Returns**: array of project objects:
281
+
282
+ ```json
283
+ [
284
+ {
285
+ "path": "/Users/you/dev/myapp",
286
+ "name": "myapp",
287
+ "sessionName": "myapp-a1b2c3",
288
+ "isRunning": true,
289
+ "hasConfig": true,
290
+ "paneCount": 2,
291
+ "paneNames": ["claude", "server"],
292
+ "devCommand": "pnpm dev",
293
+ "packageManager": "pnpm"
294
+ }
295
+ ]
296
+ ```
297
+
298
+ `devCommand` and `packageManager` are present only when detected.
299
+
300
+ ---
301
+
302
+ ### `spaces.list`
303
+
304
+ List macOS display spaces (virtual desktops).
305
+
306
+ **Params**: none
307
+
308
+ **Returns**: array of display objects:
309
+
310
+ ```json
311
+ [
312
+ {
313
+ "displayIndex": 0,
314
+ "displayId": "main",
315
+ "currentSpaceId": 1,
316
+ "spaces": [
317
+ { "id": 1, "index": 0, "display": 0, "isCurrent": true },
318
+ { "id": 2, "index": 1, "display": 0, "isCurrent": false }
319
+ ]
320
+ }
321
+ ]
322
+ ```
323
+
324
+ ---
325
+
326
+ ### `layers.list`
327
+
328
+ List configured workspace layers and the active index.
329
+
330
+ **Params**: none
331
+
332
+ **Returns**:
333
+
334
+ ```json
335
+ {
336
+ "layers": [
337
+ { "id": "web", "label": "Web", "index": 0, "projectCount": 2 },
338
+ { "id": "mobile", "label": "Mobile", "index": 1, "projectCount": 2 }
339
+ ],
340
+ "active": 0
341
+ }
342
+ ```
343
+
344
+ Returns empty `layers` array if no workspace config is loaded.
345
+
346
+ ---
347
+
348
+ ## Write methods
349
+
350
+ ### `session.launch`
351
+
352
+ Launch a new tmux session for a project.
353
+
354
+ **Params**:
355
+
356
+ | Field | Type | Required | Description |
357
+ |--------|--------|----------|----------------------------------|
358
+ | `path` | string | yes | Absolute path to project directory |
359
+
360
+ **Returns**: `{ "ok": true }`
361
+
362
+ **Errors**: `Not found` if the path isn't in the scanned project list.
363
+ Run `projects.scan` first if needed.
364
+
365
+ **Notes**: If a session already exists for this project, it will be
366
+ reattached. The project must be in the scanned project list — call
367
+ `projects.list` to check, or `projects.scan` to refresh.
368
+
369
+ ---
370
+
371
+ ### `session.kill`
372
+
373
+ Kill a tmux session by name.
374
+
375
+ **Params**:
376
+
377
+ | Field | Type | Required | Description |
378
+ |--------|--------|----------|---------------------|
379
+ | `name` | string | yes | Session name |
380
+
381
+ **Returns**: `{ "ok": true }`
382
+
383
+ ---
384
+
385
+ ### `session.detach`
386
+
387
+ Detach all clients from a session (keeps it running).
388
+
389
+ **Params**:
390
+
391
+ | Field | Type | Required | Description |
392
+ |--------|--------|----------|---------------------|
393
+ | `name` | string | yes | Session name |
394
+
395
+ **Returns**: `{ "ok": true }`
396
+
397
+ ---
398
+
399
+ ### `session.sync`
400
+
401
+ Reconcile a running session to match its declared `.lattices.json` config.
402
+ Recreates missing panes, re-applies layout, restores labels, re-runs
403
+ commands in idle panes.
404
+
405
+ **Params**:
406
+
407
+ | Field | Type | Required | Description |
408
+ |--------|--------|----------|----------------------------------|
409
+ | `path` | string | yes | Absolute path to project directory |
410
+
411
+ **Returns**: `{ "ok": true }`
412
+
413
+ **Errors**: `Not found` if the path isn't in the project list.
414
+
415
+ ---
416
+
417
+ ### `session.restart`
418
+
419
+ Restart a specific pane's process within a session.
420
+
421
+ **Params**:
422
+
423
+ | Field | Type | Required | Description |
424
+ |--------|--------|----------|----------------------------------|
425
+ | `path` | string | yes | Absolute path to project directory |
426
+ | `pane` | string | no | Pane name to restart (defaults to first pane) |
427
+
428
+ **Returns**: `{ "ok": true }`
429
+
430
+ **Errors**: `Not found` if the path isn't in the project list.
431
+
432
+ ---
433
+
434
+ ### `window.tile`
435
+
436
+ Tile a session's terminal window to a screen position.
437
+
438
+ **Params**:
439
+
440
+ | Field | Type | Required | Description |
441
+ |------------|--------|----------|-------------------------------------|
442
+ | `session` | string | yes | Session name |
443
+ | `position` | string | yes | Tile position (see below) |
444
+
445
+ **Positions**: `left`, `right`, `top`, `bottom`, `top-left`, `top-right`,
446
+ `bottom-left`, `bottom-right`, `maximize`, `center`
447
+
448
+ **Returns**: `{ "ok": true }`
449
+
450
+ ---
451
+
452
+ ### `window.focus`
453
+
454
+ Focus a window — bring it to front and switch Spaces if needed.
455
+
456
+ **Params** (one of):
457
+
458
+ | Field | Type | Required | Description |
459
+ |-----------|--------|----------|---------------------------------|
460
+ | `wid` | number | no | CGWindowID (any window) |
461
+ | `session` | string | no | Session name (lattices windows) |
462
+
463
+ Provide either `wid` or `session`. If `wid` is given, it takes priority.
464
+
465
+ **Returns**: `{ "ok": true }` (with `wid` and `app` if focused by wid)
466
+
467
+ ---
468
+
469
+ ### `window.move`
470
+
471
+ Move a session's window to a different macOS Space.
472
+
473
+ **Params**:
474
+
475
+ | Field | Type | Required | Description |
476
+ |-----------|--------|----------|----------------------------|
477
+ | `session` | string | yes | Session name |
478
+ | `spaceId` | number | yes | Target Space ID (from `spaces.list`) |
479
+
480
+ **Returns**: `{ "ok": true }`
481
+
482
+ ---
483
+
484
+ ### `layer.switch`
485
+
486
+ Switch the active workspace layer.
487
+
488
+ **Params**:
489
+
490
+ | Field | Type | Required | Description |
491
+ |---------|--------|----------|--------------------------------|
492
+ | `index` | number | yes | Layer index (0-based) |
493
+
494
+ **Returns**: `{ "ok": true }`
495
+
496
+ **Notes**: This focuses and tiles all windows in the target layer,
497
+ launches any projects that aren't running yet, and posts a
498
+ `layer.switched` event.
499
+
500
+ ---
501
+
502
+ ### `group.launch`
503
+
504
+ Launch a tab group session.
505
+
506
+ **Params**:
507
+
508
+ | Field | Type | Required | Description |
509
+ |-------|--------|----------|------------------|
510
+ | `id` | string | yes | Group ID |
511
+
512
+ **Returns**: `{ "ok": true }`
513
+
514
+ **Errors**: `Not found` if the group ID doesn't match any configured group.
515
+
516
+ ---
517
+
518
+ ### `group.kill`
519
+
520
+ Kill a tab group session.
521
+
522
+ **Params**:
523
+
524
+ | Field | Type | Required | Description |
525
+ |-------|--------|----------|------------------|
526
+ | `id` | string | yes | Group ID |
527
+
528
+ **Returns**: `{ "ok": true }`
529
+
530
+ **Errors**: `Not found` if the group ID doesn't match any configured group.
531
+
532
+ ---
533
+
534
+ ### `projects.scan`
535
+
536
+ Trigger a re-scan of the project directory. Useful after cloning a new
537
+ repo or adding a `.lattices.json` config.
538
+
539
+ **Params**: none
540
+
541
+ **Returns**: `{ "ok": true }`
542
+
543
+ ---
544
+
545
+ ## Events
546
+
547
+ Events are pushed to all connected WebSocket clients when state changes.
548
+ They have no `id` field — listen for messages with an `event` field.
549
+
550
+ ### `windows.changed`
551
+
552
+ Fired when the desktop window list changes (windows opened, closed,
553
+ moved, or resized).
554
+
555
+ ```json
556
+ {
557
+ "event": "windows.changed",
558
+ "data": {
559
+ "windows": [ ... ],
560
+ "added": [1234],
561
+ "removed": [5678]
562
+ }
563
+ }
564
+ ```
565
+
566
+ | Field | Type | Description |
567
+ |-----------|----------|------------------------------------|
568
+ | `windows` | array | Full current window list |
569
+ | `added` | number[] | Window IDs that appeared |
570
+ | `removed` | number[] | Window IDs that disappeared |
571
+
572
+ ---
573
+
574
+ ### `tmux.changed`
575
+
576
+ Fired when tmux sessions change (created, killed, panes added/removed).
577
+
578
+ ```json
579
+ {
580
+ "event": "tmux.changed",
581
+ "data": {
582
+ "sessions": [ ... ]
583
+ }
584
+ }
585
+ ```
586
+
587
+ | Field | Type | Description |
588
+ |------------|-------|--------------------------|
589
+ | `sessions` | array | Full current session list |
590
+
591
+ ---
592
+
593
+ ### `layer.switched`
594
+
595
+ Fired when the active workspace layer changes.
596
+
597
+ ```json
598
+ {
599
+ "event": "layer.switched",
600
+ "data": {
601
+ "index": 1
602
+ }
603
+ }
604
+ ```
605
+
606
+ | Field | Type | Description |
607
+ |---------|--------|------------------------------|
608
+ | `index` | number | Index of the now-active layer |
609
+
610
+ ---
611
+
612
+ ## Agent integration patterns
613
+
614
+ ### CLAUDE.md snippet
615
+
616
+ Add this to your project's `CLAUDE.md` so any AI agent working in the
617
+ project knows how to control the workspace:
618
+
619
+ ```markdown
620
+ ## Workspace Control
621
+
622
+ This project uses lattices for workspace management. The daemon API
623
+ is available at ws://127.0.0.1:9399.
624
+
625
+ ### Available commands
626
+ - List windows: `daemonCall('windows.list')`
627
+ - List sessions: `daemonCall('tmux.sessions')`
628
+ - Launch a project: `daemonCall('session.launch', { path: '/absolute/path' })`
629
+ - Tile a window: `daemonCall('window.tile', { session: 'name', position: 'left' })`
630
+ - Switch layer: `daemonCall('layer.switch', { index: 0 })`
631
+
632
+ ### Import
633
+ \```js
634
+ import { daemonCall } from 'lattices/daemon-client'
635
+ \```
636
+ ```
637
+
638
+ ### Multi-agent orchestration
639
+
640
+ An orchestrator agent can set up the full workspace for sub-agents:
641
+
642
+ ```js
643
+ import { daemonCall } from 'lattices/daemon-client'
644
+
645
+ // Discover what's available
646
+ const projects = await daemonCall('projects.list')
647
+
648
+ // Launch the projects we need
649
+ await daemonCall('session.launch', { path: '/Users/you/dev/frontend' })
650
+ await daemonCall('session.launch', { path: '/Users/you/dev/api' })
651
+
652
+ // Tile them side by side
653
+ const sessions = await daemonCall('tmux.sessions')
654
+ const fe = sessions.find(s => s.name.startsWith('frontend'))
655
+ const api = sessions.find(s => s.name.startsWith('api'))
656
+
657
+ await daemonCall('window.tile', { session: fe.name, position: 'left' })
658
+ await daemonCall('window.tile', { session: api.name, position: 'right' })
659
+ ```
660
+
661
+ ### Reactive event pattern
662
+
663
+ Subscribe to events for real-time workspace awareness:
664
+
665
+ ```js
666
+ import WebSocket from 'ws' // or use the built-in client
667
+
668
+ const ws = new WebSocket('ws://127.0.0.1:9399')
669
+
670
+ ws.on('message', (raw) => {
671
+ const msg = JSON.parse(raw)
672
+
673
+ if (msg.event === 'tmux.changed') {
674
+ console.log('Sessions changed:', msg.data.sessions.length, 'active')
675
+ }
676
+
677
+ if (msg.event === 'windows.changed') {
678
+ const latticesWindows = msg.data.windows.filter(w => w.latticesSession)
679
+ console.log('Lattices windows:', latticesWindows.length)
680
+ }
681
+
682
+ if (msg.event === 'layer.switched') {
683
+ console.log('Switched to layer', msg.data.index)
684
+ }
685
+ })
686
+
687
+ // You can also send RPC calls on the same connection
688
+ ws.on('open', () => {
689
+ ws.send(JSON.stringify({ id: '1', method: 'daemon.status' }))
690
+ })
691
+ ```
692
+
693
+ ### Health check before use
694
+
695
+ Always verify the daemon is running before making calls:
696
+
697
+ ```js
698
+ import { isDaemonRunning, daemonCall } from 'lattices/daemon-client'
699
+
700
+ if (!(await isDaemonRunning())) {
701
+ console.error('lattices daemon is not running — start it with: lattices app')
702
+ process.exit(1)
703
+ }
704
+
705
+ const status = await daemonCall('daemon.status')
706
+ console.log(`Daemon up for ${Math.round(status.uptime)}s, tracking ${status.windowCount} windows`)
707
+ ```