@lattices/cli 0.5.0 → 0.6.1
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 +14 -5
- package/apps/mac/Info.plist +4 -2
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -2
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +11 -0
- package/apps/mac/Lattices.entitlements +6 -0
- package/bin/assistant-intelligence.ts +41 -3
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +2 -1
- package/bin/cua.ts +26 -0
- package/bin/infer.ts +22 -4
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +111 -12
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +29 -2
- package/bin/lattices.ts +729 -769
- package/docs/api.md +496 -3
- package/docs/app.md +5 -4
- package/docs/assistant-knowledge.md +130 -0
- package/docs/config.md +5 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +53 -0
- package/docs/mouse-gestures.md +40 -3
- package/docs/ocr.md +3 -0
- package/docs/prompts/hands-off-system.md +9 -1
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/{LAT-006-mira-in-lattices.md → LAT-006-runs-and-capture-in-lattices.md} +83 -70
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +3 -1
- package/docs/reference/dewey.config.ts +1 -1
- package/docs/release.md +4 -3
- package/docs/repo-structure.md +1 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +5 -3
- package/docs/voice.md +3 -3
- package/package.json +29 -5
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
The agentic workspace manager for macOS.
|
|
8
8
|
|
|
9
9
|
Lattices turns your Mac workspace into a coherent API, so agents can see and
|
|
10
|
-
control windows,
|
|
10
|
+
control windows, terminal sessions, screen text, and layouts. It also gives you
|
|
11
11
|
an assistant to control that workspace in plain language.
|
|
12
12
|
|
|
13
13
|
**[lattices.dev](https://lattices.dev)** · [Docs](https://lattices.dev/docs/overview) · [Download](https://github.com/arach/lattices/releases/latest)
|
|
@@ -31,10 +31,11 @@ your project directory.
|
|
|
31
31
|
### Install the CLI
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
npm install -g @lattices
|
|
34
|
+
npm install -g @arach/lattices
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Also published as `@lattices/cli` for existing installs. The CLI and app work
|
|
38
|
+
independently — use either or both.
|
|
38
39
|
|
|
39
40
|
### Build from source
|
|
40
41
|
|
|
@@ -46,7 +47,7 @@ cd lattices
|
|
|
46
47
|
npm install
|
|
47
48
|
|
|
48
49
|
# Build/install/relaunch the dev app at the stable permission target
|
|
49
|
-
./run.sh
|
|
50
|
+
./scripts/run.sh
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
To build a signed, notarized DMG for distribution:
|
|
@@ -64,9 +65,15 @@ See [Release](docs/release.md) for the CI and local maintainer workflows.
|
|
|
64
65
|
## Quick start
|
|
65
66
|
|
|
66
67
|
```sh
|
|
68
|
+
# Show workspace status for the current directory
|
|
69
|
+
lattices
|
|
70
|
+
|
|
67
71
|
# Launch the menu bar app
|
|
68
72
|
lattices app
|
|
69
73
|
|
|
74
|
+
# Start or reattach a tmux session
|
|
75
|
+
lattices start
|
|
76
|
+
|
|
70
77
|
# Open the command palette from anywhere
|
|
71
78
|
# Cmd+Shift+M
|
|
72
79
|
```
|
|
@@ -163,7 +170,7 @@ events over WebSocket. Anything you can do from the app, an agent or
|
|
|
163
170
|
script can do over the API.
|
|
164
171
|
|
|
165
172
|
```js
|
|
166
|
-
import { daemonCall } from '@lattices/
|
|
173
|
+
import { daemonCall } from '@arach/lattices/daemon-client'
|
|
167
174
|
|
|
168
175
|
// Search windows by content — title, app, session tags, OCR
|
|
169
176
|
const results = await daemonCall('windows.search', { query: 'myproject' })
|
|
@@ -182,6 +189,7 @@ Or from the CLI:
|
|
|
182
189
|
```sh
|
|
183
190
|
lattices search myproject # Find windows by content
|
|
184
191
|
lattices search myproject --deep # Include terminal tab/process data
|
|
192
|
+
lattices search myproject --all # Same as --deep (all search sources)
|
|
185
193
|
lattices place myproject left # Search + focus + tile in one step
|
|
186
194
|
```
|
|
187
195
|
|
|
@@ -199,6 +207,7 @@ lattices ls List active sessions
|
|
|
199
207
|
lattices kill [name] Kill a session
|
|
200
208
|
lattices search <query> Search windows by title, app, session, OCR
|
|
201
209
|
lattices search <q> --deep Deep search: index + terminal inspection
|
|
210
|
+
lattices search <q> --all Same as --deep (all search sources)
|
|
202
211
|
lattices place <query> [pos] Deep search + focus + tile
|
|
203
212
|
lattices focus <session> Raise a session's window
|
|
204
213
|
lattices tile <position> Tile frontmost window
|
package/apps/mac/Info.plist
CHANGED
|
@@ -26,15 +26,17 @@
|
|
|
26
26
|
</dict>
|
|
27
27
|
</array>
|
|
28
28
|
<key>CFBundleVersion</key>
|
|
29
|
-
<string>0.
|
|
29
|
+
<string>0.6.1</string>
|
|
30
30
|
<key>CFBundleShortVersionString</key>
|
|
31
|
-
<string>0.
|
|
31
|
+
<string>0.6.1</string>
|
|
32
32
|
<key>LSMinimumSystemVersion</key>
|
|
33
33
|
<string>13.0</string>
|
|
34
34
|
<key>LSUIElement</key>
|
|
35
35
|
<true/>
|
|
36
36
|
<key>NSHighResolutionCapable</key>
|
|
37
37
|
<true/>
|
|
38
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
39
|
+
<string>Lattices uses the microphone for Hudson Voice dictation and voice commands.</string>
|
|
38
40
|
<key>NSSupportsAutomaticTermination</key>
|
|
39
41
|
<true/>
|
|
40
42
|
</dict>
|
|
@@ -26,15 +26,17 @@
|
|
|
26
26
|
</dict>
|
|
27
27
|
</array>
|
|
28
28
|
<key>CFBundleVersion</key>
|
|
29
|
-
<string>0.
|
|
29
|
+
<string>0.6.1</string>
|
|
30
30
|
<key>CFBundleShortVersionString</key>
|
|
31
|
-
<string>0.
|
|
31
|
+
<string>0.6.1</string>
|
|
32
32
|
<key>LSMinimumSystemVersion</key>
|
|
33
33
|
<string>13.0</string>
|
|
34
34
|
<key>LSUIElement</key>
|
|
35
35
|
<true/>
|
|
36
36
|
<key>NSHighResolutionCapable</key>
|
|
37
37
|
<true/>
|
|
38
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
39
|
+
<string>Lattices uses the microphone for Hudson Voice dictation and voice commands.</string>
|
|
38
40
|
<key>NSSupportsAutomaticTermination</key>
|
|
39
41
|
<true/>
|
|
40
42
|
</dict>
|
|
Binary file
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Assistant Knowledge Base
|
|
3
|
+
title: Lattices — Assistant Knowledge
|
|
4
|
+
description: Orientation + capability map the in-app Workspace Assistant uses to explain Lattices and point to the right feature or doc
|
|
5
|
+
audience: assistant
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
> You are reading the Workspace Assistant's knowledge base. It summarizes what
|
|
9
|
+
> Lattices can do and links to the deeper docs. Treat the **structured context**
|
|
10
|
+
> in your prompt (current settings, file paths, CLI commands) as ground truth for
|
|
11
|
+
> *this user's* configuration; treat this file as ground truth for *how Lattices
|
|
12
|
+
> works*. When a question goes deeper than this summary, name the relevant doc
|
|
13
|
+
> (see [References](#references)) instead of guessing.
|
|
14
|
+
|
|
15
|
+
## What Lattices is
|
|
16
|
+
|
|
17
|
+
Lattices is an **agentic window manager for macOS** — a programmable workspace
|
|
18
|
+
that pairs a native menu bar app with managed tmux sessions and a scriptable
|
|
19
|
+
agent API. Three layers, one product:
|
|
20
|
+
|
|
21
|
+
1. **Programmable workspace** — a CLI and a WebSocket agent API (`ws://127.0.0.1:9399`,
|
|
22
|
+
35+ methods, real-time events) that let scripts and AI agents observe and drive
|
|
23
|
+
the desktop the same way a person does.
|
|
24
|
+
2. **Smart layout manager** — the menu bar app tracks every window across all
|
|
25
|
+
monitors: tiling, switchable layers, snap zones, and screen-text indexing.
|
|
26
|
+
3. **Managed tmux sessions** — declare a dev environment in `.lattices.json`;
|
|
27
|
+
Lattices builds it, runs it, and keeps it alive across reboots.
|
|
28
|
+
|
|
29
|
+
Requirements: macOS 26+, Node 18+; tmux only for session management.
|
|
30
|
+
See [Overview](/docs/overview) and [Concepts](/docs/concepts).
|
|
31
|
+
|
|
32
|
+
## Capability map
|
|
33
|
+
|
|
34
|
+
Each area below is a one-paragraph summary plus the doc to cite for detail.
|
|
35
|
+
|
|
36
|
+
### Window tiling & placement
|
|
37
|
+
Snap windows to preset positions — halves, quarters, thirds, maximize, center —
|
|
38
|
+
from the command palette or `lattices tile <position>`. There is also a grid
|
|
39
|
+
placement primitive: compact `CxR:c,r` starts at 1 for command entry, while
|
|
40
|
+
canonical `grid:CxR:c,r` starts at 0 for APIs. → [Tiling reference](/docs/tiling-reference), positions in [Configuration](/docs/config).
|
|
41
|
+
|
|
42
|
+
### Workspace layers & tab groups
|
|
43
|
+
Group projects into named **layers** you can switch between, and tab-group related
|
|
44
|
+
windows. `workspace.json` layers launch/focus/tile projects. Studio layers are
|
|
45
|
+
rule-backed live window sets persisted in `~/.lattices/layers.json`; their clauses
|
|
46
|
+
support app/title/session exact, substring, regex, Space, visibility, and exclusion
|
|
47
|
+
matches. → [Layers](/docs/layers).
|
|
48
|
+
|
|
49
|
+
### Command palette & menu bar app
|
|
50
|
+
The palette (**Cmd+Shift+M**) is the app's primary surface: launch projects, tile,
|
|
51
|
+
sync, restart, open settings — all searchable. → [Menu Bar App](/docs/app).
|
|
52
|
+
|
|
53
|
+
### tmux sessions (`.lattices.json`)
|
|
54
|
+
Declare panes, commands, and layout per project. `lattices start` builds/attaches a
|
|
55
|
+
persistent session named `<basename>-<hash>`; `lattices sync` reconciles a running
|
|
56
|
+
session to its config. **Ensure** re-runs exited commands on reattach; **prefill**
|
|
57
|
+
types them and waits. → [Concepts](/docs/concepts), [Configuration](/docs/config).
|
|
58
|
+
|
|
59
|
+
### Screen OCR & search
|
|
60
|
+
The app reads on-screen text via the Accessibility API (~60s) and Apple Vision OCR
|
|
61
|
+
on background windows (~2h), indexing everything with FTS5. Search across titles,
|
|
62
|
+
app names, session tags, and OCR with `lattices search <query>` (add `--deep` or
|
|
63
|
+
`--all` to inspect terminal tabs by cwd). → [Screen OCR & Search](/docs/ocr).
|
|
64
|
+
|
|
65
|
+
### Voice commands
|
|
66
|
+
Natural-language voice control for window management ("put the browser on the
|
|
67
|
+
right", "switch to the backend layer"). → [Voice Commands](/docs/voice).
|
|
68
|
+
|
|
69
|
+
### Mouse gestures
|
|
70
|
+
Hold a mouse button, draw a direction or shape, release — runs the matched action.
|
|
71
|
+
Configured via `mouseGestures.enabled` plus `~/.lattices/mouse-shortcuts.json`.
|
|
72
|
+
→ [Mouse Gestures](/docs/mouse-gestures).
|
|
73
|
+
|
|
74
|
+
### Agent API & CLI
|
|
75
|
+
Agents connect over WebSocket and get the same control as a person: list
|
|
76
|
+
windows/projects, launch sessions, tile, switch layers, read screen text, and
|
|
77
|
+
subscribe to events (`windows.changed`, `tmux.changed`, `layer.switched`).
|
|
78
|
+
→ [Agent Guide](/docs/agents), [Agent API](/docs/api).
|
|
79
|
+
|
|
80
|
+
### Project twins
|
|
81
|
+
Pi-backed project "twins" for mediated, persistent agent execution scoped to a
|
|
82
|
+
project. → [Project Twins](/docs/twins).
|
|
83
|
+
|
|
84
|
+
## Key shortcuts
|
|
85
|
+
|
|
86
|
+
| Shortcut | Action |
|
|
87
|
+
|----------|--------|
|
|
88
|
+
| **Cmd+Shift+M** | Open the command palette |
|
|
89
|
+
| `lattices tile <position>` | Tile the focused window (CLI) |
|
|
90
|
+
| `lattices layer [name\|index]` | Switch workspace layer (CLI) |
|
|
91
|
+
| **Ctrl+B** then `D` / `Z` / arrows | tmux: detach / zoom / move pane (inside a session) |
|
|
92
|
+
|
|
93
|
+
Tiling and grid hotkeys are user-configurable — for the live set, point the user to
|
|
94
|
+
Settings or the [Tiling reference](/docs/tiling-reference) rather than asserting one.
|
|
95
|
+
|
|
96
|
+
## CLI quick reference
|
|
97
|
+
|
|
98
|
+
`lattices` · `lattices init` · `lattices sync` · `lattices start` ·
|
|
99
|
+
`lattices restart [pane]` · `lattices tile <position>` · `lattices group [id]` ·
|
|
100
|
+
`lattices layer [name|index]` · `lattices windows --json` ·
|
|
101
|
+
`lattices search <query> [--deep|--all] [--json] [--wid]` · `lattices place <query> [position]` ·
|
|
102
|
+
`lattices app restart`. Full flags: [Configuration](/docs/config).
|
|
103
|
+
|
|
104
|
+
## Config & file locations
|
|
105
|
+
|
|
106
|
+
- **Per project:** `.lattices.json` in the project root (panes, commands, layout, ensure/prefill).
|
|
107
|
+
- **User config (`~/.lattices/`):** `workspace.json`, `layers.json`, `mouse-shortcuts.json`,
|
|
108
|
+
`snap-zones.json`, `clusters.json`, `ocr.db`, `lattices.log`.
|
|
109
|
+
- **Defaults domain:** `dev.lattices.app` (read/write app settings via `defaults`).
|
|
110
|
+
|
|
111
|
+
The exact current values and paths for *this* machine arrive in your structured
|
|
112
|
+
context — prefer those over the generic paths above when answering.
|
|
113
|
+
|
|
114
|
+
## References
|
|
115
|
+
|
|
116
|
+
| Topic | Doc |
|
|
117
|
+
|-------|-----|
|
|
118
|
+
| What it is / who it's for | [Overview](/docs/overview) |
|
|
119
|
+
| Install & first run | [Quickstart](/docs/quickstart) |
|
|
120
|
+
| Architecture, glossary, internals | [Concepts](/docs/concepts) |
|
|
121
|
+
| `.lattices.json`, CLI, tile positions | [Configuration](/docs/config) |
|
|
122
|
+
| Command palette, tiling, sessions | [Menu Bar App](/docs/app) |
|
|
123
|
+
| Tiling & grid placement | [Tiling reference](/docs/tiling-reference) |
|
|
124
|
+
| Layers & tab groups | [Layers](/docs/layers) |
|
|
125
|
+
| Screen OCR & full-text search | [Screen OCR & Search](/docs/ocr) |
|
|
126
|
+
| Voice control | [Voice Commands](/docs/voice) |
|
|
127
|
+
| Mouse gestures | [Mouse Gestures](/docs/mouse-gestures) |
|
|
128
|
+
| Agent contracts (voice/CLI/daemon) | [Agent Guide](/docs/agents) |
|
|
129
|
+
| WebSocket RPC method reference | [Agent API](/docs/api) |
|
|
130
|
+
| Project twins | [Project Twins](/docs/twins) |
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
<data>
|
|
9
9
|
3sIZmtGJHMo2S/XvIMl46SOHcFY=
|
|
10
10
|
</data>
|
|
11
|
+
<key>Resources/docs/assistant-knowledge.md</key>
|
|
12
|
+
<data>
|
|
13
|
+
+ps2zJk/lYJk2p6BwFaTyELnEYc=
|
|
14
|
+
</data>
|
|
11
15
|
<key>Resources/tap.wav</key>
|
|
12
16
|
<data>
|
|
13
17
|
eOpp5td/ovQGMumXPpwy4Vyt/uc=
|
|
@@ -22,6 +26,13 @@
|
|
|
22
26
|
LZsztS/9I1hmuQmDOk+anfxOpqVryB3y4a1kwSaUK4s=
|
|
23
27
|
</data>
|
|
24
28
|
</dict>
|
|
29
|
+
<key>Resources/docs/assistant-knowledge.md</key>
|
|
30
|
+
<dict>
|
|
31
|
+
<key>hash2</key>
|
|
32
|
+
<data>
|
|
33
|
+
R8Iq4eNj09I0SVKg3WEFeYJ6q1tLM4x3hwTG9XO7oco=
|
|
34
|
+
</data>
|
|
35
|
+
</dict>
|
|
25
36
|
<key>Resources/tap.wav</key>
|
|
26
37
|
<dict>
|
|
27
38
|
<key>hash2</key>
|
|
@@ -11,5 +11,11 @@
|
|
|
11
11
|
<true/>
|
|
12
12
|
<key>com.apple.security.network.client</key>
|
|
13
13
|
<true/>
|
|
14
|
+
|
|
15
|
+
<!-- Microphone: required under Hardened Runtime for the embedded Hudson Voice
|
|
16
|
+
dictation/voice-command capture. Without it, AVCaptureDevice.requestAccess
|
|
17
|
+
is denied instantly with no prompt. -->
|
|
18
|
+
<key>com.apple.security.device.audio-input</key>
|
|
19
|
+
<true/>
|
|
14
20
|
</dict>
|
|
15
21
|
</plist>
|
|
@@ -87,7 +87,7 @@ export const intentDefinitions: IntentDefinition[] = [
|
|
|
87
87
|
intent: "tile_window",
|
|
88
88
|
description: "Tile one window to a named position or grid cell.",
|
|
89
89
|
slots: [
|
|
90
|
-
{ name: "position", required: true, description: "Named tile position
|
|
90
|
+
{ name: "position", required: true, description: "Named tile position, canonical 0-based grid:CxR:C,R, or compact 1-based CxR:C,R syntax." },
|
|
91
91
|
{ name: "app", description: "Loose app name when no window id is known." },
|
|
92
92
|
{ name: "wid", description: "Specific macOS window id from the desktop snapshot." },
|
|
93
93
|
{ name: "session", description: "Tmux session name." },
|
|
@@ -96,6 +96,7 @@ export const intentDefinitions: IntentDefinition[] = [
|
|
|
96
96
|
"tile chrome left",
|
|
97
97
|
"snap this to the top right",
|
|
98
98
|
"maximize the window",
|
|
99
|
+
"put chrome in the top-right cell of a 4x4 grid",
|
|
99
100
|
],
|
|
100
101
|
},
|
|
101
102
|
{
|
|
@@ -687,8 +688,11 @@ function parseLayerSwitch(text: string): string | null {
|
|
|
687
688
|
}
|
|
688
689
|
|
|
689
690
|
function findPosition(text: string): { position: string; phrase: string } | null {
|
|
690
|
-
const grid = text.match(/grid
|
|
691
|
-
if (grid)
|
|
691
|
+
const grid = text.match(/(?:grid:)?\d+x\d+:\d+,\d+(?:-\d+,\d+)?/);
|
|
692
|
+
if (grid) {
|
|
693
|
+
const position = canonicalGridPosition(grid[0]);
|
|
694
|
+
if (position) return { position, phrase: grid[0] };
|
|
695
|
+
}
|
|
692
696
|
|
|
693
697
|
for (const entry of positionAliases) {
|
|
694
698
|
for (const phrase of entry.phrases) {
|
|
@@ -700,6 +704,40 @@ function findPosition(text: string): { position: string; phrase: string } | null
|
|
|
700
704
|
return null;
|
|
701
705
|
}
|
|
702
706
|
|
|
707
|
+
function canonicalGridPosition(raw: string): string | null {
|
|
708
|
+
const match = raw.toLowerCase().match(/^(grid:)?(\d+)x(\d+):(\d+),(\d+)(?:-(\d+),(\d+))?$/);
|
|
709
|
+
if (!match) return null;
|
|
710
|
+
|
|
711
|
+
const oneBased = !match[1];
|
|
712
|
+
const columns = Number(match[2]);
|
|
713
|
+
const rows = Number(match[3]);
|
|
714
|
+
let c0 = Number(match[4]);
|
|
715
|
+
let r0 = Number(match[5]);
|
|
716
|
+
let c1 = match[6] === undefined ? c0 : Number(match[6]);
|
|
717
|
+
let r1 = match[7] === undefined ? r0 : Number(match[7]);
|
|
718
|
+
if (oneBased) {
|
|
719
|
+
c0 -= 1;
|
|
720
|
+
r0 -= 1;
|
|
721
|
+
c1 -= 1;
|
|
722
|
+
r1 -= 1;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const left = Math.min(c0, c1);
|
|
726
|
+
const right = Math.max(c0, c1);
|
|
727
|
+
const top = Math.min(r0, r1);
|
|
728
|
+
const bottom = Math.max(r0, r1);
|
|
729
|
+
if (
|
|
730
|
+
columns <= 0 || rows <= 0 ||
|
|
731
|
+
left < 0 || top < 0 ||
|
|
732
|
+
right >= columns || bottom >= rows
|
|
733
|
+
) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (left === right && top === bottom) return `grid:${columns}x${rows}:${left},${top}`;
|
|
738
|
+
return `grid:${columns}x${rows}:${left},${top}-${right},${bottom}`;
|
|
739
|
+
}
|
|
740
|
+
|
|
703
741
|
function regionFromText(text: string): string | null {
|
|
704
742
|
const hit = findPosition(text);
|
|
705
743
|
if (!hit) return null;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasFlag,
|
|
3
|
+
nonFlagArgs,
|
|
4
|
+
parseFlagValue,
|
|
5
|
+
parseOptionalNumber,
|
|
6
|
+
} from "./helpers.ts";
|
|
7
|
+
import { withDaemon } from "./daemon.ts";
|
|
8
|
+
|
|
9
|
+
export async function captureCommand(subcommand?: string, ...rawArgs: string[]): Promise<void> {
|
|
10
|
+
const sub = subcommand || "window";
|
|
11
|
+
const dashIndex = rawArgs.indexOf("--");
|
|
12
|
+
const commandArgs = dashIndex >= 0 ? rawArgs.slice(0, dashIndex) : rawArgs;
|
|
13
|
+
const childArgs = dashIndex >= 0 ? rawArgs.slice(dashIndex + 1) : [];
|
|
14
|
+
const jsonFlag = hasFlag(commandArgs, "json");
|
|
15
|
+
const positional = nonFlagArgs(commandArgs);
|
|
16
|
+
|
|
17
|
+
if (["stop", "stop-recording", "stopRecording"].includes(sub)) {
|
|
18
|
+
const params: Record<string, unknown> = {};
|
|
19
|
+
const runId = positional[0] || parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId") || parseFlagValue(commandArgs, "id");
|
|
20
|
+
const stopFile = parseFlagValue(commandArgs, "stop-file") || parseFlagValue(commandArgs, "stopFile");
|
|
21
|
+
const finishedFile = parseFlagValue(commandArgs, "finished-file") || parseFlagValue(commandArgs, "finishedFile");
|
|
22
|
+
const timeoutMs = Number(parseFlagValue(commandArgs, "timeout-ms") || parseFlagValue(commandArgs, "timeoutMs") || 30000);
|
|
23
|
+
if (runId) params.runId = runId;
|
|
24
|
+
if (stopFile) params.stopFile = stopFile;
|
|
25
|
+
if (finishedFile) params.finishedFile = finishedFile;
|
|
26
|
+
if (Number.isFinite(timeoutMs)) params.timeoutMs = timeoutMs;
|
|
27
|
+
params.wait = !hasFlag(commandArgs, "no-wait");
|
|
28
|
+
|
|
29
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
30
|
+
const result = await daemonCall("capture.stopRecording", params, timeoutMs + 5000) as any;
|
|
31
|
+
if (jsonFlag) {
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(result.finished ? "Recording finished." : "Recording stop requested.");
|
|
36
|
+
if (result.run?.id) console.log(` run: ${result.run.id}`);
|
|
37
|
+
if (result.marker) console.log(` marker: ${result.marker}`);
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isRecordCommand = [
|
|
43
|
+
"record-command",
|
|
44
|
+
"recordCommand",
|
|
45
|
+
"record-run",
|
|
46
|
+
"recordRun",
|
|
47
|
+
"record-exec",
|
|
48
|
+
"recordExec",
|
|
49
|
+
].includes(sub);
|
|
50
|
+
|
|
51
|
+
if (isRecordCommand) {
|
|
52
|
+
if (!childArgs.length) {
|
|
53
|
+
console.log(`lattices capture record-command — record while running a command
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
lattices capture record-command --app Scout --filename demo.mov -- <command> [...args]
|
|
57
|
+
`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const params: Record<string, unknown> = { source: "cli" };
|
|
62
|
+
const explicitWid = positional[0] ? Number(positional[0]) : NaN;
|
|
63
|
+
if (Number.isFinite(explicitWid)) params.wid = explicitWid;
|
|
64
|
+
|
|
65
|
+
const session = parseFlagValue(commandArgs, "session");
|
|
66
|
+
const app = parseFlagValue(commandArgs, "app");
|
|
67
|
+
const title = parseFlagValue(commandArgs, "title");
|
|
68
|
+
const filename = parseFlagValue(commandArgs, "filename");
|
|
69
|
+
const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
|
|
70
|
+
const mode = parseFlagValue(commandArgs, "mode");
|
|
71
|
+
const fps = parseOptionalNumber(commandArgs, "fps");
|
|
72
|
+
const scale = parseOptionalNumber(commandArgs, "scale");
|
|
73
|
+
const timeoutMs = Number(parseFlagValue(commandArgs, "timeout-ms") || parseFlagValue(commandArgs, "timeoutMs") || 30000);
|
|
74
|
+
if (session) params.session = session;
|
|
75
|
+
if (app) params.app = app;
|
|
76
|
+
if (title) params.title = title;
|
|
77
|
+
if (filename) params.filename = filename;
|
|
78
|
+
if (runId) params.runId = runId;
|
|
79
|
+
if (mode) params.mode = mode;
|
|
80
|
+
if (fps !== undefined) params.fps = fps;
|
|
81
|
+
if (scale !== undefined) params.scale = scale;
|
|
82
|
+
|
|
83
|
+
for (const [flag, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["w", "w"], ["h", "h"]] as const) {
|
|
84
|
+
const value = parseOptionalNumber(commandArgs, flag);
|
|
85
|
+
if (value !== undefined) params[key] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const recordsRegion = hasFlag(commandArgs, "region") ||
|
|
89
|
+
(params.x !== undefined && params.y !== undefined &&
|
|
90
|
+
(params.width !== undefined || params.w !== undefined) &&
|
|
91
|
+
(params.height !== undefined || params.h !== undefined));
|
|
92
|
+
const method = recordsRegion ? "capture.recordRegion" : "capture.recordWindow";
|
|
93
|
+
|
|
94
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
95
|
+
const start = await daemonCall(method, params, 20000) as any;
|
|
96
|
+
let childExitCode = 0;
|
|
97
|
+
let childError: string | undefined;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const proc = Bun.spawn(childArgs, {
|
|
101
|
+
cwd: process.cwd(),
|
|
102
|
+
env: process.env,
|
|
103
|
+
stdin: "inherit",
|
|
104
|
+
stdout: "inherit",
|
|
105
|
+
stderr: "inherit",
|
|
106
|
+
});
|
|
107
|
+
childExitCode = await proc.exited;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
childExitCode = 127;
|
|
110
|
+
childError = (error as Error).message;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stop = await daemonCall(
|
|
114
|
+
"capture.stopRecording",
|
|
115
|
+
{ runId: start.run?.id, wait: true, timeoutMs },
|
|
116
|
+
timeoutMs + 5000
|
|
117
|
+
) as any;
|
|
118
|
+
|
|
119
|
+
if (jsonFlag) {
|
|
120
|
+
console.log(JSON.stringify({
|
|
121
|
+
ok: childExitCode === 0 && stop.ok !== false,
|
|
122
|
+
child: {
|
|
123
|
+
command: childArgs,
|
|
124
|
+
exitCode: childExitCode,
|
|
125
|
+
error: childError,
|
|
126
|
+
},
|
|
127
|
+
recording: start,
|
|
128
|
+
stopResult: stop,
|
|
129
|
+
}, null, 2));
|
|
130
|
+
} else {
|
|
131
|
+
const artifact = start.artifact || {};
|
|
132
|
+
const run = stop.run || start.run || {};
|
|
133
|
+
console.log(`Recording finished.`);
|
|
134
|
+
console.log(` run: ${run.id || start.run?.id || "?"}`);
|
|
135
|
+
console.log(` artifact: ${artifact.path || "?"}`);
|
|
136
|
+
console.log(` child exit: ${childExitCode}`);
|
|
137
|
+
if (childError) console.log(` child error: ${childError}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (childExitCode !== 0 && !hasFlag(commandArgs, "ignore-child-failure")) {
|
|
141
|
+
process.exitCode = childExitCode;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isRecord = ["record", "record-window", "recording", "video"].includes(sub);
|
|
148
|
+
const isRecordRegion = ["record-region", "recordRegion", "region-recording"].includes(sub) ||
|
|
149
|
+
(sub === "record" && ["region", "rect"].includes(positional[0] || ""));
|
|
150
|
+
|
|
151
|
+
if (isRecord || isRecordRegion) {
|
|
152
|
+
const params: Record<string, unknown> = { source: "cli" };
|
|
153
|
+
const targetKind = sub === "record" ? positional[0] : undefined;
|
|
154
|
+
const positionalOffset = targetKind === "window" || targetKind === "region" || targetKind === "rect" ? 1 : 0;
|
|
155
|
+
const explicitWid = positional[positionalOffset] ? Number(positional[positionalOffset]) : NaN;
|
|
156
|
+
if (Number.isFinite(explicitWid)) params.wid = explicitWid;
|
|
157
|
+
|
|
158
|
+
const session = parseFlagValue(commandArgs, "session");
|
|
159
|
+
const app = parseFlagValue(commandArgs, "app");
|
|
160
|
+
const title = parseFlagValue(commandArgs, "title");
|
|
161
|
+
const filename = parseFlagValue(commandArgs, "filename");
|
|
162
|
+
const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
|
|
163
|
+
const mode = parseFlagValue(commandArgs, "mode");
|
|
164
|
+
const fps = parseOptionalNumber(commandArgs, "fps");
|
|
165
|
+
const scale = parseOptionalNumber(commandArgs, "scale");
|
|
166
|
+
const durationMs = parseOptionalNumber(commandArgs, "duration-ms", "durationMs", "duration");
|
|
167
|
+
if (session) params.session = session;
|
|
168
|
+
if (app) params.app = app;
|
|
169
|
+
if (title) params.title = title;
|
|
170
|
+
if (filename) params.filename = filename;
|
|
171
|
+
if (runId) params.runId = runId;
|
|
172
|
+
if (mode) params.mode = mode;
|
|
173
|
+
if (fps !== undefined) params.fps = fps;
|
|
174
|
+
if (scale !== undefined) params.scale = scale;
|
|
175
|
+
|
|
176
|
+
for (const [flag, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["w", "w"], ["h", "h"]] as const) {
|
|
177
|
+
const value = parseOptionalNumber(commandArgs, flag);
|
|
178
|
+
if (value !== undefined) params[key] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
182
|
+
const method = isRecordRegion ? "capture.recordRegion" : "capture.recordWindow";
|
|
183
|
+
const result = await daemonCall(method, params, 20000) as any;
|
|
184
|
+
|
|
185
|
+
if (durationMs !== undefined && durationMs > 0) {
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
187
|
+
const stop = await daemonCall(
|
|
188
|
+
"capture.stopRecording",
|
|
189
|
+
{ runId: result.run?.id, wait: true, timeoutMs: 30000 },
|
|
190
|
+
35000
|
|
191
|
+
) as any;
|
|
192
|
+
result.stopResult = stop;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (jsonFlag) {
|
|
196
|
+
console.log(JSON.stringify(result, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const artifact = result.artifact || {};
|
|
200
|
+
const run = result.stopResult?.run || result.run || {};
|
|
201
|
+
console.log(`Recording ${result.stopResult ? "finished" : "started"}.`);
|
|
202
|
+
console.log(` run: ${run.id || result.run?.id || "?"}`);
|
|
203
|
+
console.log(` artifact: ${artifact.path || "?"}`);
|
|
204
|
+
if (!result.stopResult) {
|
|
205
|
+
console.log(` stop: lattices capture stop ${result.run?.id || ""}`);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!["window", "screenshot", "shot"].includes(sub)) {
|
|
212
|
+
console.log(`lattices capture — capture run artifacts
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
lattices capture window [wid] [--json]
|
|
216
|
+
lattices capture screenshot [wid] [--session name] [--app name]
|
|
217
|
+
lattices capture record window [wid] [--app name] [--duration-ms 5000] [--json]
|
|
218
|
+
lattices capture record region --x N --y N --width N --height N [--duration-ms 5000]
|
|
219
|
+
lattices capture record-command --app Scout --filename demo.mov -- <command> [...args]
|
|
220
|
+
lattices capture stop <run-id>
|
|
221
|
+
`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const params: Record<string, unknown> = { source: "cli" };
|
|
226
|
+
const explicitWid = positional[0] ? Number(positional[0]) : NaN;
|
|
227
|
+
if (Number.isFinite(explicitWid)) params.wid = explicitWid;
|
|
228
|
+
const session = parseFlagValue(commandArgs, "session");
|
|
229
|
+
const app = parseFlagValue(commandArgs, "app");
|
|
230
|
+
const title = parseFlagValue(commandArgs, "title");
|
|
231
|
+
const filename = parseFlagValue(commandArgs, "filename");
|
|
232
|
+
const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
|
|
233
|
+
if (session) params.session = session;
|
|
234
|
+
if (app) params.app = app;
|
|
235
|
+
if (title) params.title = title;
|
|
236
|
+
if (filename) params.filename = filename;
|
|
237
|
+
if (runId) params.runId = runId;
|
|
238
|
+
|
|
239
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
240
|
+
const result = await daemonCall("capture.screenshotWindow", params, 20000) as any;
|
|
241
|
+
if (jsonFlag) {
|
|
242
|
+
console.log(JSON.stringify(result, null, 2));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const artifact = result.artifact || {};
|
|
246
|
+
const run = result.run || {};
|
|
247
|
+
const target = result.target || {};
|
|
248
|
+
console.log(`Captured ${target.app || "window"} ${target.wid ? `wid:${target.wid}` : ""}`);
|
|
249
|
+
console.log(` run: ${run.id || "?"}`);
|
|
250
|
+
console.log(` artifact: ${artifact.path || "?"}`);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type DaemonClient = typeof import("../daemon-client.ts");
|
|
2
|
+
|
|
3
|
+
export async function withDaemon<T>(
|
|
4
|
+
fn: (client: DaemonClient) => Promise<T>,
|
|
5
|
+
opts?: { message?: string; exitCode?: number }
|
|
6
|
+
): Promise<T> {
|
|
7
|
+
const message = opts?.message ?? "Daemon not running. Start with: lattices app";
|
|
8
|
+
const exitCode = opts?.exitCode ?? 1;
|
|
9
|
+
|
|
10
|
+
const client = await import("../daemon-client.ts");
|
|
11
|
+
if (!(await client.isDaemonRunning())) {
|
|
12
|
+
console.error(message);
|
|
13
|
+
process.exit(exitCode);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return await fn(client);
|
|
18
|
+
} catch (e: unknown) {
|
|
19
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
20
|
+
process.exit(exitCode);
|
|
21
|
+
}
|
|
22
|
+
}
|