@lattices/cli 0.6.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.
Files changed (44) hide show
  1. package/README.md +13 -4
  2. package/apps/mac/Info.plist +4 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -2
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  6. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +11 -0
  7. package/apps/mac/Lattices.entitlements +6 -0
  8. package/bin/assistant-intelligence.ts +41 -3
  9. package/bin/cli/capture.ts +252 -0
  10. package/bin/cli/daemon.ts +22 -0
  11. package/bin/cli/helpers.ts +105 -0
  12. package/bin/cli/layer.ts +178 -0
  13. package/bin/cli/runs.ts +43 -0
  14. package/bin/cli/search.ts +141 -0
  15. package/bin/cli/session.ts +32 -0
  16. package/bin/client.ts +2 -1
  17. package/bin/cua.ts +26 -0
  18. package/bin/infer.ts +22 -4
  19. package/bin/keychain.ts +75 -0
  20. package/bin/lattices-app.ts +111 -12
  21. package/bin/lattices-build-env.ts +77 -0
  22. package/bin/lattices-dev +29 -2
  23. package/bin/lattices.ts +729 -769
  24. package/docs/api.md +496 -3
  25. package/docs/app.md +5 -4
  26. package/docs/assistant-knowledge.md +130 -0
  27. package/docs/config.md +5 -0
  28. package/docs/hyperspace-grid-snappiness.md +210 -0
  29. package/docs/layers.md +53 -0
  30. package/docs/mouse-gestures.md +40 -3
  31. package/docs/ocr.md +3 -0
  32. package/docs/prompts/hands-off-system.md +9 -1
  33. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  34. package/docs/proposals/{LAT-006-mira-in-lattices.md → LAT-006-runs-and-capture-in-lattices.md} +83 -70
  35. package/docs/quickstart.md +3 -1
  36. package/docs/reference/dewey.config.ts +1 -1
  37. package/docs/release.md +4 -3
  38. package/docs/terminal-kit.md +87 -0
  39. package/docs/tiling-reference.md +5 -3
  40. package/docs/voice.md +3 -3
  41. package/package.json +27 -5
  42. package/packages/npm/sdk/cua.d.mts +1 -0
  43. package/packages/npm/sdk/cua.d.ts +188 -0
  44. 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, tmux sessions, screen text, and layouts. It also gives you
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/cli
34
+ npm install -g @arach/lattices
35
35
  ```
36
36
 
37
- The CLI and app work independently use either or both.
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
 
@@ -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/cli/daemon-client'
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
@@ -26,15 +26,17 @@
26
26
  </dict>
27
27
  </array>
28
28
  <key>CFBundleVersion</key>
29
- <string>0.5.0</string>
29
+ <string>0.6.1</string>
30
30
  <key>CFBundleShortVersionString</key>
31
- <string>0.5.0</string>
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.6.0</string>
29
+ <string>0.6.1</string>
30
30
  <key>CFBundleShortVersionString</key>
31
- <string>0.6.0</string>
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>
@@ -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 or grid:CxR:C,R syntax." },
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:\d+x\d+:\d+,\d+/);
691
- if (grid) return { position: grid[0], phrase: grid[0] };
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
+ }