@lattices/cli 0.3.0 → 0.4.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 (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
@@ -1,32 +1,37 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
 
3
3
  import { createHash } from "node:crypto";
4
4
  import { execSync } from "node:child_process";
5
5
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
- import { basename, resolve, dirname } from "node:path";
6
+ import { basename, resolve } from "node:path";
7
7
  import { homedir } from "node:os";
8
- import { fileURLToPath } from "node:url";
9
8
 
10
9
  // Daemon client (lazy-loaded to avoid blocking startup for TTY commands)
11
- let _daemonClient;
12
- async function getDaemonClient() {
10
+ let _daemonClient: typeof import("./daemon-client.ts") | undefined;
11
+ async function getDaemonClient(): Promise<typeof import("./daemon-client.ts")> {
13
12
  if (!_daemonClient) {
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- _daemonClient = await import(resolve(__dirname, "daemon-client.js"));
13
+ _daemonClient = await import("./daemon-client.ts");
16
14
  }
17
15
  return _daemonClient;
18
16
  }
19
17
 
20
- const args = process.argv.slice(2);
21
- const command = args[0];
18
+ const args: string[] = process.argv.slice(2);
19
+ const command: string | undefined = args[0];
22
20
 
23
21
  // ── Helpers ──────────────────────────────────────────────────────────
24
22
 
25
- function run(cmd, opts = {}) {
26
- return execSync(cmd, { encoding: "utf8", ...opts }).trim();
23
+ interface ExecOpts {
24
+ encoding?: string;
25
+ stdio?: string | string[];
26
+ cwd?: string;
27
+ [key: string]: any;
27
28
  }
28
29
 
29
- function runQuiet(cmd) {
30
+ function run(cmd: string, opts: ExecOpts = {}): string {
31
+ return execSync(cmd, { encoding: "utf8", ...opts } as any).trim();
32
+ }
33
+
34
+ function runQuiet(cmd: string): string | null {
30
35
  try {
31
36
  return run(cmd, { stdio: "pipe" });
32
37
  } catch {
@@ -34,77 +39,117 @@ function runQuiet(cmd) {
34
39
  }
35
40
  }
36
41
 
37
- function hasTmux() {
42
+ function hasTmux(): boolean {
38
43
  return runQuiet("which tmux") !== null;
39
44
  }
40
45
 
41
- function isInsideTmux() {
46
+ /** Commands that require tmux to be installed */
47
+ const tmuxRequiredCommands = new Set([
48
+ "init", "ls", "list", "kill", "rm", "sync", "reconcile",
49
+ "restart", "respawn", "group", "groups", "tab", "status",
50
+ "inventory", "distribute", "sessions",
51
+ ]);
52
+
53
+ function requireTmux(command: string | undefined): void {
54
+ if (hasTmux()) return;
55
+
56
+ const isImplicitCreate = command && !tmuxRequiredCommands.has(command)
57
+ && !["search", "s", "focus", "place", "tile", "t", "windows", "window",
58
+ "voice", "call", "layer", "layers", "diag", "diagnostics", "scan",
59
+ "ocr", "daemon", "dev", "app", "mouse", "help", "-h", "--help"].includes(command);
60
+
61
+ if (command && !tmuxRequiredCommands.has(command) && !isImplicitCreate) return;
62
+
63
+ console.error(`
64
+ \x1b[1;31m✘ tmux not found\x1b[0m
65
+
66
+ Lattices uses tmux for terminal session management.
67
+ Install it with Homebrew:
68
+
69
+ \x1b[1mbrew install tmux\x1b[0m
70
+
71
+ If tmux is installed somewhere else, make sure it's on your PATH:
72
+
73
+ \x1b[90mexport PATH="/path/to/tmux/bin:$PATH"\x1b[0m
74
+
75
+ Then run this command again.
76
+ `.trim());
77
+ process.exit(1);
78
+ }
79
+
80
+ function isInsideTmux(): boolean {
42
81
  return !!process.env.TMUX;
43
82
  }
44
83
 
45
- function sessionExists(name) {
84
+ function sessionExists(name: string): boolean {
46
85
  return runQuiet(`tmux has-session -t "${name}" 2>&1`) !== null;
47
86
  }
48
87
 
49
- function pathHash(dir) {
88
+ function pathHash(dir: string): string {
50
89
  return createHash("sha256").update(resolve(dir)).digest("hex").slice(0, 6);
51
90
  }
52
91
 
53
- function toSessionName(dir) {
92
+ function toSessionName(dir: string): string {
54
93
  const base = basename(dir).replace(/[^a-zA-Z0-9_-]/g, "-");
55
94
  return `${base}-${pathHash(dir)}`;
56
95
  }
57
96
 
58
- function esc(str) {
97
+ function esc(str: string): string {
59
98
  return str.replace(/'/g, "'\\''");
60
99
  }
61
100
 
62
101
  // ── Config ───────────────────────────────────────────────────────────
63
102
 
64
- function readConfig(dir) {
103
+ function readConfig(dir: string): any | null {
65
104
  const configPath = resolve(dir, ".lattices.json");
66
105
  if (!existsSync(configPath)) return null;
67
106
  try {
68
107
  const raw = readFileSync(configPath, "utf8");
69
108
  return JSON.parse(raw);
70
- } catch (e) {
71
- console.warn(`Warning: invalid .lattices.json — ${e.message}`);
109
+ } catch (e: unknown) {
110
+ console.warn(`Warning: invalid .lattices.json — ${(e as Error).message}`);
72
111
  return null;
73
112
  }
74
113
  }
75
114
 
76
115
  // ── Workspace config (tab groups) ───────────────────────────────────
77
116
 
78
- function readWorkspaceConfig() {
117
+ function readWorkspaceConfig(): any | null {
79
118
  const configPath = resolve(homedir(), ".lattices", "workspace.json");
80
119
  if (!existsSync(configPath)) return null;
81
120
  try {
82
121
  const raw = readFileSync(configPath, "utf8");
83
122
  return JSON.parse(raw);
84
- } catch (e) {
85
- console.warn(`Warning: invalid workspace.json — ${e.message}`);
123
+ } catch (e: unknown) {
124
+ console.warn(`Warning: invalid workspace.json — ${(e as Error).message}`);
86
125
  return null;
87
126
  }
88
127
  }
89
128
 
90
- function toGroupSessionName(groupId) {
129
+ function toGroupSessionName(groupId: string): string {
91
130
  return `lattices-group-${groupId}`;
92
131
  }
93
132
 
94
133
  /** Get ordered pane IDs for a specific window within a session */
95
- function getPaneIdsForWindow(sessionName, windowIndex) {
134
+ function getPaneIdsForWindow(sessionName: string, windowIndex: number): string[] {
96
135
  const out = runQuiet(
97
136
  `tmux list-panes -t "${sessionName}:${windowIndex}" -F "#{pane_id}"`
98
137
  );
99
138
  return out ? out.split("\n").filter(Boolean) : [];
100
139
  }
101
140
 
141
+ interface PaneConfig {
142
+ name?: string;
143
+ cmd?: string;
144
+ size?: number;
145
+ }
146
+
102
147
  /** Create a tmux window with pane layout for a project dir */
103
- function createWindowForProject(sessionName, windowIndex, dir, label) {
148
+ function createWindowForProject(sessionName: string, windowIndex: number, dir: string, label?: string): void {
104
149
  const config = readConfig(dir);
105
150
  const d = esc(dir);
106
151
 
107
- let panes;
152
+ let panes: PaneConfig[];
108
153
  if (config?.panes?.length) {
109
154
  panes = resolvePane(config.panes, dir);
110
155
  } else {
@@ -141,7 +186,7 @@ function createWindowForProject(sessionName, windowIndex, dir, label) {
141
186
  const paneIds = getPaneIdsForWindow(sessionName, windowIndex);
142
187
  for (let i = 0; i < panes.length && i < paneIds.length; i++) {
143
188
  if (panes[i].cmd) {
144
- run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}' Enter`);
189
+ run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
145
190
  }
146
191
  if (panes[i].name) {
147
192
  runQuiet(`tmux select-pane -t "${paneIds[i]}" -T "${panes[i].name}"`);
@@ -154,8 +199,19 @@ function createWindowForProject(sessionName, windowIndex, dir, label) {
154
199
  }
155
200
  }
156
201
 
202
+ interface TabConfig {
203
+ path: string;
204
+ label?: string;
205
+ }
206
+
207
+ interface GroupConfig {
208
+ id: string;
209
+ label?: string;
210
+ tabs?: TabConfig[];
211
+ }
212
+
157
213
  /** Create a group session with one tmux window per tab */
158
- function createGroupSession(group) {
214
+ function createGroupSession(group: GroupConfig): string | null {
159
215
  const name = toGroupSessionName(group.id);
160
216
  const tabs = group.tabs || [];
161
217
 
@@ -194,7 +250,7 @@ function createGroupSession(group) {
194
250
  return name;
195
251
  }
196
252
 
197
- function listGroups() {
253
+ function listGroups(): void {
198
254
  const ws = readWorkspaceConfig();
199
255
  if (!ws?.groups?.length) {
200
256
  console.log("No tab groups configured in ~/.lattices/workspace.json");
@@ -204,12 +260,12 @@ function listGroups() {
204
260
  console.log("Tab Groups:\n");
205
261
  for (const group of ws.groups) {
206
262
  const tabs = group.tabs || [];
207
- const runningCount = tabs.filter((t) => sessionExists(toSessionName(resolve(t.path)))).length;
263
+ const runningCount = tabs.filter((t: TabConfig) => sessionExists(toSessionName(resolve(t.path)))).length;
208
264
  const running = runningCount > 0;
209
265
  const status = running
210
266
  ? `\x1b[32m● ${runningCount}/${tabs.length} running\x1b[0m`
211
267
  : "\x1b[90m○ stopped\x1b[0m";
212
- const tabLabels = tabs.map((t) => t.label || basename(t.path)).join(", ");
268
+ const tabLabels = tabs.map((t: TabConfig) => t.label || basename(t.path)).join(", ");
213
269
  console.log(` ${group.label || group.id} ${status}`);
214
270
  console.log(` id: ${group.id}`);
215
271
  console.log(` tabs: ${tabLabels}`);
@@ -217,7 +273,7 @@ function listGroups() {
217
273
  }
218
274
  }
219
275
 
220
- function groupCommand(id) {
276
+ function groupCommand(id?: string): void {
221
277
  const ws = readWorkspaceConfig();
222
278
  if (!ws?.groups?.length) {
223
279
  console.log("No tab groups configured in ~/.lattices/workspace.json");
@@ -229,9 +285,9 @@ function groupCommand(id) {
229
285
  return;
230
286
  }
231
287
 
232
- const group = ws.groups.find((g) => g.id === id);
288
+ const group = ws.groups.find((g: GroupConfig) => g.id === id);
233
289
  if (!group) {
234
- console.log(`No group "${id}". Available: ${ws.groups.map((g) => g.id).join(", ")}`);
290
+ console.log(`No group "${id}". Available: ${ws.groups.map((g: GroupConfig) => g.id).join(", ")}`);
235
291
  return;
236
292
  }
237
293
 
@@ -267,7 +323,7 @@ function groupCommand(id) {
267
323
  attach(firstName);
268
324
  }
269
325
 
270
- function tabCommand(groupId, tabName) {
326
+ function tabCommand(groupId?: string, tabName?: string): void {
271
327
  if (!groupId) {
272
328
  console.log("Usage: lattices tab <group-id> <tab-name|index>");
273
329
  return;
@@ -279,13 +335,13 @@ function tabCommand(groupId, tabName) {
279
335
  return;
280
336
  }
281
337
 
282
- const group = ws.groups.find((g) => g.id === groupId);
338
+ const group = ws.groups.find((g: GroupConfig) => g.id === groupId);
283
339
  if (!group) {
284
340
  console.log(`No group "${groupId}".`);
285
341
  return;
286
342
  }
287
343
 
288
- const tabs = group.tabs || [];
344
+ const tabs: TabConfig[] = group.tabs || [];
289
345
 
290
346
  if (!tabName) {
291
347
  // List tabs with their session status
@@ -301,7 +357,7 @@ function tabCommand(groupId, tabName) {
301
357
  }
302
358
 
303
359
  // Resolve tab target to an index
304
- let tabIdx;
360
+ let tabIdx: number;
305
361
  if (/^\d+$/.test(tabName)) {
306
362
  tabIdx = parseInt(tabName, 10);
307
363
  } else {
@@ -337,7 +393,7 @@ function tabCommand(groupId, tabName) {
337
393
 
338
394
  // ── Detect dev command ───────────────────────────────────────────────
339
395
 
340
- function detectPackageManager(dir) {
396
+ function detectPackageManager(dir: string): string {
341
397
  if (existsSync(resolve(dir, "pnpm-lock.yaml"))) return "pnpm";
342
398
  if (existsSync(resolve(dir, "bun.lockb")) || existsSync(resolve(dir, "bun.lock")))
343
399
  return "bun";
@@ -345,11 +401,11 @@ function detectPackageManager(dir) {
345
401
  return "npm";
346
402
  }
347
403
 
348
- function detectDevCommand(dir) {
404
+ function detectDevCommand(dir: string): string | null {
349
405
  const pkgPath = resolve(dir, "package.json");
350
406
  if (!existsSync(pkgPath)) return null;
351
407
 
352
- let pkg;
408
+ let pkg: any;
353
409
  try {
354
410
  pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
355
411
  } catch {
@@ -358,19 +414,19 @@ function detectDevCommand(dir) {
358
414
 
359
415
  const scripts = pkg.scripts || {};
360
416
  const pm = detectPackageManager(dir);
361
- const run = pm === "npm" ? "npm run" : pm;
417
+ const runCmd = pm === "npm" ? "npm run" : pm;
362
418
 
363
- if (scripts.dev) return `${run} dev`;
364
- if (scripts.start) return `${run} start`;
365
- if (scripts.serve) return `${run} serve`;
366
- if (scripts.watch) return `${run} watch`;
419
+ if (scripts.dev) return `${runCmd} dev`;
420
+ if (scripts.start) return `${runCmd} start`;
421
+ if (scripts.serve) return `${runCmd} serve`;
422
+ if (scripts.watch) return `${runCmd} watch`;
367
423
  return null;
368
424
  }
369
425
 
370
426
  // ── Session creation ─────────────────────────────────────────────────
371
427
 
372
- function resolvePane(panes, dir) {
373
- return panes.map((p) => ({
428
+ function resolvePane(panes: any[], dir: string): PaneConfig[] {
429
+ return panes.map((p: any) => ({
374
430
  name: p.name || "",
375
431
  cmd: p.cmd || undefined,
376
432
  size: p.size || undefined,
@@ -378,19 +434,19 @@ function resolvePane(panes, dir) {
378
434
  }
379
435
 
380
436
  /** Get ordered pane IDs (e.g. ["%0", "%1"]) for a session */
381
- function getPaneIds(name) {
437
+ function getPaneIds(name: string): string[] {
382
438
  const out = runQuiet(
383
439
  `tmux list-panes -t "${name}" -F "#{pane_id}"`
384
440
  );
385
441
  return out ? out.split("\n").filter(Boolean) : [];
386
442
  }
387
443
 
388
- function createSession(dir) {
444
+ function createSession(dir: string): string {
389
445
  const name = toSessionName(dir);
390
446
  const config = readConfig(dir);
391
447
  const d = esc(dir);
392
448
 
393
- let panes;
449
+ let panes: PaneConfig[];
394
450
  if (config?.panes?.length) {
395
451
  panes = resolvePane(config.panes, dir);
396
452
  console.log(`Using .lattices.json (${panes.length} panes)`);
@@ -425,7 +481,7 @@ function createSession(dir) {
425
481
  // Send commands and name each pane
426
482
  for (let i = 0; i < panes.length && i < paneIds.length; i++) {
427
483
  if (panes[i].cmd) {
428
- run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}' Enter`);
484
+ run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
429
485
  }
430
486
  if (panes[i].name) {
431
487
  runQuiet(`tmux select-pane -t "${paneIds[i]}" -T "${panes[i].name}"`);
@@ -449,9 +505,9 @@ function createSession(dir) {
449
505
  /** Check each pane and prefill or restart commands that have exited.
450
506
  * mode: "prefill" types the command without pressing Enter
451
507
  * mode: "ensure" types the command and presses Enter */
452
- function restoreCommands(name, dir, mode) {
508
+ function restoreCommands(name: string, dir: string, mode: "prefill" | "ensure"): void {
453
509
  const config = readConfig(dir);
454
- let panes;
510
+ let panes: PaneConfig[];
455
511
  if (config?.panes?.length) {
456
512
  panes = resolvePane(config.panes, dir);
457
513
  } else {
@@ -469,9 +525,9 @@ function restoreCommands(name, dir, mode) {
469
525
  );
470
526
  if (cur && shells.has(cur)) {
471
527
  if (mode === "ensure") {
472
- run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}' Enter`);
528
+ run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
473
529
  } else {
474
- run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}'`);
530
+ run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}'`);
475
531
  }
476
532
  count++;
477
533
  }
@@ -484,7 +540,7 @@ function restoreCommands(name, dir, mode) {
484
540
 
485
541
  // ── Sync / reconcile ────────────────────────────────────────────────
486
542
 
487
- function resolvePanes(dir) {
543
+ function resolvePanes(dir: string): PaneConfig[] {
488
544
  const config = readConfig(dir);
489
545
  if (config?.panes?.length) {
490
546
  return resolvePane(config.panes, dir);
@@ -492,7 +548,117 @@ function resolvePanes(dir) {
492
548
  return defaultPanes(dir);
493
549
  }
494
550
 
495
- function defaultPanes(dir) {
551
+ // ── Dev command ──────────────────────────────────────────────────────
552
+
553
+ function detectProjectType(dir: string): string | null {
554
+ // Check for lattices-style hybrid project (Swift app + Node CLI)
555
+ if (existsSync(resolve(dir, "app/Package.swift")) && existsSync(resolve(dir, "bin/lattices-app.ts")))
556
+ return "lattices-app";
557
+ if (existsSync(resolve(dir, "Package.swift"))) return "swift";
558
+ if (existsSync(resolve(dir, "Cargo.toml"))) return "rust";
559
+ if (existsSync(resolve(dir, "go.mod"))) return "go";
560
+ if (existsSync(resolve(dir, "package.json"))) return "node";
561
+ if (existsSync(resolve(dir, "Makefile"))) return "make";
562
+ return null;
563
+ }
564
+
565
+ async function devCommand(sub?: string, ...flags: string[]): Promise<void> {
566
+ const dir = process.cwd();
567
+ const type = detectProjectType(dir);
568
+
569
+ // Helper to forward to lattices-app.ts
570
+ async function forwardToAppScript(cmd: string, extraFlags: string[] = []): Promise<void> {
571
+ const appScript = resolve(import.meta.dir, "lattices-app.ts");
572
+ const { execFileSync } = await import("node:child_process");
573
+ try {
574
+ execFileSync("bun", [appScript, cmd, ...extraFlags], { stdio: "inherit" });
575
+ } catch { /* exit code forwarded */ }
576
+ }
577
+
578
+ if (!sub) {
579
+ // bare `lattices dev` — run dev server
580
+ if (!type) {
581
+ console.log("No recognized project in current directory.");
582
+ return;
583
+ }
584
+ console.log(`Detected: ${type} project`);
585
+ if (type === "lattices-app") {
586
+ await forwardToAppScript("restart", flags);
587
+ } else if (type === "node") {
588
+ const cmd = detectDevCommand(dir);
589
+ if (cmd) {
590
+ console.log(`Running: ${cmd}`);
591
+ execSync(cmd, { cwd: dir, stdio: "inherit" });
592
+ } else {
593
+ console.log("No dev script found in package.json.");
594
+ }
595
+ } else if (type === "swift") {
596
+ console.log("Running: swift run");
597
+ execSync("swift run", { cwd: dir, stdio: "inherit" });
598
+ } else if (type === "rust") {
599
+ console.log("Running: cargo run");
600
+ execSync("cargo run", { cwd: dir, stdio: "inherit" });
601
+ } else if (type === "go") {
602
+ console.log("Running: go run .");
603
+ execSync("go run .", { cwd: dir, stdio: "inherit" });
604
+ } else if (type === "make") {
605
+ execSync("make", { cwd: dir, stdio: "inherit" });
606
+ }
607
+ return;
608
+ }
609
+
610
+ if (sub === "build") {
611
+ if (!type) {
612
+ console.log("No recognized project in current directory.");
613
+ return;
614
+ }
615
+ if (type === "lattices-app") {
616
+ await forwardToAppScript("build");
617
+ } else if (type === "swift") {
618
+ console.log("Building: swift build -c release");
619
+ execSync("swift build -c release", { cwd: dir, stdio: "inherit" });
620
+ } else if (type === "node") {
621
+ const pm = detectPackageManager(dir);
622
+ const runCmd = pm === "npm" ? "npm run" : pm;
623
+ const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8"));
624
+ if (pkg.scripts?.build) {
625
+ console.log(`Running: ${runCmd} build`);
626
+ execSync(`${runCmd} build`, { cwd: dir, stdio: "inherit" });
627
+ } else {
628
+ console.log("No build script found in package.json.");
629
+ }
630
+ } else if (type === "rust") {
631
+ console.log("Building: cargo build --release");
632
+ execSync("cargo build --release", { cwd: dir, stdio: "inherit" });
633
+ } else if (type === "go") {
634
+ console.log("Building: go build .");
635
+ execSync("go build .", { cwd: dir, stdio: "inherit" });
636
+ } else if (type === "make") {
637
+ execSync("make", { cwd: dir, stdio: "inherit" });
638
+ }
639
+ return;
640
+ }
641
+
642
+ if (sub === "restart") {
643
+ if (type === "lattices-app") {
644
+ await forwardToAppScript("restart", flags);
645
+ } else {
646
+ // For other project types, just rebuild
647
+ await devCommand("build");
648
+ }
649
+ return;
650
+ }
651
+
652
+ if (sub === "type") {
653
+ console.log(type || "unknown");
654
+ return;
655
+ }
656
+
657
+ console.log(`Unknown dev subcommand: ${sub}`);
658
+ console.log("Usage: lattices dev [build|restart|type]");
659
+ }
660
+
661
+ function defaultPanes(dir: string): PaneConfig[] {
496
662
  const devCmd = detectDevCommand(dir);
497
663
  if (devCmd) {
498
664
  return [
@@ -504,7 +670,7 @@ function defaultPanes(dir) {
504
670
  return [{ name: "claude", cmd: "claude" }];
505
671
  }
506
672
 
507
- function syncSession() {
673
+ function syncSession(): void {
508
674
  const dir = process.cwd();
509
675
  const name = toSessionName(dir);
510
676
 
@@ -564,7 +730,7 @@ function syncSession() {
564
730
  `tmux display-message -t "${freshIds[i]}" -p "#{pane_current_command}"`
565
731
  );
566
732
  if (cur && shells.has(cur)) {
567
- run(`tmux send-keys -t "${freshIds[i]}" '${esc(panes[i].cmd)}' Enter`);
733
+ run(`tmux send-keys -t "${freshIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
568
734
  restored++;
569
735
  }
570
736
  }
@@ -583,7 +749,7 @@ function syncSession() {
583
749
 
584
750
  // ── Restart pane ────────────────────────────────────────────────────
585
751
 
586
- function restartPane(target) {
752
+ function restartPane(target?: string): void {
587
753
  const dir = process.cwd();
588
754
  const name = toSessionName(dir);
589
755
 
@@ -596,7 +762,7 @@ function restartPane(target) {
596
762
  const paneIds = getPaneIds(name);
597
763
 
598
764
  // Resolve target to an index
599
- let idx;
765
+ let idx: number;
600
766
  if (target === undefined || target === null || target === "") {
601
767
  // Default: first pane (claude)
602
768
  idx = 0;
@@ -665,10 +831,22 @@ function restartPane(target) {
665
831
 
666
832
  // ── Daemon-aware commands ────────────────────────────────────────────
667
833
 
668
- async function daemonStatusCommand() {
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
+
846
+ async function daemonStatusCommand(): Promise<void> {
669
847
  try {
670
848
  const { daemonCall } = await getDaemonClient();
671
- const status = await daemonCall("daemon.status");
849
+ const status = await daemonCall("daemon.status") as any;
672
850
  const uptime = Math.round(status.uptime);
673
851
  const h = Math.floor(uptime / 3600);
674
852
  const m = Math.floor((uptime % 3600) / 60);
@@ -685,10 +863,10 @@ async function daemonStatusCommand() {
685
863
  }
686
864
  }
687
865
 
688
- async function windowsCommand(jsonFlag) {
866
+ async function windowsCommand(jsonFlag: boolean): Promise<void> {
689
867
  try {
690
868
  const { daemonCall } = await getDaemonClient();
691
- const windows = await daemonCall("windows.list");
869
+ const windows = await daemonCall("windows.list") as any[];
692
870
  if (jsonFlag) {
693
871
  console.log(JSON.stringify(windows, null, 2));
694
872
  return;
@@ -712,7 +890,7 @@ async function windowsCommand(jsonFlag) {
712
890
  }
713
891
  }
714
892
 
715
- async function windowAssignCommand(wid, layerId) {
893
+ async function windowAssignCommand(wid?: string, layerId?: string): Promise<void> {
716
894
  if (!wid || !layerId) {
717
895
  console.log("Usage: lattices window assign <wid> <layer-id>");
718
896
  return;
@@ -721,15 +899,15 @@ async function windowAssignCommand(wid, layerId) {
721
899
  const { daemonCall } = await getDaemonClient();
722
900
  await daemonCall("window.assignLayer", { wid: parseInt(wid), layer: layerId });
723
901
  console.log(`Tagged wid:${wid} → layer:${layerId}`);
724
- } catch (e) {
725
- console.log(`Error: ${e.message}`);
902
+ } catch (e: unknown) {
903
+ console.log(`Error: ${(e as Error).message}`);
726
904
  }
727
905
  }
728
906
 
729
- async function windowLayerMapCommand(jsonFlag) {
907
+ async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
730
908
  try {
731
909
  const { daemonCall } = await getDaemonClient();
732
- const map = await daemonCall("window.layerMap");
910
+ const map = await daemonCall("window.layerMap") as any;
733
911
  if (jsonFlag) {
734
912
  console.log(JSON.stringify(map, null, 2));
735
913
  return;
@@ -748,7 +926,7 @@ async function windowLayerMapCommand(jsonFlag) {
748
926
  }
749
927
  }
750
928
 
751
- async function focusCommand(session) {
929
+ async function focusCommand(session?: string): Promise<void> {
752
930
  if (!session) {
753
931
  console.log("Usage: lattices focus <session-name>");
754
932
  return;
@@ -757,16 +935,304 @@ async function focusCommand(session) {
757
935
  const { daemonCall } = await getDaemonClient();
758
936
  await daemonCall("window.focus", { session });
759
937
  console.log(`Focused: ${session}`);
760
- } catch (e) {
761
- console.log(`Error: ${e.message}`);
938
+ } catch (e: unknown) {
939
+ console.log(`Error: ${(e as Error).message}`);
940
+ }
941
+ }
942
+
943
+ // ── Search ───────────────────────────────────────────────────────────
944
+
945
+ interface SearchResult {
946
+ score: number;
947
+ window: any;
948
+ tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
949
+ reasons: string[];
950
+ }
951
+
952
+ function relativeTime(iso: string): string {
953
+ const ms = Date.now() - new Date(iso).getTime();
954
+ const s = Math.floor(ms / 1000);
955
+ if (s < 60) return "just now";
956
+ const m = Math.floor(s / 60);
957
+ if (m < 60) return `${m}m ago`;
958
+ const h = Math.floor(m / 60);
959
+ if (h < 24) return `${h}h ago`;
960
+ const d = Math.floor(h / 24);
961
+ return `${d}d ago`;
962
+ }
963
+
964
+ // Unified search via lattices.search daemon API.
965
+ // All search surfaces should go through this one function.
966
+ interface SearchOptions {
967
+ sources?: string[]; // e.g. ["titles", "apps", "cwd", "ocr"] — omit for smart default
968
+ after?: string; // ISO8601 — only windows interacted after this time
969
+ before?: string; // ISO8601 — only windows interacted before this time
970
+ recency?: boolean; // boost recently-focused windows (default true)
971
+ mode?: string; // legacy compat: "quick", "complete", "terminal"
972
+ }
973
+
974
+ async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
975
+ const { daemonCall } = await getDaemonClient();
976
+ const params: Record<string, any> = { query };
977
+ if (opts.sources) params.sources = opts.sources;
978
+ if (opts.after) params.after = opts.after;
979
+ if (opts.before) params.before = opts.before;
980
+ if (opts.recency !== undefined) params.recency = opts.recency;
981
+ if (opts.mode) params.mode = opts.mode; // legacy fallback
982
+ const hits = await daemonCall("lattices.search", params, 10000) as any[];
983
+ return hits.map((w: any) => ({
984
+ score: w.score || 0,
985
+ window: w,
986
+ tabs: (w.terminalTabs || []).map((t: any) => ({
987
+ tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
988
+ })),
989
+ reasons: w.matchSources || [],
990
+ }));
991
+ }
992
+
993
+ // Convenience aliases
994
+ async function deepSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["all"] }); }
995
+ async function terminalSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["terminals"] }); }
996
+
997
+ // Format and print search results
998
+ function printResults(ranked: SearchResult[]): void {
999
+ if (!ranked.length) return;
1000
+ for (const r of ranked) {
1001
+ const w = r.window;
1002
+ const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
1003
+ console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
1004
+ for (const t of r.tabs) {
1005
+ const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
1006
+ const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
1007
+ console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
1008
+ }
1009
+ if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
1010
+ }
1011
+ console.log();
1012
+ }
1013
+
1014
+ // ── search command ───────────────────────────────────────────────────
1015
+
1016
+ async function searchCommand(query: string | undefined, flags: Set<string>, rawArgs: string[] = []): Promise<void> {
1017
+ if (!query) {
1018
+ console.log("Usage: lattices search <query> [--quick | --terminal | --all | --sources=... | --after=... | --before=... | --json | --wid]");
1019
+ return;
1020
+ }
1021
+
1022
+ // Build search options from flags
1023
+ const opts: SearchOptions = {};
1024
+
1025
+ // Source selection: explicit --sources, or legacy --quick/--terminal, or default
1026
+ const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
1027
+ if (sourcesFlag) {
1028
+ opts.sources = sourcesFlag.slice("--sources=".length).split(",");
1029
+ } else if (flags.has("--all")) {
1030
+ opts.sources = ["all"];
1031
+ } else if (flags.has("--quick")) {
1032
+ opts.sources = ["titles", "apps", "sessions"];
1033
+ } else if (flags.has("--terminal")) {
1034
+ opts.sources = ["terminals"];
1035
+ }
1036
+ // else: omit → smart default on daemon side
1037
+
1038
+ // Time filters
1039
+ const afterFlag = rawArgs.find(a => a.startsWith("--after="));
1040
+ if (afterFlag) opts.after = afterFlag.slice("--after=".length);
1041
+ const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
1042
+ if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
1043
+
1044
+ // No-recency flag
1045
+ if (flags.has("--no-recency")) opts.recency = false;
1046
+
1047
+ const ranked = await search(query, opts);
1048
+ const jsonOut = flags.has("--json");
1049
+ const widOnly = flags.has("--wid");
1050
+
1051
+ if (jsonOut) {
1052
+ console.log(JSON.stringify(ranked.map(r => ({
1053
+ wid: r.window.wid, app: r.window.app, title: r.window.title,
1054
+ score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
1055
+ })), null, 2));
1056
+ return;
1057
+ }
1058
+
1059
+ if (widOnly) {
1060
+ for (const r of ranked) console.log(r.window.wid);
1061
+ return;
1062
+ }
1063
+
1064
+ if (!ranked.length) {
1065
+ console.log(`No results for "${query}"`);
1066
+ return;
1067
+ }
1068
+
1069
+ printResults(ranked);
1070
+ }
1071
+
1072
+ // ── place command ────────────────────────────────────────────────────
1073
+
1074
+ async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
1075
+ if (!query) {
1076
+ console.log("Usage: lattices place <query> [position]");
1077
+ return;
1078
+ }
1079
+ try {
1080
+ const { daemonCall } = await getDaemonClient();
1081
+ const ranked = await deepSearch(query);
1082
+
1083
+ if (!ranked.length) {
1084
+ console.log(`No window matching "${query}"`);
1085
+ return;
1086
+ }
1087
+
1088
+ const pos = tilePosition || "bottom-right";
1089
+ const win = ranked[0].window;
1090
+ await daemonCall("window.focus", { wid: win.wid });
1091
+ await daemonCall("intents.execute", {
1092
+ intent: "tile_window",
1093
+ slots: { position: pos, wid: win.wid }
1094
+ }, 3000);
1095
+ console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
1096
+ } catch (e: unknown) {
1097
+ console.log(`Error: ${(e as Error).message}`);
1098
+ }
1099
+ }
1100
+
1101
+ async function sessionsCommand(jsonFlag: boolean): Promise<void> {
1102
+ try {
1103
+ const { daemonCall } = await getDaemonClient();
1104
+ const sessions = await daemonCall("tmux.sessions") as any[];
1105
+ if (jsonFlag) {
1106
+ console.log(JSON.stringify(sessions, null, 2));
1107
+ return;
1108
+ }
1109
+ if (!sessions.length) {
1110
+ console.log("No active sessions.");
1111
+ return;
1112
+ }
1113
+ console.log(`Sessions (${sessions.length}):\n`);
1114
+ for (const s of sessions) {
1115
+ const windows = s.windowCount || s.windows || "?";
1116
+ console.log(` \x1b[1m${s.name}\x1b[0m (${windows} windows)`);
1117
+ }
1118
+ } catch {
1119
+ console.log("Daemon not running. Start with: lattices app");
1120
+ }
1121
+ }
1122
+
1123
+ async function voiceCommand(subcommand?: string, ...rest: string[]): Promise<void> {
1124
+ const { daemonCall } = await getDaemonClient();
1125
+ try {
1126
+ switch (subcommand) {
1127
+ case "status": {
1128
+ const status = await daemonCall("voice.status") as any;
1129
+ console.log(`Provider: ${status.provider}`);
1130
+ console.log(`Available: ${status.available}`);
1131
+ console.log(`Listening: ${status.listening}`);
1132
+ if (status.lastTranscript) console.log(`Last: "${status.lastTranscript}"`);
1133
+ break;
1134
+ }
1135
+ case "simulate":
1136
+ case "sim": {
1137
+ const text = rest.join(" ");
1138
+ if (!text) {
1139
+ console.log("Usage: lattices voice simulate <text>");
1140
+ return;
1141
+ }
1142
+ const execute = !rest.includes("--dry-run");
1143
+ const dryFlag = rest.includes("--dry-run");
1144
+ const cleanText = dryFlag ? rest.filter(r => r !== "--dry-run").join(" ") : text;
1145
+ const result = await daemonCall("voice.simulate", { text: cleanText, execute }, 15000) as any;
1146
+ if (!result.parsed) {
1147
+ console.log(`\x1b[33mNo match:\x1b[0m "${cleanText}"`);
1148
+ return;
1149
+ }
1150
+ const slots = Object.entries(result.slots || {}).map(([k,v]) => `${k}: ${v}`).join(", ");
1151
+ const conf = result.confidence ? ` (${(result.confidence * 100).toFixed(0)}%)` : "";
1152
+ console.log(`\x1b[36m${result.intent}\x1b[0m${slots ? ` ${slots}` : ""}${conf}`);
1153
+ if (result.executed) {
1154
+ console.log(`\x1b[32mExecuted\x1b[0m`);
1155
+ } else if (result.error) {
1156
+ console.log(`\x1b[31mError:\x1b[0m ${result.error}`);
1157
+ }
1158
+ break;
1159
+ }
1160
+ case "intents": {
1161
+ const intents = await daemonCall("intents.list") as any[];
1162
+ for (const intent of intents) {
1163
+ const slots = intent.slots.map((s: any) => `${s.name}:${s.type}${s.required ? "*" : ""}`).join(", ");
1164
+ console.log(` \x1b[1m${intent.intent}\x1b[0m ${intent.description}`);
1165
+ if (slots) console.log(` slots: ${slots}`);
1166
+ console.log(` e.g. "${intent.examples[0]}"`);
1167
+ console.log();
1168
+ }
1169
+ break;
1170
+ }
1171
+ default:
1172
+ console.log("Usage: lattices voice <subcommand>\n");
1173
+ console.log(" status Show voice provider status");
1174
+ console.log(" simulate Parse and execute a voice command");
1175
+ console.log(" intents List all available intents");
1176
+ console.log("\nExamples:");
1177
+ console.log(' lattices voice simulate "tile this left"');
1178
+ console.log(' lattices voice simulate "focus chrome" --dry-run');
1179
+ }
1180
+ } catch (e: unknown) {
1181
+ console.log(`Error: ${(e as Error).message}`);
1182
+ }
1183
+ }
1184
+
1185
+ async function callCommand(method?: string, ...rest: string[]): Promise<void> {
1186
+ if (!method) {
1187
+ console.log("Usage: lattices call <method> [params-json]");
1188
+ console.log("\nExamples:");
1189
+ console.log(" lattices call daemon.status");
1190
+ console.log(" lattices call api.schema");
1191
+ console.log(' lattices call window.place \'{"session":"vox","placement":"left"}\'');
1192
+ return;
1193
+ }
1194
+ try {
1195
+ const { daemonCall } = await getDaemonClient();
1196
+ const params = rest[0] ? JSON.parse(rest[0]) : null;
1197
+ const result = await daemonCall(method, params, 15000);
1198
+ console.log(JSON.stringify(result, null, 2));
1199
+ } catch (e: unknown) {
1200
+ console.log(`Error: ${(e as Error).message}`);
762
1201
  }
763
1202
  }
764
1203
 
765
- async function layerCommand(index) {
1204
+ async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
766
1205
  try {
767
1206
  const { daemonCall } = await getDaemonClient();
768
- if (index === undefined || index === null || index === "") {
769
- const result = await daemonCall("layers.list");
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 === "") {
1235
+ const result = await daemonCall("layers.list") as any;
770
1236
  if (!result.layers.length) {
771
1237
  console.log("No layers configured.");
772
1238
  return;
@@ -778,23 +1244,148 @@ async function layerCommand(index) {
778
1244
  }
779
1245
  return;
780
1246
  }
781
- const idx = parseInt(index, 10);
1247
+ const idx = parseInt(sub, 10);
782
1248
  if (!isNaN(idx)) {
783
- await daemonCall("layer.switch", { index: idx });
784
- console.log(`Switched to layer ${idx}`);
1249
+ await daemonCall("layer.activate", { index: idx, mode: "launch" });
1250
+ console.log(`Activated layer ${idx}`);
785
1251
  } else {
786
- await daemonCall("layer.switch", { name: index });
787
- console.log(`Switched to layer "${index}"`);
1252
+ await daemonCall("layer.activate", { name: sub, mode: "launch" });
1253
+ console.log(`Activated layer "${sub}"`);
788
1254
  }
789
- } catch (e) {
790
- console.log(`Error: ${e.message}`);
1255
+ } catch (e: unknown) {
1256
+ console.log(`Error: ${(e as Error).message}`);
791
1257
  }
792
1258
  }
793
1259
 
794
- async function diagCommand(limit) {
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
+
1385
+ async function diagCommand(limit?: string): Promise<void> {
795
1386
  try {
796
1387
  const { daemonCall } = await getDaemonClient();
797
- const result = await daemonCall("diagnostics.list", { limit: parseInt(limit, 10) || 40 });
1388
+ const result = await daemonCall("diagnostics.list", { limit: parseInt(limit || "", 10) || 40 }) as any;
798
1389
  if (!result.entries || !result.entries.length) {
799
1390
  console.log("No diagnostic entries.");
800
1391
  return;
@@ -805,26 +1396,26 @@ async function diagCommand(limit) {
805
1396
  entry.level === "error" ? "\x1b[31m✗\x1b[0m" : "›";
806
1397
  console.log(` \x1b[90m${entry.time}\x1b[0m ${icon} ${entry.message}`);
807
1398
  }
808
- } catch (e) {
809
- console.log(`Error: ${e.message}`);
1399
+ } catch (e: unknown) {
1400
+ console.log(`Error: ${(e as Error).message}`);
810
1401
  }
811
1402
  }
812
1403
 
813
- async function distributeCommand() {
1404
+ async function distributeCommand(): Promise<void> {
814
1405
  try {
815
1406
  const { daemonCall } = await getDaemonClient();
816
- await daemonCall("layout.distribute");
1407
+ await daemonCall("space.optimize", { scope: "visible", strategy: "balanced" });
817
1408
  console.log("Distributed visible windows into grid");
818
1409
  } catch {
819
1410
  console.log("Daemon not running. Start with: lattices app");
820
1411
  }
821
1412
  }
822
1413
 
823
- async function daemonLsCommand() {
1414
+ async function daemonLsCommand(): Promise<boolean> {
824
1415
  try {
825
1416
  const { daemonCall, isDaemonRunning } = await getDaemonClient();
826
1417
  if (!(await isDaemonRunning())) return false;
827
- const sessions = await daemonCall("tmux.sessions");
1418
+ const sessions = await daemonCall("tmux.sessions") as any[];
828
1419
  if (!sessions.length) {
829
1420
  console.log("No active tmux sessions.");
830
1421
  return true;
@@ -832,7 +1423,7 @@ async function daemonLsCommand() {
832
1423
 
833
1424
  // Annotate sessions with workspace group info
834
1425
  const ws = readWorkspaceConfig();
835
- const sessionGroupMap = new Map();
1426
+ const sessionGroupMap = new Map<string, { group: string; tab: string }>();
836
1427
  if (ws?.groups) {
837
1428
  for (const g of ws.groups) {
838
1429
  for (const tab of g.tabs || []) {
@@ -858,14 +1449,14 @@ async function daemonLsCommand() {
858
1449
  }
859
1450
  }
860
1451
 
861
- async function daemonStatusInventory() {
1452
+ async function daemonStatusInventory(): Promise<boolean> {
862
1453
  try {
863
1454
  const { daemonCall, isDaemonRunning } = await getDaemonClient();
864
1455
  if (!(await isDaemonRunning())) return false;
865
- const inv = await daemonCall("tmux.inventory");
1456
+ const inv = await daemonCall("tmux.inventory") as any;
866
1457
 
867
1458
  // Build managed session name set
868
- const managed = new Map();
1459
+ const managed = new Map<string, string>();
869
1460
  const ws = readWorkspaceConfig();
870
1461
  if (ws?.groups) {
871
1462
  for (const g of ws.groups) {
@@ -879,7 +1470,7 @@ async function daemonStatusInventory() {
879
1470
  for (const s of inv.all) {
880
1471
  if (!managed.has(s.name)) {
881
1472
  // Check if it matches a scanned project (via daemon)
882
- const projects = await daemonCall("projects.list");
1473
+ const projects = await daemonCall("projects.list") as any[];
883
1474
  for (const p of projects) {
884
1475
  managed.set(p.sessionName, p.name);
885
1476
  }
@@ -887,7 +1478,7 @@ async function daemonStatusInventory() {
887
1478
  }
888
1479
  }
889
1480
 
890
- const managedSessions = inv.all.filter((s) => managed.has(s.name));
1481
+ const managedSessions = inv.all.filter((s: any) => managed.has(s.name));
891
1482
  const orphanSessions = inv.orphans;
892
1483
 
893
1484
  if (managedSessions.length > 0) {
@@ -926,14 +1517,14 @@ async function daemonStatusInventory() {
926
1517
 
927
1518
  // ── OCR commands ──────────────────────────────────────────────────────
928
1519
 
929
- async function scanCommand(sub, ...rest) {
1520
+ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
930
1521
  const { daemonCall } = await getDaemonClient();
931
1522
 
932
1523
  if (!sub || sub === "snapshot" || sub === "ls" || sub === "--full" || sub === "-f" || sub === "--json") {
933
1524
  const full = sub === "--full" || sub === "-f" || rest.includes("--full") || rest.includes("-f");
934
1525
  const json = sub === "--json" || rest.includes("--json");
935
1526
  try {
936
- const results = await daemonCall("ocr.snapshot", null, 5000);
1527
+ const results = await daemonCall("ocr.snapshot", null, 5000) as any[];
937
1528
  if (!results.length) {
938
1529
  console.log("No scan results yet. The first scan runs ~60s after launch.");
939
1530
  return;
@@ -957,7 +1548,7 @@ async function scanCommand(sub, ...rest) {
957
1548
  }
958
1549
  } else {
959
1550
  const maxPreview = 5;
960
- const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
1551
+ const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
961
1552
  for (const line of preview) {
962
1553
  console.log(` \x1b[90m${line}\x1b[0m`);
963
1554
  }
@@ -983,7 +1574,7 @@ async function scanCommand(sub, ...rest) {
983
1574
  return;
984
1575
  }
985
1576
  try {
986
- const results = await daemonCall("ocr.search", { query }, 5000);
1577
+ const results = await daemonCall("ocr.search", { query }, 5000) as any[];
987
1578
  if (!results.length) {
988
1579
  console.log(`No matches for "${query}".`);
989
1580
  return;
@@ -997,8 +1588,8 @@ async function scanCommand(sub, ...rest) {
997
1588
  console.log(` ${snippet}`);
998
1589
  console.log();
999
1590
  }
1000
- } catch (e) {
1001
- console.log(`Error: ${e.message}`);
1591
+ } catch (e: unknown) {
1592
+ console.log(`Error: ${(e as Error).message}`);
1002
1593
  }
1003
1594
  return;
1004
1595
  }
@@ -1006,9 +1597,9 @@ async function scanCommand(sub, ...rest) {
1006
1597
  if (sub === "recent" || sub === "log") {
1007
1598
  const full = rest.includes("--full") || rest.includes("-f");
1008
1599
  const numArg = rest.find(a => !a.startsWith("-"));
1009
- const limit = parseInt(numArg, 10) || 20;
1600
+ const limit = parseInt(numArg || "", 10) || 20;
1010
1601
  try {
1011
- const results = await daemonCall("ocr.recent", { limit }, 5000);
1602
+ const results = await daemonCall("ocr.recent", { limit }, 5000) as any[];
1012
1603
  if (!results.length) {
1013
1604
  console.log("No history yet. The first scan runs ~60s after launch.");
1014
1605
  return;
@@ -1026,7 +1617,7 @@ async function scanCommand(sub, ...rest) {
1026
1617
  }
1027
1618
  } else {
1028
1619
  const maxPreview = 5;
1029
- const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
1620
+ const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
1030
1621
  for (const line of preview) {
1031
1622
  console.log(` \x1b[90m${line}\x1b[0m`);
1032
1623
  }
@@ -1047,8 +1638,8 @@ async function scanCommand(sub, ...rest) {
1047
1638
  console.log("Triggering deep scan (Vision OCR)...");
1048
1639
  await daemonCall("ocr.scan", null, 30000);
1049
1640
  console.log("Done.");
1050
- } catch (e) {
1051
- console.log(`Error: ${e.message}`);
1641
+ } catch (e: unknown) {
1642
+ console.log(`Error: ${(e as Error).message}`);
1052
1643
  }
1053
1644
  return;
1054
1645
  }
@@ -1060,7 +1651,7 @@ async function scanCommand(sub, ...rest) {
1060
1651
  return;
1061
1652
  }
1062
1653
  try {
1063
- const results = await daemonCall("ocr.history", { wid }, 5000);
1654
+ const results = await daemonCall("ocr.history", { wid }, 5000) as any[];
1064
1655
  if (!results.length) {
1065
1656
  console.log(`No history for wid:${wid}.`);
1066
1657
  return;
@@ -1070,15 +1661,15 @@ async function scanCommand(sub, ...rest) {
1070
1661
  const ts = new Date(r.timestamp * 1000).toLocaleTimeString();
1071
1662
  const src = r.source === "accessibility" ? "\x1b[33mAX\x1b[0m" : "\x1b[35mOCR\x1b[0m";
1072
1663
  const lines = (r.fullText || "").split("\n").filter(Boolean);
1073
- const preview = lines.slice(0, 2).map(l => l.length > 80 ? l.slice(0, 77) + "..." : l);
1664
+ const preview = lines.slice(0, 2).map((l: string) => l.length > 80 ? l.slice(0, 77) + "..." : l);
1074
1665
  console.log(` \x1b[90m${ts}\x1b[0m ${src} \x1b[1m${r.app}\x1b[0m — "${r.title}"`);
1075
1666
  for (const line of preview) {
1076
1667
  console.log(` \x1b[90m${line}\x1b[0m`);
1077
1668
  }
1078
1669
  console.log();
1079
1670
  }
1080
- } catch (e) {
1081
- console.log(`Error: ${e.message}`);
1671
+ } catch (e: unknown) {
1672
+ console.log(`Error: ${(e as Error).message}`);
1082
1673
  }
1083
1674
  return;
1084
1675
  }
@@ -1097,7 +1688,7 @@ Usage:
1097
1688
  `);
1098
1689
  }
1099
1690
 
1100
- function printUsage() {
1691
+ function printUsage(): void {
1101
1692
  console.log(`lattices — Claude Code + dev server in tmux
1102
1693
 
1103
1694
  Usage:
@@ -1111,17 +1702,38 @@ Usage:
1111
1702
  lattices group [id] List tab groups or launch/attach a group
1112
1703
  lattices groups List all tab groups with status
1113
1704
  lattices tab <group> [tab] Switch tab within a group (by label or index)
1705
+ lattices search <query> Search windows by title, app, session, OCR
1706
+ lattices search <q> --deep Deep search: index + live terminal inspection
1707
+ lattices search <q> --wid Print matching window IDs only (pipeable)
1708
+ lattices search <q> --json JSON output
1709
+ lattices place <query> [pos] Deep search + focus + tile (default: bottom-right)
1710
+ lattices focus <session> Raise a session's window
1114
1711
  lattices windows [--json] List all desktop windows (daemon required)
1115
- lattices focus <session> Focus a session's terminal window (daemon required)
1712
+ lattices sessions [--json] List active tmux sessions via daemon
1116
1713
  lattices tile <position> Tile the frontmost window (left, right, top, etc.)
1117
1714
  lattices distribute Smart-grid all visible windows (daemon required)
1118
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
1721
+ lattices voice status Voice provider status
1722
+ lattices voice simulate <t> Parse and execute a voice command
1723
+ lattices voice intents List all available intents
1724
+ lattices call <method> [p] Raw daemon API call (params as JSON)
1119
1725
  lattices scan Show text from all visible windows
1120
1726
  lattices scan --full Full text dump
1121
1727
  lattices scan search <q> Full-text search across scanned windows
1122
1728
  lattices scan recent [n] Show recent scans chronologically
1123
1729
  lattices scan deep Trigger a deep Vision OCR scan
1124
1730
  lattices scan history <wid> Scan timeline for a specific window
1731
+ lattices dev Run dev server (auto-detected)
1732
+ lattices dev build Build the project (swift/node/rust/go/make)
1733
+ lattices dev restart Build + restart (swift app) or just build
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
1125
1737
  lattices daemon status Show daemon status
1126
1738
  lattices diag [limit] Show diagnostic log entries
1127
1739
  lattices app Launch the menu bar companion app
@@ -1172,7 +1784,7 @@ Layouts:
1172
1784
  `);
1173
1785
  }
1174
1786
 
1175
- function initConfig() {
1787
+ function initConfig(): void {
1176
1788
  const dir = process.cwd();
1177
1789
  const configPath = resolve(dir, ".lattices.json");
1178
1790
 
@@ -1192,7 +1804,7 @@ function initConfig() {
1192
1804
  console.log(JSON.stringify(config, null, 2));
1193
1805
  }
1194
1806
 
1195
- function listSessions() {
1807
+ function listSessions(): void {
1196
1808
  const out = runQuiet(
1197
1809
  "tmux list-sessions -F '#{session_name} (#{session_windows} windows, created #{session_created_string})'"
1198
1810
  );
@@ -1203,7 +1815,7 @@ function listSessions() {
1203
1815
 
1204
1816
  // Annotate sessions that belong to tab groups
1205
1817
  const ws = readWorkspaceConfig();
1206
- const sessionGroupMap = new Map();
1818
+ const sessionGroupMap = new Map<string, { group: string; tab: string }>();
1207
1819
  if (ws?.groups) {
1208
1820
  for (const g of ws.groups) {
1209
1821
  for (const tab of g.tabs || []) {
@@ -1216,7 +1828,7 @@ function listSessions() {
1216
1828
  }
1217
1829
  }
1218
1830
 
1219
- const lines = out.split("\n").map((line) => {
1831
+ const lines = out.split("\n").map((line: string) => {
1220
1832
  const sessionName = line.split(" ")[0];
1221
1833
  const info = sessionGroupMap.get(sessionName);
1222
1834
  return info
@@ -1228,7 +1840,7 @@ function listSessions() {
1228
1840
  console.log(lines.join("\n"));
1229
1841
  }
1230
1842
 
1231
- function killSession(name) {
1843
+ function killSession(name?: string): void {
1232
1844
  if (!name) name = toSessionName(process.cwd());
1233
1845
  if (!sessionExists(name)) {
1234
1846
  console.log(`No session "${name}".`);
@@ -1240,7 +1852,14 @@ function killSession(name) {
1240
1852
 
1241
1853
  // ── Window tiling ────────────────────────────────────────────────────
1242
1854
 
1243
- function getScreenBounds() {
1855
+ interface ScreenBounds {
1856
+ x: number;
1857
+ y: number;
1858
+ w: number;
1859
+ h: number;
1860
+ }
1861
+
1862
+ function getScreenBounds(): ScreenBounds {
1244
1863
  // Get the visible area (excludes menu bar and dock) in AppleScript coordinates (top-left origin)
1245
1864
  const script = `
1246
1865
  tell application "Finder"
@@ -1255,7 +1874,7 @@ function getScreenBounds() {
1255
1874
  }
1256
1875
 
1257
1876
  // Presets return AppleScript bounds: [left, top, right, bottom] within the visible area
1258
- const tilePresets = {
1877
+ const tilePresets: Record<string, (s: ScreenBounds) => number[]> = {
1259
1878
  "left": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
1260
1879
  "left-half": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
1261
1880
  "right": (s) => [s.x + s.w / 2, s.y, s.x + s.w, s.y + s.h],
@@ -1282,7 +1901,7 @@ const tilePresets = {
1282
1901
  "right-third": (s) => [s.x + Math.round(s.w * 0.667), s.y, s.x + s.w, s.y + s.h],
1283
1902
  };
1284
1903
 
1285
- function tileWindow(position) {
1904
+ function tileWindow(position: string): void {
1286
1905
  const preset = tilePresets[position];
1287
1906
  if (!preset) {
1288
1907
  console.log(`Unknown position: ${position}`);
@@ -1302,7 +1921,7 @@ function tileWindow(position) {
1302
1921
  console.log(`Tiled → ${position}`);
1303
1922
  }
1304
1923
 
1305
- function createOrAttach() {
1924
+ function createOrAttach(): void {
1306
1925
  const dir = process.cwd();
1307
1926
  const name = toSessionName(dir);
1308
1927
 
@@ -1323,7 +1942,7 @@ function createOrAttach() {
1323
1942
  attach(name);
1324
1943
  }
1325
1944
 
1326
- function attach(name) {
1945
+ function attach(name: string): void {
1327
1946
  if (isInsideTmux()) {
1328
1947
  execSync(`tmux switch-client -t "${name}"`, { stdio: "inherit" });
1329
1948
  } else {
@@ -1333,7 +1952,7 @@ function attach(name) {
1333
1952
 
1334
1953
  // ── Status / Inventory ───────────────────────────────────────────────
1335
1954
 
1336
- function statusInventory() {
1955
+ function statusInventory(): void {
1337
1956
  // Query all tmux sessions
1338
1957
  const sessionsRaw = runQuiet(
1339
1958
  'tmux list-sessions -F "#{session_name}\t#{session_windows}\t#{session_attached}"'
@@ -1349,17 +1968,17 @@ function statusInventory() {
1349
1968
  );
1350
1969
 
1351
1970
  // Parse panes grouped by session
1352
- const panesBySession = new Map();
1971
+ const panesBySession = new Map<string, { title: string; cmd: string }[]>();
1353
1972
  if (panesRaw) {
1354
1973
  for (const line of panesRaw.split("\n").filter(Boolean)) {
1355
1974
  const [sess, title, cmd] = line.split("\t");
1356
1975
  if (!panesBySession.has(sess)) panesBySession.set(sess, []);
1357
- panesBySession.get(sess).push({ title, cmd });
1976
+ panesBySession.get(sess)!.push({ title, cmd });
1358
1977
  }
1359
1978
  }
1360
1979
 
1361
1980
  // Build managed session name set
1362
- const managed = new Map(); // name -> label
1981
+ const managed = new Map<string, string>(); // name -> label
1363
1982
 
1364
1983
  // From workspace groups
1365
1984
  const ws = readWorkspaceConfig();
@@ -1391,7 +2010,7 @@ function statusInventory() {
1391
2010
  }
1392
2011
 
1393
2012
  // Parse sessions and classify
1394
- const sessions = sessionsRaw.split("\n").filter(Boolean).map((line) => {
2013
+ const sessions = sessionsRaw.split("\n").filter(Boolean).map((line: string) => {
1395
2014
  const [name, windows, attached] = line.split("\t");
1396
2015
  return { name, windows: parseInt(windows) || 1, attached: attached !== "0" };
1397
2016
  });
@@ -1437,10 +2056,7 @@ function statusInventory() {
1437
2056
 
1438
2057
  // ── Main ─────────────────────────────────────────────────────────────
1439
2058
 
1440
- if (!hasTmux()) {
1441
- console.error("tmux is not installed. Install with: brew install tmux");
1442
- process.exit(1);
1443
- }
2059
+ requireTmux(command);
1444
2060
 
1445
2061
  switch (command) {
1446
2062
  case "init":
@@ -1509,12 +2125,28 @@ switch (command) {
1509
2125
  console.log(" lattices window map [--json] Show all layer tags");
1510
2126
  }
1511
2127
  break;
2128
+ case "search":
2129
+ case "s":
2130
+ await searchCommand(args[1], new Set(args.slice(2)));
2131
+ break;
1512
2132
  case "focus":
1513
2133
  await focusCommand(args[1]);
1514
2134
  break;
2135
+ case "place":
2136
+ await placeCommand(args[1], args[2]);
2137
+ break;
2138
+ case "sessions":
2139
+ await sessionsCommand(args[1] === "--json");
2140
+ break;
2141
+ case "voice":
2142
+ await voiceCommand(args[1], ...args.slice(2));
2143
+ break;
2144
+ case "call":
2145
+ await callCommand(args[1], ...args.slice(2));
2146
+ break;
1515
2147
  case "layer":
1516
2148
  case "layers":
1517
- await layerCommand(args[1]);
2149
+ await layerCommand(args[1], ...args.slice(2));
1518
2150
  break;
1519
2151
  case "diag":
1520
2152
  case "diagnostics":
@@ -1524,6 +2156,9 @@ switch (command) {
1524
2156
  case "ocr":
1525
2157
  await scanCommand(args[1], ...args.slice(2));
1526
2158
  break;
2159
+ case "mouse":
2160
+ await mouseCommand(args[1]);
2161
+ break;
1527
2162
  case "daemon":
1528
2163
  if (args[1] === "status") {
1529
2164
  await daemonStatusCommand();
@@ -1531,13 +2166,15 @@ switch (command) {
1531
2166
  console.log("Usage: lattices daemon status");
1532
2167
  }
1533
2168
  break;
2169
+ case "dev":
2170
+ await devCommand(args[1], ...args.slice(2));
2171
+ break;
1534
2172
  case "app": {
1535
2173
  // Forward to lattices-app script
1536
2174
  const { execFileSync } = await import("node:child_process");
1537
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
1538
- const appScript = resolve(__dirname2, "lattices-app.js");
2175
+ const appScript = resolve(import.meta.dir, "lattices-app.ts");
1539
2176
  try {
1540
- execFileSync("node", [appScript, ...args.slice(1)], { stdio: "inherit" });
2177
+ execFileSync("bun", [appScript, ...args.slice(1)], { stdio: "inherit" });
1541
2178
  } catch { /* exit code forwarded */ }
1542
2179
  break;
1543
2180
  }