@lattices/cli 0.4.0 → 0.4.2

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/bin/lattices.ts CHANGED
@@ -56,7 +56,7 @@ function requireTmux(command: string | undefined): void {
56
56
  const isImplicitCreate = command && !tmuxRequiredCommands.has(command)
57
57
  && !["search", "s", "focus", "place", "tile", "t", "windows", "window",
58
58
  "voice", "call", "layer", "layers", "diag", "diagnostics", "scan",
59
- "ocr", "daemon", "dev", "app", "help", "-h", "--help"].includes(command);
59
+ "ocr", "daemon", "dev", "app", "mouse", "help", "-h", "--help"].includes(command);
60
60
 
61
61
  if (command && !tmuxRequiredCommands.has(command) && !isImplicitCreate) return;
62
62
 
@@ -831,6 +831,18 @@ function restartPane(target?: string): void {
831
831
 
832
832
  // ── Daemon-aware commands ────────────────────────────────────────────
833
833
 
834
+ async function mouseCommand(sub?: string): Promise<void> {
835
+ const { daemonCall } = await getDaemonClient();
836
+ if (sub === "summon") {
837
+ const result = await daemonCall("mouse.summon") as any;
838
+ console.log(`🎯 Mouse summoned to (${result.x}, ${result.y})`);
839
+ } else {
840
+ // Default: find
841
+ const result = await daemonCall("mouse.find") as any;
842
+ console.log(`🔍 Mouse at (${result.x}, ${result.y})`);
843
+ }
844
+ }
845
+
834
846
  async function daemonStatusCommand(): Promise<void> {
835
847
  try {
836
848
  const { daemonCall } = await getDaemonClient();
@@ -1189,10 +1201,37 @@ async function callCommand(method?: string, ...rest: string[]): Promise<void> {
1189
1201
  }
1190
1202
  }
1191
1203
 
1192
- async function layerCommand(index?: string): Promise<void> {
1204
+ async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
1193
1205
  try {
1194
1206
  const { daemonCall } = await getDaemonClient();
1195
- if (index === undefined || index === null || index === "") {
1207
+
1208
+ // ── Subcommands ──
1209
+ if (sub === "create") {
1210
+ await layerCreateCommand(rest);
1211
+ return;
1212
+ }
1213
+ if (sub === "snap") {
1214
+ await layerSnapCommand(rest[0]);
1215
+ return;
1216
+ }
1217
+ if (sub === "session" || sub === "sessions") {
1218
+ await layerSessionCommand(rest[0]);
1219
+ return;
1220
+ }
1221
+ if (sub === "clear") {
1222
+ await daemonCall("session.layers.clear");
1223
+ console.log("Cleared all session layers.");
1224
+ return;
1225
+ }
1226
+ if (sub === "delete" || sub === "rm") {
1227
+ if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
1228
+ await daemonCall("session.layers.delete", { name: rest[0] });
1229
+ console.log(`Deleted session layer "${rest[0]}".`);
1230
+ return;
1231
+ }
1232
+
1233
+ // ── List or switch (original behavior) ──
1234
+ if (sub === undefined || sub === null || sub === "") {
1196
1235
  const result = await daemonCall("layers.list") as any;
1197
1236
  if (!result.layers.length) {
1198
1237
  console.log("No layers configured.");
@@ -1205,19 +1244,144 @@ async function layerCommand(index?: string): Promise<void> {
1205
1244
  }
1206
1245
  return;
1207
1246
  }
1208
- const idx = parseInt(index, 10);
1247
+ const idx = parseInt(sub, 10);
1209
1248
  if (!isNaN(idx)) {
1210
1249
  await daemonCall("layer.activate", { index: idx, mode: "launch" });
1211
1250
  console.log(`Activated layer ${idx}`);
1212
1251
  } else {
1213
- await daemonCall("layer.activate", { name: index, mode: "launch" });
1214
- console.log(`Activated layer "${index}"`);
1252
+ await daemonCall("layer.activate", { name: sub, mode: "launch" });
1253
+ console.log(`Activated layer "${sub}"`);
1215
1254
  }
1216
1255
  } catch (e: unknown) {
1217
1256
  console.log(`Error: ${(e as Error).message}`);
1218
1257
  }
1219
1258
  }
1220
1259
 
1260
+ // ── Layer create: build a session layer from window specs ────────────
1261
+ // Usage: lattices layer create <name> [wid:123 wid:456 ...]
1262
+ // lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
1263
+ async function layerCreateCommand(args: string[]): Promise<void> {
1264
+ const { daemonCall } = await getDaemonClient();
1265
+ const name = args[0];
1266
+ if (!name) {
1267
+ console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
1268
+ return;
1269
+ }
1270
+
1271
+ const jsonIdx = args.indexOf("--json");
1272
+ if (jsonIdx !== -1 && args[jsonIdx + 1]) {
1273
+ // JSON mode: parse window specs with tile positions
1274
+ const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
1275
+ wid?: number; app?: string; title?: string; tile?: string;
1276
+ }>;
1277
+
1278
+ // Collect wids, resolve app-based specs
1279
+ const windowIds: number[] = [];
1280
+ const windows: Array<{ app: string; contentHint?: string }> = [];
1281
+ const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
1282
+
1283
+ for (const spec of specs) {
1284
+ if (spec.wid) {
1285
+ windowIds.push(spec.wid);
1286
+ if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
1287
+ } else if (spec.app) {
1288
+ windows.push({ app: spec.app, contentHint: spec.title });
1289
+ if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
1290
+ }
1291
+ }
1292
+
1293
+ const result = await daemonCall("session.layers.create", {
1294
+ name,
1295
+ ...(windowIds.length ? { windowIds } : {}),
1296
+ ...(windows.length ? { windows } : {}),
1297
+ }) as any;
1298
+
1299
+ console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
1300
+
1301
+ // Apply tile positions
1302
+ for (const t of tiles) {
1303
+ try {
1304
+ await daemonCall("window.place", {
1305
+ ...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
1306
+ placement: t.tile,
1307
+ });
1308
+ } catch { /* window may not be resolved yet */ }
1309
+ }
1310
+
1311
+ if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
1312
+ return;
1313
+ }
1314
+
1315
+ // Simple wid mode: lattices layer create <name> wid:123 wid:456
1316
+ const wids = args.slice(1)
1317
+ .filter(a => a.startsWith("wid:"))
1318
+ .map(a => parseInt(a.slice(4), 10))
1319
+ .filter(n => !isNaN(n));
1320
+
1321
+ const result = await daemonCall("session.layers.create", {
1322
+ name,
1323
+ ...(wids.length ? { windowIds: wids } : {}),
1324
+ }) as any;
1325
+
1326
+ console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
1327
+ }
1328
+
1329
+ // ── Layer snap: snapshot current visible windows into a session layer ─
1330
+ async function layerSnapCommand(name?: string): Promise<void> {
1331
+ const { daemonCall } = await getDaemonClient();
1332
+ const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
1333
+
1334
+ // Get all current windows
1335
+ const windows = await daemonCall("windows.list") as any[];
1336
+ const visibleWids = windows
1337
+ .filter((w: any) => !w.isMinimized && w.app !== "lattices")
1338
+ .map((w: any) => w.wid);
1339
+
1340
+ if (!visibleWids.length) {
1341
+ console.log("No visible windows to snapshot.");
1342
+ return;
1343
+ }
1344
+
1345
+ await daemonCall("session.layers.create", {
1346
+ name: layerName,
1347
+ windowIds: visibleWids,
1348
+ });
1349
+
1350
+ console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
1351
+ }
1352
+
1353
+ // ── Layer session: list or switch session layers ─────────────────────
1354
+ async function layerSessionCommand(nameOrIndex?: string): Promise<void> {
1355
+ const { daemonCall } = await getDaemonClient();
1356
+ const result = await daemonCall("session.layers.list") as any;
1357
+
1358
+ if (!nameOrIndex) {
1359
+ // List session layers
1360
+ if (!result.layers.length) {
1361
+ console.log("No session layers. Create one with: lattices layer create <name>");
1362
+ return;
1363
+ }
1364
+ console.log("Session layers:\n");
1365
+ for (let i = 0; i < result.layers.length; i++) {
1366
+ const l = result.layers[i];
1367
+ const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
1368
+ const winCount = l.windows?.length || 0;
1369
+ console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
1370
+ }
1371
+ return;
1372
+ }
1373
+
1374
+ // Switch by index or name
1375
+ const idx = parseInt(nameOrIndex, 10);
1376
+ if (!isNaN(idx)) {
1377
+ await daemonCall("session.layers.switch", { index: idx });
1378
+ console.log(`Switched to session layer ${idx}.`);
1379
+ } else {
1380
+ await daemonCall("session.layers.switch", { name: nameOrIndex });
1381
+ console.log(`Switched to session layer "${nameOrIndex}".`);
1382
+ }
1383
+ }
1384
+
1221
1385
  async function diagCommand(limit?: string): Promise<void> {
1222
1386
  try {
1223
1387
  const { daemonCall } = await getDaemonClient();
@@ -1549,6 +1713,11 @@ Usage:
1549
1713
  lattices tile <position> Tile the frontmost window (left, right, top, etc.)
1550
1714
  lattices distribute Smart-grid all visible windows (daemon required)
1551
1715
  lattices layer [name|index] List layers or switch by name/index (daemon required)
1716
+ lattices layer create <name> [wid:N ...] [--json '<specs>'] Create a session layer
1717
+ lattices layer snap [name] Snapshot visible windows into a session layer
1718
+ lattices layer session [n] List or switch session layers (runtime, no restart)
1719
+ lattices layer delete <name> Delete a session layer
1720
+ lattices layer clear Clear all session layers
1552
1721
  lattices voice status Voice provider status
1553
1722
  lattices voice simulate <t> Parse and execute a voice command
1554
1723
  lattices voice intents List all available intents
@@ -1563,6 +1732,8 @@ Usage:
1563
1732
  lattices dev build Build the project (swift/node/rust/go/make)
1564
1733
  lattices dev restart Build + restart (swift app) or just build
1565
1734
  lattices dev type Print detected project type
1735
+ lattices mouse Find mouse — sonar pulse at cursor position
1736
+ lattices mouse summon Summon mouse to screen center
1566
1737
  lattices daemon status Show daemon status
1567
1738
  lattices diag [limit] Show diagnostic log entries
1568
1739
  lattices app Launch the menu bar companion app
@@ -1975,7 +2146,7 @@ switch (command) {
1975
2146
  break;
1976
2147
  case "layer":
1977
2148
  case "layers":
1978
- await layerCommand(args[1]);
2149
+ await layerCommand(args[1], ...args.slice(2));
1979
2150
  break;
1980
2151
  case "diag":
1981
2152
  case "diagnostics":
@@ -1985,6 +2156,9 @@ switch (command) {
1985
2156
  case "ocr":
1986
2157
  await scanCommand(args[1], ...args.slice(2));
1987
2158
  break;
2159
+ case "mouse":
2160
+ await mouseCommand(args[1]);
2161
+ break;
1988
2162
  case "daemon":
1989
2163
  if (args[1] === "status") {
1990
2164
  await daemonStatusCommand();
@@ -0,0 +1,207 @@
1
+ # Agent Guide: Generating Layers
2
+
3
+ How to create and manage Lattices workspace layers programmatically. This guide is for AI agents (Claude Code, etc.) that want to generate layers from high-level user descriptions.
4
+
5
+ ## Quick Reference
6
+
7
+ ```bash
8
+ # See what's on screen
9
+ lattices windows --json
10
+
11
+ # Create a layer with tiling
12
+ lattices layer create "Design" --json '[
13
+ {"app": "Figma", "tile": "left"},
14
+ {"app": "Google Chrome", "title": "Tailwind", "tile": "right"}
15
+ ]'
16
+
17
+ # Snapshot current windows as a layer
18
+ lattices layer snap "my-context"
19
+
20
+ # List / switch / delete session layers
21
+ lattices layer session
22
+ lattices layer session "Design"
23
+ lattices layer delete "Design"
24
+ lattices layer clear
25
+ ```
26
+
27
+ ## How It Works
28
+
29
+ There are two kinds of layers:
30
+
31
+ | Type | Storage | Requires restart? | How to create |
32
+ |------|---------|-------------------|---------------|
33
+ | **Config layers** | `~/.lattices/workspace.json` | Yes (or refresh) | Edit JSON file |
34
+ | **Session layers** | In-memory (daemon) | No | CLI or daemon API |
35
+
36
+ **Session layers are what you want.** They're created via TypeScript CLI commands, take effect immediately, and don't require restarting anything.
37
+
38
+ ## Step-by-Step: Generating a Layer
39
+
40
+ ### 1. Discover what's available
41
+
42
+ ```bash
43
+ lattices windows --json
44
+ ```
45
+
46
+ Returns an array of window objects:
47
+ ```json
48
+ [
49
+ {
50
+ "wid": 1234,
51
+ "app": "iTerm2",
52
+ "title": "lattices — zsh",
53
+ "latticesSession": "lattices-abc123",
54
+ "frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
55
+ "spaceIds": [1]
56
+ },
57
+ {
58
+ "wid": 5678,
59
+ "app": "Google Chrome",
60
+ "title": "GitHub - arach/lattices",
61
+ "frame": { "x": 960, "y": 25, "w": 960, "h": 1050 },
62
+ "spaceIds": [1]
63
+ }
64
+ ]
65
+ ```
66
+
67
+ Key fields for matching:
68
+ - `wid` — unique window ID (most precise)
69
+ - `app` — application name
70
+ - `title` — window title (use for disambiguation when multiple windows of same app)
71
+ - `latticesSession` — tmux session name (for terminal windows)
72
+
73
+ ### 2. Decide on a layout
74
+
75
+ Pick tile positions based on how many windows and what makes sense:
76
+
77
+ | Windows | Good layout | Tile values |
78
+ |---------|-------------|-------------|
79
+ | 2 | Side by side | `left`, `right` |
80
+ | 2 | Stacked | `top`, `bottom` |
81
+ | 3 | Main + sidebar | `left` (60%), `top-right`, `bottom-right` |
82
+ | 3 | Columns | `left-third`, `center-third`, `right-third` |
83
+ | 4 | Quadrants | `top-left`, `top-right`, `bottom-left`, `bottom-right` |
84
+ | 1 | Focused | `maximize` or `center` |
85
+
86
+ Full position reference:
87
+ - **Halves**: `left`, `right`, `top`, `bottom`
88
+ - **Quarters**: `top-left`, `top-right`, `bottom-left`, `bottom-right`
89
+ - **Thirds**: `left-third`, `center-third`, `right-third`
90
+ - **Sixths**: `top-left-third`, `top-center-third`, `top-right-third`, `bottom-left-third`, `bottom-center-third`, `bottom-right-third`
91
+ - **Fourths**: `first-fourth`, `second-fourth`, `third-fourth`, `last-fourth`
92
+ - **Special**: `maximize`, `center`
93
+ - **Custom grid**: `grid:CxR:C,R` (e.g. `grid:5x3:2,1`)
94
+
95
+ ### 3. Create the layer
96
+
97
+ **Option A: By window ID (most reliable)**
98
+ ```bash
99
+ lattices layer create "Coding" --json '[
100
+ {"wid": 1234, "tile": "left"},
101
+ {"wid": 5678, "tile": "right"}
102
+ ]'
103
+ ```
104
+
105
+ **Option B: By app name (survives window recreation)**
106
+ ```bash
107
+ lattices layer create "Research" --json '[
108
+ {"app": "Google Chrome", "title": "docs", "tile": "left"},
109
+ {"app": "Notes", "tile": "right"}
110
+ ]'
111
+ ```
112
+
113
+ **Option C: Simple wid list (no tiling)**
114
+ ```bash
115
+ lattices layer create "Focus" wid:1234 wid:5678
116
+ ```
117
+
118
+ **Option D: Snapshot everything visible**
119
+ ```bash
120
+ lattices layer snap "Current Context"
121
+ ```
122
+
123
+ ### 4. Switch between layers
124
+
125
+ ```bash
126
+ lattices layer session # list all session layers
127
+ lattices layer session "Coding" # switch to "Coding"
128
+ lattices layer session 0 # switch by index
129
+ ```
130
+
131
+ ## Daemon API (Advanced)
132
+
133
+ For finer control, use raw daemon calls:
134
+
135
+ ```bash
136
+ # Create layer with window IDs
137
+ lattices call session.layers.create '{"name":"Coding","windowIds":[1234,5678]}'
138
+
139
+ # Create layer with app references
140
+ lattices call session.layers.create '{"name":"Design","windows":[{"app":"Figma"},{"app":"Google Chrome","contentHint":"Tailwind"}]}'
141
+
142
+ # Tile a specific window
143
+ lattices call window.place '{"wid":1234,"placement":"left"}'
144
+
145
+ # Switch layer
146
+ lattices call session.layers.switch '{"name":"Coding"}'
147
+
148
+ # List session layers
149
+ lattices call session.layers.list
150
+
151
+ # Delete
152
+ lattices call session.layers.delete '{"name":"old-layer"}'
153
+ ```
154
+
155
+ ## Composing Layers from Intent
156
+
157
+ When a user says something high-level, here's how to think about it:
158
+
159
+ ### "Make me a coding layer"
160
+ 1. Find terminal windows (iTerm2, Terminal, Warp, etc.)
161
+ 2. Find browser windows with dev-related titles (GitHub, docs, localhost)
162
+ 3. Main editor/terminal on `left`, reference material on `right`
163
+
164
+ ### "Set up a design layer"
165
+ 1. Find design tools (Figma, Sketch, Adobe XD)
166
+ 2. Find browser windows with design references
167
+ 3. Design tool `left` (or `maximize`), references `right`
168
+
169
+ ### "Create a writing layer"
170
+ 1. Find text editors, notes apps (Notes, Obsidian, iA Writer, VS Code with .md)
171
+ 2. Find research/reference windows
172
+ 3. Writing app `left` or `center`, references `right`
173
+
174
+ ### "Give me a communication layer"
175
+ 1. Find messaging apps (Slack, Discord, Messages)
176
+ 2. Find email (Mail, Gmail in browser)
177
+ 3. Arrange by priority — primary tool `left`, secondary `right`
178
+
179
+ ### "Split my work into layers by project"
180
+ 1. Group windows by project (match on title keywords, session names, or app)
181
+ 2. Create one layer per project group
182
+ 3. Use the 3-window layout pattern: main `left`, support `top-right`, `bottom-right`
183
+
184
+ ## App Grouping Heuristics
185
+
186
+ When deciding which windows go together:
187
+
188
+ | Category | Common apps | Goes well with |
189
+ |----------|-------------|----------------|
190
+ | **Code** | iTerm2, Terminal, VS Code, Xcode | Chrome (docs/GitHub), Simulator |
191
+ | **Design** | Figma, Sketch, Pixelmator | Chrome (design systems), Preview |
192
+ | **Writing** | Notes, Obsidian, iA Writer | Chrome (research), Preview |
193
+ | **Communication** | Slack, Discord, Messages, Mail | Calendar, Notes |
194
+ | **Media** | Spotify, Music, Podcasts | (background, no tile needed) |
195
+ | **Reference** | Chrome, Safari, Preview, Finder | (depends on content) |
196
+
197
+ Browser windows are chameleons — use `title` matching to assign them to the right layer based on their content.
198
+
199
+ ## Tips
200
+
201
+ - Prefer `wid` when the windows are already open — it's unambiguous.
202
+ - Use `app` + `title` when you want the layer to survive window restarts.
203
+ - Don't put more than 4-5 windows in a single layer — it gets cramped.
204
+ - Background apps (music, etc.) usually don't need to be in any layer.
205
+ - The `snap` command is great for "save what I have now" scenarios.
206
+ - Session layers are ephemeral — they live until the daemon restarts. For permanent layers, edit `~/.lattices/workspace.json`.
207
+ - You can create multiple layers in sequence, then switch between them with `lattices layer session <name>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lattices/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Agentic window manager for macOS — programmable workspace, smart layouts, managed tmux sessions, and a 35+-method agent API",
5
5
  "bin": {
6
6
  "lattices": "./bin/lattices.ts",
@@ -27,15 +27,22 @@
27
27
  },
28
28
  "scripts": {
29
29
  "dev": "bun --cwd docs-site dev",
30
- "typecheck": "tsc --noEmit"
30
+ "typecheck": "tsc --noEmit",
31
+ "build:app-bundle": "bash ./bin/lattices-dev build",
32
+ "prepack": "bash ./bin/lattices-dev build"
31
33
  },
32
34
  "type": "module",
33
35
  "os": ["darwin"],
34
36
  "files": [
35
37
  "bin",
38
+ "app/Info.plist",
39
+ "app/Lattices.app",
40
+ "app/Lattices.entitlements",
36
41
  "app/Package.swift",
42
+ "app/Resources",
37
43
  "app/Sources",
38
- "app/Lattices.app/Contents/Info.plist",
44
+ "app/Tests",
45
+ "assets/AppIcon.icns",
39
46
  "docs"
40
47
  ],
41
48
  "devDependencies": {
@@ -48,6 +55,7 @@
48
55
  "@ai-sdk/openai": "^3.0.41",
49
56
  "@ai-sdk/xai": "^3.0.67",
50
57
  "@arach/speakeasy": "^0.2.8",
51
- "ai": "^6.0.116"
58
+ "ai": "^6.0.116",
59
+ "zod": "^3.25.76"
52
60
  }
53
61
  }