@lattices/cli 0.3.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 (74) hide show
  1. package/README.md +155 -0
  2. package/app/Lattices.app/Contents/Info.plist +24 -0
  3. package/app/Package.swift +13 -0
  4. package/app/Sources/AccessibilityTextExtractor.swift +111 -0
  5. package/app/Sources/ActionRow.swift +61 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +242 -0
  8. package/app/Sources/AppShellView.swift +62 -0
  9. package/app/Sources/AppTypeClassifier.swift +70 -0
  10. package/app/Sources/AppWindowShell.swift +63 -0
  11. package/app/Sources/CheatSheetHUD.swift +332 -0
  12. package/app/Sources/CommandModeState.swift +1362 -0
  13. package/app/Sources/CommandModeView.swift +1405 -0
  14. package/app/Sources/CommandModeWindow.swift +192 -0
  15. package/app/Sources/CommandPaletteView.swift +307 -0
  16. package/app/Sources/CommandPaletteWindow.swift +134 -0
  17. package/app/Sources/DaemonProtocol.swift +101 -0
  18. package/app/Sources/DaemonServer.swift +414 -0
  19. package/app/Sources/DesktopModel.swift +149 -0
  20. package/app/Sources/DesktopModelTypes.swift +71 -0
  21. package/app/Sources/DiagnosticLog.swift +271 -0
  22. package/app/Sources/EventBus.swift +30 -0
  23. package/app/Sources/HotkeyManager.swift +254 -0
  24. package/app/Sources/HotkeyStore.swift +338 -0
  25. package/app/Sources/InventoryManager.swift +35 -0
  26. package/app/Sources/InventoryPath.swift +43 -0
  27. package/app/Sources/KeyRecorderView.swift +210 -0
  28. package/app/Sources/LatticesApi.swift +1234 -0
  29. package/app/Sources/LayerBezel.swift +203 -0
  30. package/app/Sources/MainView.swift +479 -0
  31. package/app/Sources/MainWindow.swift +83 -0
  32. package/app/Sources/OcrModel.swift +430 -0
  33. package/app/Sources/OcrStore.swift +329 -0
  34. package/app/Sources/OmniSearchState.swift +283 -0
  35. package/app/Sources/OmniSearchView.swift +288 -0
  36. package/app/Sources/OmniSearchWindow.swift +105 -0
  37. package/app/Sources/OrphanRow.swift +129 -0
  38. package/app/Sources/PaletteCommand.swift +419 -0
  39. package/app/Sources/PermissionChecker.swift +125 -0
  40. package/app/Sources/Preferences.swift +99 -0
  41. package/app/Sources/ProcessModel.swift +199 -0
  42. package/app/Sources/ProcessQuery.swift +151 -0
  43. package/app/Sources/Project.swift +28 -0
  44. package/app/Sources/ProjectRow.swift +368 -0
  45. package/app/Sources/ProjectScanner.swift +128 -0
  46. package/app/Sources/ScreenMapState.swift +2387 -0
  47. package/app/Sources/ScreenMapView.swift +2820 -0
  48. package/app/Sources/ScreenMapWindowController.swift +89 -0
  49. package/app/Sources/SessionManager.swift +72 -0
  50. package/app/Sources/SettingsView.swift +1064 -0
  51. package/app/Sources/SettingsWindow.swift +20 -0
  52. package/app/Sources/TabGroupRow.swift +178 -0
  53. package/app/Sources/Terminal.swift +259 -0
  54. package/app/Sources/TerminalQuery.swift +156 -0
  55. package/app/Sources/TerminalSynthesizer.swift +200 -0
  56. package/app/Sources/Theme.swift +163 -0
  57. package/app/Sources/TilePickerView.swift +209 -0
  58. package/app/Sources/TmuxModel.swift +53 -0
  59. package/app/Sources/TmuxQuery.swift +81 -0
  60. package/app/Sources/WindowTiler.swift +1778 -0
  61. package/app/Sources/WorkspaceManager.swift +575 -0
  62. package/bin/client.js +4 -0
  63. package/bin/daemon-client.js +187 -0
  64. package/bin/lattices-app.js +221 -0
  65. package/bin/lattices.js +1551 -0
  66. package/docs/api.md +924 -0
  67. package/docs/app.md +297 -0
  68. package/docs/concepts.md +135 -0
  69. package/docs/config.md +245 -0
  70. package/docs/layers.md +410 -0
  71. package/docs/ocr.md +185 -0
  72. package/docs/overview.md +94 -0
  73. package/docs/quickstart.md +75 -0
  74. package/package.json +42 -0
