@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.
- package/README.md +155 -0
- package/app/Lattices.app/Contents/Info.plist +24 -0
- package/app/Package.swift +13 -0
- package/app/Sources/AccessibilityTextExtractor.swift +111 -0
- package/app/Sources/ActionRow.swift +61 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +242 -0
- package/app/Sources/AppShellView.swift +62 -0
- package/app/Sources/AppTypeClassifier.swift +70 -0
- package/app/Sources/AppWindowShell.swift +63 -0
- package/app/Sources/CheatSheetHUD.swift +332 -0
- package/app/Sources/CommandModeState.swift +1362 -0
- package/app/Sources/CommandModeView.swift +1405 -0
- package/app/Sources/CommandModeWindow.swift +192 -0
- package/app/Sources/CommandPaletteView.swift +307 -0
- package/app/Sources/CommandPaletteWindow.swift +134 -0
- package/app/Sources/DaemonProtocol.swift +101 -0
- package/app/Sources/DaemonServer.swift +414 -0
- package/app/Sources/DesktopModel.swift +149 -0
- package/app/Sources/DesktopModelTypes.swift +71 -0
- package/app/Sources/DiagnosticLog.swift +271 -0
- package/app/Sources/EventBus.swift +30 -0
- package/app/Sources/HotkeyManager.swift +254 -0
- package/app/Sources/HotkeyStore.swift +338 -0
- package/app/Sources/InventoryManager.swift +35 -0
- package/app/Sources/InventoryPath.swift +43 -0
- package/app/Sources/KeyRecorderView.swift +210 -0
- package/app/Sources/LatticesApi.swift +1234 -0
- package/app/Sources/LayerBezel.swift +203 -0
- package/app/Sources/MainView.swift +479 -0
- package/app/Sources/MainWindow.swift +83 -0
- package/app/Sources/OcrModel.swift +430 -0
- package/app/Sources/OcrStore.swift +329 -0
- package/app/Sources/OmniSearchState.swift +283 -0
- package/app/Sources/OmniSearchView.swift +288 -0
- package/app/Sources/OmniSearchWindow.swift +105 -0
- package/app/Sources/OrphanRow.swift +129 -0
- package/app/Sources/PaletteCommand.swift +419 -0
- package/app/Sources/PermissionChecker.swift +125 -0
- package/app/Sources/Preferences.swift +99 -0
- package/app/Sources/ProcessModel.swift +199 -0
- package/app/Sources/ProcessQuery.swift +151 -0
- package/app/Sources/Project.swift +28 -0
- package/app/Sources/ProjectRow.swift +368 -0
- package/app/Sources/ProjectScanner.swift +128 -0
- package/app/Sources/ScreenMapState.swift +2387 -0
- package/app/Sources/ScreenMapView.swift +2820 -0
- package/app/Sources/ScreenMapWindowController.swift +89 -0
- package/app/Sources/SessionManager.swift +72 -0
- package/app/Sources/SettingsView.swift +1064 -0
- package/app/Sources/SettingsWindow.swift +20 -0
- package/app/Sources/TabGroupRow.swift +178 -0
- package/app/Sources/Terminal.swift +259 -0
- package/app/Sources/TerminalQuery.swift +156 -0
- package/app/Sources/TerminalSynthesizer.swift +200 -0
- package/app/Sources/Theme.swift +163 -0
- package/app/Sources/TilePickerView.swift +209 -0
- package/app/Sources/TmuxModel.swift +53 -0
- package/app/Sources/TmuxQuery.swift +81 -0
- package/app/Sources/WindowTiler.swift +1778 -0
- package/app/Sources/WorkspaceManager.swift +575 -0
- package/bin/client.js +4 -0
- package/bin/daemon-client.js +187 -0
- package/bin/lattices-app.js +221 -0
- package/bin/lattices.js +1551 -0
- package/docs/api.md +924 -0
- package/docs/app.md +297 -0
- package/docs/concepts.md +135 -0
- package/docs/config.md +245 -0
- package/docs/layers.md +410 -0
- package/docs/ocr.md +185 -0
- package/docs/overview.md +94 -0
- package/docs/quickstart.md +75 -0
- 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
|
+
```
|