package/docs/api.md ADDED
@@ -0,0 +1,924 @@
1
+ ---
2
+ title: Agent API
3
+ description: WebSocket API reference for programmatic control of lattices
4
+ order: 5
5
+ ---
6
+
7
+ The lattices menu bar app runs a WebSocket server on `ws://127.0.0.1:9399`.
8
+ 35+ RPC methods and 5 real-time events.
9
+
10
+ ## Quick start
11
+
12
+ 1. Launch the server (it starts with the menu bar app):
13
+
14
+ ```bash
15
+ lattices app
16
+ ```
17
+
18
+ 2. Check that it's running:
19
+
20
+ ```bash
21
+ lattices daemon status
22
+ ```
23
+
24
+ 3. Call a method from Node.js:
25
+
26
+ ```js
27
+ import { daemonCall } from '@lattices/cli'
28
+
29
+ const windows = await daemonCall('windows.list')
30
+ console.log(windows) // [{ wid, app, title, frame, ... }, ...]
31
+ ```
32
+
33
+ Or from any language — it's a standard WebSocket:
34
+
35
+ ```bash
36
+ # Plain websocat example
37
+ echo '{"id":"1","method":"daemon.status"}' | websocat ws://127.0.0.1:9399
38
+ ```
39
+
40
+ ## Wire protocol
41
+
42
+ lattices uses a JSON-RPC-style protocol over WebSocket on port **9399**.
43
+
44
+ ### Request
45
+
46
+ ```json
47
+ {
48
+ "id": "unique-string",
49
+ "method": "windows.list",
50
+ "params": { "wid": 1234 }
51
+ }
52
+ ```
53
+
54
+ | Field | Type | Required | Description |
55
+ |----------|---------|----------|--------------------------------------|
56
+ | `id` | string | yes | Caller-chosen ID, echoed in response |
57
+ | `method` | string | yes | Method name (see below) |
58
+ | `params` | object | no | Method-specific parameters |
59
+
60
+ ### Response
61
+
62
+ ```json
63
+ {
64
+ "id": "unique-string",
65
+ "result": [ ... ],
66
+ "error": null
67
+ }
68
+ ```
69
+
70
+ | Field | Type | Description |
71
+ |----------|----------------|----------------------------------------------|
72
+ | `id` | string | Echoed from request |
73
+ | `result` | any \| null | Method return value (null on error) |
74
+ | `error` | string \| null | Error message (null on success) |
75
+
76
+ ### Errors
77
+
78
+ | Error | Meaning |
79
+ |-----------------|--------------------------------------|
80
+ | Unknown method | The `method` string is not recognized |
81
+ | Missing parameter | A required param was not provided |
82
+ | Not found | The referenced resource doesn't exist |
83
+
84
+ ### Connection lifecycle
85
+
86
+ - The server starts when the menu bar app launches and stops when it quits.
87
+ - Connections are plain WebSocket. No handshake, no auth, no heartbeat.
88
+ - The Node.js `daemonCall()` client opens a fresh connection per call and
89
+ closes it when the response arrives. For event subscriptions, hold the
90
+ connection open (see [Reactive event pattern](#agent-integration)).
91
+ - If the server restarts (e.g. after `lattices app restart`), existing
92
+ connections are dropped. Clients should reconnect and treat it as
93
+ stateless. There is no session resumption.
94
+
95
+ ## Node.js client
96
+
97
+ lattices ships a zero-dependency WebSocket client that works with
98
+ Node.js 18+. It handles connection, framing, and request/response
99
+ matching internally.
100
+
101
+ ### `daemonCall(method, params?, timeoutMs?)`
102
+
103
+ Send an RPC call and await the response.
104
+
105
+ ```js
106
+ import { daemonCall } from '@lattices/cli'
107
+
108
+ // Read-only
109
+ const status = await daemonCall('daemon.status')
110
+ const windows = await daemonCall('windows.list')
111
+ const win = await daemonCall('windows.get', { wid: 1234 })
112
+
113
+ // Mutations
114
+ await daemonCall('session.launch', { path: '/Users/you/dev/myapp' })
115
+ await daemonCall('window.tile', { session: 'myapp-a1b2c3', position: 'left' })
116
+
117
+ // Custom timeout (default: 3000ms)
118
+ await daemonCall('projects.scan', null, 10000)
119
+ ```
120
+
121
+ **Returns** the `result` field from the response.
122
+ **Throws** if the server returns an error, the connection fails, or the timeout is reached.
123
+
124
+ ### `isDaemonRunning()`
125
+
126
+ Check if the server is reachable.
127
+
128
+ ```js
129
+ import { isDaemonRunning } from '@lattices/cli'
130
+
131
+ if (await isDaemonRunning()) {
132
+ console.log('daemon is up')
133
+ }
134
+ ```
135
+
136
+ Returns `true` if `daemon.status` responds within 1 second.
137
+
138
+ ### Error handling
139
+
140
+ `daemonCall` throws on errors — always wrap calls in try/catch:
141
+
142
+ ```js
143
+ import { daemonCall } from '@lattices/cli'
144
+
145
+ try {
146
+ await daemonCall('session.launch', { path: '/nonexistent' })
147
+ } catch (err) {
148
+ // err.message is one of:
149
+ // "Not found" — resource doesn't exist
150
+ // "Missing parameter: ..." — required param missing
151
+ // "Unknown method: ..." — bad method name
152
+ // "Daemon request timed out" — no response within timeout
153
+ // ECONNREFUSED — daemon not running
154
+ console.error('Daemon error:', err.message)
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## System
161
+
162
+ | Method | Type | Description |
163
+ |--------|------|-------------|
164
+ | `daemon.status` | read | Health check and stats |
165
+ | `api.schema` | read | Full API schema for self-discovery |
166
+ | `diagnostics.list` | read | Recent diagnostic entries |
167
+
168
+ #### `daemon.status`
169
+
170
+ Health check and basic stats.
171
+
172
+ **Params**: none
173
+
174
+ **Returns**:
175
+
176
+ ```json
177
+ {
178
+ "uptime": 3600.5,
179
+ "clientCount": 2,
180
+ "version": "1.0.0",
181
+ "windowCount": 12,
182
+ "tmuxSessionCount": 3
183
+ }
184
+ ```
185
+
186
+ #### `api.schema`
187
+
188
+ Return the full API schema including version, models, and method definitions.
189
+ Useful for agent self-discovery.
190
+
191
+ **Params**: none
192
+
193
+ #### `diagnostics.list`
194
+
195
+ Return recent diagnostic log entries from the daemon.
196
+
197
+ **Params**:
198
+
199
+ | Field | Type | Required | Description |
200
+ |---------|--------|----------|--------------------------------|
201
+ | `limit` | number | no | Max entries to return (default 50) |
202
+
203
+ ---
204
+
205
+ ## Windows & Spaces
206
+
207
+ | Method | Type | Description |
208
+ |--------|------|-------------|
209
+ | `windows.list` | read | All visible windows |
210
+ | `windows.get` | read | Single window by ID |
211
+ | `windows.search` | read | Search windows by query |
212
+ | `spaces.list` | read | macOS display spaces |
213
+ | `window.tile` | write | Tile a window to a position |
214
+ | `window.focus` | write | Focus a window / switch Spaces |
215
+ | `window.move` | write | Move a window to another Space |
216
+ | `window.assignLayer` | write | Tag a window to a layer |
217
+ | `window.removeLayer` | write | Remove a window's layer tag |
218
+ | `window.layerMap` | read | All window→layer assignments |
219
+ | `layout.distribute` | write | Distribute windows evenly |
220
+
221
+ #### `windows.list`
222
+
223
+ List all visible windows tracked by the desktop model.
224
+
225
+ **Params**: none
226
+
227
+ **Returns**: array of window objects:
228
+
229
+ ```json
230
+ [
231
+ {
232
+ "wid": 1234,
233
+ "app": "Terminal",
234
+ "pid": 5678,
235
+ "title": "[lattices:myapp-a1b2c3] zsh",
236
+ "frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
237
+ "spaceIds": [1],
238
+ "isOnScreen": true,
239
+ "latticesSession": "myapp-a1b2c3",
240
+ "layerTag": "web"
241
+ }
242
+ ]
243
+ ```
244
+
245
+ The `latticesSession` field is present only on windows that belong to
246
+ a lattices session (matched via the `[lattices:name]` title tag).
247
+
248
+ The `layerTag` field is present when a window has been manually assigned
249
+ to a layer via `window.assignLayer`.
250
+
251
+ #### `windows.get`
252
+
253
+ Get a single window by its CGWindowID.
254
+
255
+ **Params**:
256
+
257
+ | Field | Type | Required | Description |
258
+ |-------|--------|----------|-------------------|
259
+ | `wid` | number | yes | CGWindowID |
260
+
261
+ **Returns**: a single window object (same shape as `windows.list` items).
262
+ **Errors**: `Not found` if the window ID doesn't exist.
263
+
264
+ #### `windows.search`
265
+
266
+ Search windows by text query, including OCR content.
267
+
268
+ **Params**:
269
+
270
+ | Field | Type | Required | Description |
271
+ |---------|--------|----------|--------------------------------|
272
+ | `query` | string | yes | Search query (matches title, app, OCR text) |
273
+ | `ocr` | boolean| no | Include OCR text in search (default true) |
274
+ | `limit` | number | no | Max results (default 20) |
275
+
276
+ **Returns**: array of window objects matching the query.
277
+
278
+ #### `spaces.list`
279
+
280
+ List macOS display spaces (virtual desktops).
281
+
282
+ **Params**: none
283
+
284
+ **Returns**: array of display objects:
285
+
286
+ ```json
287
+ [
288
+ {
289
+ "displayIndex": 0,
290
+ "displayId": "main",
291
+ "currentSpaceId": 1,
292
+ "spaces": [
293
+ { "id": 1, "index": 0, "display": 0, "isCurrent": true },
294
+ { "id": 2, "index": 1, "display": 0, "isCurrent": false }
295
+ ]
296
+ }
297
+ ]
298
+ ```
299
+
300
+ #### `window.tile`
301
+
302
+ Tile a session's terminal window to a screen position.
303
+
304
+ **Params**:
305
+
306
+ | Field | Type | Required | Description |
307
+ |------------|--------|----------|-------------------------------------|
308
+ | `session` | string | yes | Session name |
309
+ | `position` | string | yes | Tile position (see below) |
310
+
311
+ **Positions**: `left`, `right`, `top`, `bottom`, `top-left`, `top-right`,
312
+ `bottom-left`, `bottom-right`, `left-third`, `center-third`, `right-third`,
313
+ `maximize`, `center`
314
+
315
+ #### `window.focus`
316
+
317
+ Focus a window — bring it to front and switch Spaces if needed.
318
+
319
+ **Params** (one of):
320
+
321
+ | Field | Type | Required | Description |
322
+ |-----------|--------|----------|---------------------------------|
323
+ | `wid` | number | no | CGWindowID (any window) |
324
+ | `session` | string | no | Session name (lattices windows) |
325
+
326
+ Provide either `wid` or `session`. If `wid` is given, it takes priority.
327
+
328
+ #### `window.move`
329
+
330
+ Move a session's window to a different macOS Space.
331
+
332
+ **Params**:
333
+
334
+ | Field | Type | Required | Description |
335
+ |-----------|--------|----------|----------------------------|
336
+ | `session` | string | yes | Session name |
337
+ | `spaceId` | number | yes | Target Space ID (from `spaces.list`) |
338
+
339
+ #### `window.assignLayer`
340
+
341
+ Manually tag a window to a layer. Tagged windows are raised and tiled
342
+ when that layer activates, even if they aren't declared in `workspace.json`.
343
+
344
+ **Params**:
345
+
346
+ | Field | Type | Required | Description |
347
+ |---------|--------|----------|--------------------------------|
348
+ | `wid` | number | yes | CGWindowID |
349
+ | `layer` | string | yes | Layer ID to assign |
350
+
351
+ #### `window.removeLayer`
352
+
353
+ Remove a window's layer tag.
354
+
355
+ **Params**:
356
+
357
+ | Field | Type | Required | Description |
358
+ |-------|--------|----------|----------------|
359
+ | `wid` | number | yes | CGWindowID |
360
+
361
+ #### `window.layerMap`
362
+
363
+ Return all current window→layer assignments.
364
+
365
+ **Params**: none
366
+
367
+ **Returns**:
368
+
369
+ ```json
370
+ {
371
+ "1234": "web",
372
+ "5678": "mobile"
373
+ }
374
+ ```
375
+
376
+ Keys are CGWindowIDs (as strings), values are layer IDs.
377
+
378
+ #### `layout.distribute`
379
+
380
+ Distribute all visible lattices windows evenly across the screen.
381
+
382
+ **Params**: none
383
+
384
+ ---
385
+
386
+ ## Sessions
387
+
388
+ | Method | Type | Description |
389
+ |--------|------|-------------|
390
+ | `tmux.sessions` | read | Lattices tmux sessions |
391
+ | `tmux.inventory` | read | All sessions including orphans |
392
+ | `session.launch` | write | Launch a project session |
393
+ | `session.kill` | write | Kill a session |
394
+ | `session.detach` | write | Detach clients from a session |
395
+ | `session.sync` | write | Reconcile session to config |
396
+ | `session.restart` | write | Restart a pane's process |
397
+
398
+ All session methods require tmux to be installed.
399
+
400
+ #### `tmux.sessions`
401
+
402
+ List tmux sessions that belong to lattices.
403
+
404
+ **Params**: none
405
+
406
+ **Returns**: array of session objects:
407
+
408
+ ```json
409
+ [
410
+ {
411
+ "name": "myapp-a1b2c3",
412
+ "windowCount": 1,
413
+ "attached": true,
414
+ "panes": [
415
+ {
416
+ "id": "%0",
417
+ "windowIndex": 0,
418
+ "windowName": "main",
419
+ "title": "claude",
420
+ "currentCommand": "claude",
421
+ "pid": 9876,
422
+ "isActive": true
423
+ }
424
+ ]
425
+ }
426
+ ]
427
+ ```
428
+
429
+ #### `tmux.inventory`
430
+
431
+ List all tmux sessions including orphans (sessions not tracked by lattices).
432
+
433
+ **Params**: none
434
+
435
+ **Returns**:
436
+
437
+ ```json
438
+ {
439
+ "all": [ ... ],
440
+ "orphans": [ ... ]
441
+ }
442
+ ```
443
+
444
+ Both arrays contain session objects (same shape as `tmux.sessions`).
445
+
446
+ #### `session.launch`
447
+
448
+ Launch a new tmux session for a project. If a session already exists,
449
+ it will be reattached. The project must be in the scanned project list —
450
+ call `projects.list` to check, or `projects.scan` to refresh.
451
+
452
+ **Params**:
453
+
454
+ | Field | Type | Required | Description |
455
+ |--------|--------|----------|----------------------------------|
456
+ | `path` | string | yes | Absolute path to project directory |
457
+
458
+ **Returns**: `{ "ok": true }`
459
+ **Errors**: `Not found` if the path isn't in the scanned project list.
460
+
461
+ #### `session.kill`
462
+
463
+ Kill a tmux session by name.
464
+
465
+ **Params**:
466
+
467
+ | Field | Type | Required | Description |
468
+ |--------|--------|----------|---------------------|
469
+ | `name` | string | yes | Session name |
470
+
471
+ #### `session.detach`
472
+
473
+ Detach all clients from a session (keeps it running).
474
+
475
+ **Params**:
476
+
477
+ | Field | Type | Required | Description |
478
+ |--------|--------|----------|---------------------|
479
+ | `name` | string | yes | Session name |
480
+
481
+ #### `session.sync`
482
+
483
+ Reconcile a running session to match its declared `.lattices.json` config.
484
+ Recreates missing panes, re-applies layout, restores labels, re-runs
485
+ commands in idle panes.
486
+
487
+ **Params**:
488
+
489
+ | Field | Type | Required | Description |
490
+ |--------|--------|----------|----------------------------------|
491
+ | `path` | string | yes | Absolute path to project directory |
492
+
493
+ **Errors**: `Not found` if the path isn't in the project list.
494
+
495
+ #### `session.restart`
496
+
497
+ Restart a specific pane's process within a session.
498
+
499
+ **Params**:
500
+
501
+ | Field | Type | Required | Description |
502
+ |--------|--------|----------|----------------------------------|
503
+ | `path` | string | yes | Absolute path to project directory |
504
+ | `pane` | string | no | Pane name to restart (defaults to first pane) |
505
+
506
+ ---
507
+
508
+ ## Projects & Layers
509
+
510
+ | Method | Type | Description |
511
+ |--------|------|-------------|
512
+ | `projects.list` | read | Discovered projects |
513
+ | `projects.scan` | write | Re-scan project directory |
514
+ | `layers.list` | read | Workspace layers and active index |
515
+ | `layer.switch` | write | Switch workspace layer |
516
+ | `group.launch` | write | Launch a tab group |
517
+ | `group.kill` | write | Kill a tab group |
518
+
519
+ #### `projects.list`
520
+
521
+ List all discovered projects.
522
+
523
+ **Params**: none
524
+
525
+ **Returns**: array of project objects:
526
+
527
+ ```json
528
+ [
529
+ {
530
+ "path": "/Users/you/dev/myapp",
531
+ "name": "myapp",
532
+ "sessionName": "myapp-a1b2c3",
533
+ "isRunning": true,
534
+ "hasConfig": true,
535
+ "paneCount": 2,
536
+ "paneNames": ["claude", "server"],
537
+ "devCommand": "pnpm dev",
538
+ "packageManager": "pnpm"
539
+ }
540
+ ]
541
+ ```
542
+
543
+ `devCommand` and `packageManager` are present only when detected.
544
+
545
+ #### `projects.scan`
546
+
547
+ Trigger a re-scan of the project directory. Useful after cloning a new
548
+ repo or adding a `.lattices.json` config.
549
+
550
+ **Params**: none
551
+
552
+ #### `layers.list`
553
+
554
+ List configured workspace layers and the active index.
555
+
556
+ **Params**: none
557
+
558
+ **Returns**:
559
+
560
+ ```json
561
+ {
562
+ "layers": [
563
+ { "id": "web", "label": "Web", "index": 0, "projectCount": 2 },
564
+ { "id": "mobile", "label": "Mobile", "index": 1, "projectCount": 2 }
565
+ ],
566
+ "active": 0
567
+ }
568
+ ```
569
+
570
+ Returns empty `layers` array if no workspace config is loaded.
571
+
572
+ #### `layer.switch`
573
+
574
+ Switch the active workspace layer. Focuses and tiles all windows in the
575
+ target layer, launches any projects that aren't running yet, and posts
576
+ a `layer.switched` event.
577
+
578
+ **Params**:
579
+
580
+ | Field | Type | Required | Description |
581
+ |---------|--------|----------|--------------------------------|
582
+ | `index` | number | no | Layer index (0-based) |
583
+ | `name` | string | no | Layer ID or label |
584
+
585
+ Provide either `index` or `name`. If both are given, `name` takes priority.
586
+
587
+ #### `group.launch`
588
+
589
+ Launch a tab group session.
590
+
591
+ **Params**:
592
+
593
+ | Field | Type | Required | Description |
594
+ |-------|--------|----------|------------------|
595
+ | `id` | string | yes | Group ID |
596
+
597
+ **Errors**: `Not found` if the group ID doesn't match any configured group.
598
+
599
+ #### `group.kill`
600
+
601
+ Kill a tab group session.
602
+
603
+ **Params**:
604
+
605
+ | Field | Type | Required | Description |
606
+ |-------|--------|----------|------------------|
607
+ | `id` | string | yes | Group ID |
608
+
609
+ ---
610
+
611
+ ## Processes & Terminals
612
+
613
+ | Method | Type | Description |
614
+ |--------|------|-------------|
615
+ | `processes.list` | read | Running developer processes |
616
+ | `processes.tree` | read | Process tree from a PID |
617
+ | `terminals.list` | read | Terminal instances with processes |
618
+ | `terminals.search` | read | Search terminals by criteria |
619
+
620
+ #### `processes.list`
621
+
622
+ List running processes relevant to development (editors, servers, build tools).
623
+
624
+ **Params**:
625
+
626
+ | Field | Type | Required | Description |
627
+ |-----------|--------|----------|------------------------------------|
628
+ | `command` | string | no | Filter by command name substring |
629
+
630
+ **Returns**: array of process objects:
631
+
632
+ ```json
633
+ [
634
+ {
635
+ "pid": 1234,
636
+ "ppid": 567,
637
+ "command": "node",
638
+ "args": "server.js",
639
+ "cwd": "/Users/you/dev/myapp",
640
+ "tty": "/dev/ttys003",
641
+ "tmuxSession": "myapp-a1b2c3",
642
+ "tmuxPaneId": "%0"
643
+ }
644
+ ]
645
+ ```
646
+
647
+ #### `processes.tree`
648
+
649
+ Get the process tree rooted at a given PID.
650
+
651
+ **Params**:
652
+
653
+ | Field | Type | Required | Description |
654
+ |-------|--------|----------|---------------|
655
+ | `pid` | number | yes | Root PID |
656
+
657
+ **Returns**: array of process objects (same shape as `processes.list`).
658
+
659
+ #### `terminals.list`
660
+
661
+ List all discovered terminal instances with their processes, tabs, and tmux associations.
662
+
663
+ **Params**:
664
+
665
+ | Field | Type | Required | Description |
666
+ |-----------|---------|----------|--------------------------------------|
667
+ | `refresh` | boolean | no | Force-refresh the terminal tab cache |
668
+
669
+ **Returns**: array of terminal instance objects:
670
+
671
+ ```json
672
+ [
673
+ {
674
+ "tty": "/dev/ttys003",
675
+ "app": "Terminal",
676
+ "windowIndex": 0,
677
+ "tabIndex": 0,
678
+ "isActiveTab": true,
679
+ "tabTitle": "myapp",
680
+ "processes": [ ... ],
681
+ "shellPid": 1234,
682
+ "cwd": "/Users/you/dev/myapp",
683
+ "tmuxSession": "myapp-a1b2c3",
684
+ "tmuxPaneId": "%0",
685
+ "hasClaude": true,
686
+ "displayName": "Terminal — myapp"
687
+ }
688
+ ]
689
+ ```
690
+
691
+ #### `terminals.search`
692
+
693
+ Search terminal instances by various criteria.
694
+
695
+ **Params**:
696
+
697
+ | Field | Type | Required | Description |
698
+ |------------|---------|----------|--------------------------------------|
699
+ | `command` | string | no | Filter by command name substring |
700
+ | `cwd` | string | no | Filter by working directory substring |
701
+ | `app` | string | no | Filter by terminal app name |
702
+ | `session` | string | no | Filter by tmux session name |
703
+ | `hasClaude`| boolean | no | Filter to only Claude-running TTYs |
704
+
705
+ **Returns**: filtered array of terminal instance objects (same shape as `terminals.list`).
706
+
707
+ ---
708
+
709
+ ## OCR
710
+
711
+ | Method | Type | Description |
712
+ |--------|------|-------------|
713
+ | `ocr.snapshot` | read | Current OCR results for all visible windows |
714
+ | `ocr.search` | read | Full-text search across OCR history |
715
+ | `ocr.history` | read | OCR timeline for a specific window |
716
+ | `ocr.scan` | write | Trigger an immediate OCR scan |
717
+
718
+ See [Screen OCR](/docs/ocr) for configuration, scan schedules, and storage details.
719
+
720
+ #### `ocr.snapshot`
721
+
722
+ Get the current in-memory OCR results for all visible windows.
723
+
724
+ **Params**: none
725
+
726
+ **Returns**: array of OCR result objects:
727
+
728
+ ```json
729
+ [
730
+ {
731
+ "wid": 1234,
732
+ "app": "Terminal",
733
+ "title": "zsh",
734
+ "frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
735
+ "fullText": "~/dev/myapp $ npm run dev\nready on port 3000",
736
+ "blocks": [
737
+ {
738
+ "text": "~/dev/myapp $ npm run dev",
739
+ "confidence": 0.95,
740
+ "x": 0.02, "y": 0.05, "w": 0.6, "h": 0.04
741
+ }
742
+ ],
743
+ "timestamp": 1709568000.0
744
+ }
745
+ ]
746
+ ```
747
+
748
+ #### `ocr.search`
749
+
750
+ Full-text search across OCR history using SQLite FTS5.
751
+
752
+ **Params**:
753
+
754
+ | Field | Type | Required | Description |
755
+ |---------|---------|----------|------------------------------------------|
756
+ | `query` | string | yes | FTS5 search query |
757
+ | `app` | string | no | Filter by application name |
758
+ | `limit` | number | no | Max results (default 50) |
759
+ | `live` | boolean | no | Search live snapshot instead of history (default false) |
760
+
761
+ **FTS5 query examples**: `error`, `"build failed"`, `error OR warning`, `npm AND dev`, `react*`
762
+
763
+ #### `ocr.history`
764
+
765
+ Get the OCR timeline for a specific window, ordered by most recent first.
766
+
767
+ **Params**:
768
+
769
+ | Field | Type | Required | Description |
770
+ |---------|--------|----------|----------------------------|
771
+ | `wid` | number | yes | CGWindowID |
772
+ | `limit` | number | no | Max results (default 50) |
773
+
774
+ #### `ocr.scan`
775
+
776
+ Trigger an immediate OCR scan of all visible windows, bypassing the
777
+ periodic timer. Results available via `ocr.snapshot` once complete;
778
+ an `ocr.scanComplete` event is broadcast to all clients.
779
+
780
+ **Params**: none
781
+
782
+ ---
783
+
784
+ ## Events
785
+
786
+ Events are pushed to all connected WebSocket clients when state changes.
787
+ They have no `id` field — listen for messages with an `event` field.
788
+
789
+ | Event | Trigger |
790
+ |-------|---------|
791
+ | `windows.changed` | Desktop window list changes |
792
+ | `tmux.changed` | Sessions created, killed, or modified |
793
+ | `layer.switched` | Active workspace layer changes |
794
+ | `ocr.scanComplete` | OCR scan cycle finishes |
795
+ | `processes.changed` | Developer processes start or stop |
796
+
797
+ #### `windows.changed`
798
+
799
+ ```json
800
+ { "event": "windows.changed", "data": { "windowCount": 12, "added": [1234], "removed": [5678] } }
801
+ ```
802
+
803
+ #### `tmux.changed`
804
+
805
+ ```json
806
+ { "event": "tmux.changed", "data": { "sessionCount": 3, "sessions": ["myapp-a1b2c3"] } }
807
+ ```
808
+
809
+ #### `layer.switched`
810
+
811
+ ```json
812
+ { "event": "layer.switched", "data": { "index": 1, "name": "mobile" } }
813
+ ```
814
+
815
+ #### `ocr.scanComplete`
816
+
817
+ ```json
818
+ { "event": "ocr.scanComplete", "data": { "windowCount": 12, "totalBlocks": 342 } }
819
+ ```
820
+
821
+ #### `processes.changed`
822
+
823
+ ```json
824
+ { "event": "processes.changed", "data": { "interestingCount": 5, "pids": [1234, 5678] } }
825
+ ```
826
+
827
+ ---
828
+
829
+ ## Agent integration
830
+
831
+ ### CLAUDE.md snippet
832
+
833
+ Add this to your project's `CLAUDE.md` so any AI agent working in the
834
+ project knows how to control the workspace:
835
+
836
+ ```markdown
837
+ ## Workspace Control
838
+
839
+ This project uses lattices for workspace management. The daemon API
840
+ is available at ws://127.0.0.1:9399.
841
+
842
+ ### Available commands
843
+ - List windows: `daemonCall('windows.list')`
844
+ - List sessions: `daemonCall('tmux.sessions')`
845
+ - Launch a project: `daemonCall('session.launch', { path: '/absolute/path' })`
846
+ - Tile a window: `daemonCall('window.tile', { session: 'name', position: 'left' })`
847
+ - Switch layer: `daemonCall('layer.switch', { index: 0 })`
848
+ - Switch layer by name: `daemonCall('layer.switch', { name: 'web' })`
849
+
850
+ ### Import
851
+ \```js
852
+ import { daemonCall } from '@lattices/cli'
853
+ \```
854
+ ```
855
+
856
+ ### Multi-agent orchestration
857
+
858
+ An orchestrator agent can set up the full workspace for sub-agents:
859
+
860
+ ```js
861
+ import { daemonCall } from '@lattices/cli'
862
+
863
+ // Discover what's available
864
+ const projects = await daemonCall('projects.list')
865
+
866
+ // Launch the projects we need
867
+ await daemonCall('session.launch', { path: '/Users/you/dev/frontend' })
868
+ await daemonCall('session.launch', { path: '/Users/you/dev/api' })
869
+
870
+ // Tile them side by side
871
+ const sessions = await daemonCall('tmux.sessions')
872
+ const fe = sessions.find(s => s.name.startsWith('frontend'))
873
+ const api = sessions.find(s => s.name.startsWith('api'))
874
+
875
+ await daemonCall('window.tile', { session: fe.name, position: 'left' })
876
+ await daemonCall('window.tile', { session: api.name, position: 'right' })
877
+ ```
878
+
879
+ ### Reactive event pattern
880
+
881
+ Subscribe to events to react to workspace changes:
882
+
883
+ ```js
884
+ import WebSocket from 'ws'
885
+
886
+ const ws = new WebSocket('ws://127.0.0.1:9399')
887
+
888
+ ws.on('message', (raw) => {
889
+ const msg = JSON.parse(raw)
890
+
891
+ if (msg.event === 'tmux.changed') {
892
+ console.log('Sessions:', msg.data.sessions.join(', '))
893
+ }
894
+
895
+ if (msg.event === 'windows.changed') {
896
+ console.log('Windows:', msg.data.windowCount, 'total')
897
+ }
898
+
899
+ if (msg.event === 'layer.switched') {
900
+ console.log('Switched to layer', msg.data.index)
901
+ }
902
+ })
903
+
904
+ // You can also send RPC calls on the same connection
905
+ ws.on('open', () => {
906
+ ws.send(JSON.stringify({ id: '1', method: 'daemon.status' }))
907
+ })
908
+ ```
909
+
910
+ ### Health check before use
911
+
912
+ Always verify the daemon is running before making calls:
913
+
914
+ ```js
915
+ import { isDaemonRunning, daemonCall } from '@lattices/cli'
916
+
917
+ if (!(await isDaemonRunning())) {
918
+ console.error('lattices daemon is not running — start it with: lattices app')
919
+ process.exit(1)
920
+ }
921
+
922
+ const status = await daemonCall('daemon.status')
923
+ console.log(`Daemon up for ${Math.round(status.uptime)}s, tracking ${status.windowCount} windows`)
924
+ ```