@lattices/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +85 -9
  2. package/app/Package.swift +8 -1
  3. package/app/Sources/AdvisorLearningStore.swift +90 -0
  4. package/app/Sources/AgentSession.swift +377 -0
  5. package/app/Sources/AppDelegate.swift +44 -12
  6. package/app/Sources/AppShellView.swift +81 -8
  7. package/app/Sources/AudioProvider.swift +386 -0
  8. package/app/Sources/CheatSheetHUD.swift +261 -19
  9. package/app/Sources/DaemonProtocol.swift +13 -0
  10. package/app/Sources/DaemonServer.swift +8 -0
  11. package/app/Sources/DesktopModel.swift +164 -5
  12. package/app/Sources/DesktopModelTypes.swift +2 -0
  13. package/app/Sources/DiagnosticLog.swift +104 -2
  14. package/app/Sources/EventBus.swift +1 -0
  15. package/app/Sources/HUDBottomBar.swift +279 -0
  16. package/app/Sources/HUDController.swift +1158 -0
  17. package/app/Sources/HUDLeftBar.swift +849 -0
  18. package/app/Sources/HUDMinimap.swift +179 -0
  19. package/app/Sources/HUDRightBar.swift +774 -0
  20. package/app/Sources/HUDState.swift +367 -0
  21. package/app/Sources/HUDTopBar.swift +243 -0
  22. package/app/Sources/HandsOffSession.swift +733 -0
  23. package/app/Sources/HomeDashboardView.swift +125 -0
  24. package/app/Sources/HotkeyManager.swift +2 -0
  25. package/app/Sources/HotkeyStore.swift +45 -9
  26. package/app/Sources/IntentEngine.swift +925 -0
  27. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  28. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  29. package/app/Sources/Intents/FocusIntent.swift +69 -0
  30. package/app/Sources/Intents/HelpIntent.swift +41 -0
  31. package/app/Sources/Intents/KillIntent.swift +47 -0
  32. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  33. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  34. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  35. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  36. package/app/Sources/Intents/ScanIntent.swift +52 -0
  37. package/app/Sources/Intents/SearchIntent.swift +190 -0
  38. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  39. package/app/Sources/Intents/TileIntent.swift +61 -0
  40. package/app/Sources/LatticesApi.swift +1235 -30
  41. package/app/Sources/LauncherHUD.swift +348 -0
  42. package/app/Sources/MainView.swift +147 -44
  43. package/app/Sources/OcrModel.swift +34 -1
  44. package/app/Sources/OmniSearchState.swift +99 -102
  45. package/app/Sources/OnboardingView.swift +457 -0
  46. package/app/Sources/PermissionChecker.swift +2 -12
  47. package/app/Sources/PiChatDock.swift +454 -0
  48. package/app/Sources/PiChatSession.swift +815 -0
  49. package/app/Sources/PiWorkspaceView.swift +364 -0
  50. package/app/Sources/PlacementSpec.swift +195 -0
  51. package/app/Sources/Preferences.swift +59 -0
  52. package/app/Sources/ProjectScanner.swift +1 -1
  53. package/app/Sources/ScreenMapState.swift +701 -55
  54. package/app/Sources/ScreenMapView.swift +843 -103
  55. package/app/Sources/ScreenMapWindowController.swift +22 -0
  56. package/app/Sources/SessionLayerStore.swift +285 -0
  57. package/app/Sources/SessionManager.swift +4 -1
  58. package/app/Sources/SettingsView.swift +186 -3
  59. package/app/Sources/Theme.swift +9 -8
  60. package/app/Sources/TmuxModel.swift +7 -0
  61. package/app/Sources/TmuxQuery.swift +27 -3
  62. package/app/Sources/VoiceChatView.swift +192 -0
  63. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  64. package/app/Sources/VoiceIntentResolver.swift +671 -0
  65. package/app/Sources/VoxClient.swift +454 -0
  66. package/app/Sources/WindowTiler.swift +348 -87
  67. package/app/Sources/WorkspaceManager.swift +127 -18
  68. package/bin/client.ts +16 -0
  69. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  70. package/bin/handsoff-infer.ts +280 -0
  71. package/bin/handsoff-worker.ts +731 -0
  72. package/bin/{lattices-app.js → lattices-app.ts} +67 -32
  73. package/bin/lattices-dev +160 -0
  74. package/bin/{lattices.js → lattices.ts} +600 -137
  75. package/bin/project-twin.ts +645 -0
  76. package/docs/agent-execution-plan.md +562 -0
  77. package/docs/agents.md +142 -0
  78. package/docs/api.md +153 -34
  79. package/docs/app.md +29 -1
  80. package/docs/config.md +5 -1
  81. package/docs/handsoff-test-scenarios.md +84 -0
  82. package/docs/layers.md +20 -20
  83. package/docs/ocr.md +14 -5
  84. package/docs/overview.md +5 -1
  85. package/docs/presentation-execution-review.md +491 -0
  86. package/docs/prompts/hands-off-system.md +374 -0
  87. package/docs/prompts/hands-off-turn.md +30 -0
  88. package/docs/prompts/voice-advisor.md +31 -0
  89. package/docs/prompts/voice-fallback.md +23 -0
  90. package/docs/tiling-reference.md +167 -0
  91. package/docs/twins.md +138 -0
  92. package/docs/voice-command-protocol.md +278 -0
  93. package/docs/voice.md +219 -0
  94. package/package.json +21 -10
  95. package/bin/client.js +0 -4
@@ -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", "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,10 @@ function restartPane(target) {
665
831
 
666
832
  // ── Daemon-aware commands ────────────────────────────────────────────
667
833
 
668
- async function daemonStatusCommand() {
834
+ async function daemonStatusCommand(): Promise<void> {
669
835
  try {
670
836
  const { daemonCall } = await getDaemonClient();
671
- const status = await daemonCall("daemon.status");
837
+ const status = await daemonCall("daemon.status") as any;
672
838
  const uptime = Math.round(status.uptime);
673
839
  const h = Math.floor(uptime / 3600);
674
840
  const m = Math.floor((uptime % 3600) / 60);
@@ -685,10 +851,10 @@ async function daemonStatusCommand() {
685
851
  }
686
852
  }
687
853
 
688
- async function windowsCommand(jsonFlag) {
854
+ async function windowsCommand(jsonFlag: boolean): Promise<void> {
689
855
  try {
690
856
  const { daemonCall } = await getDaemonClient();
691
- const windows = await daemonCall("windows.list");
857
+ const windows = await daemonCall("windows.list") as any[];
692
858
  if (jsonFlag) {
693
859
  console.log(JSON.stringify(windows, null, 2));
694
860
  return;
@@ -712,7 +878,7 @@ async function windowsCommand(jsonFlag) {
712
878
  }
713
879
  }
714
880
 
715
- async function windowAssignCommand(wid, layerId) {
881
+ async function windowAssignCommand(wid?: string, layerId?: string): Promise<void> {
716
882
  if (!wid || !layerId) {
717
883
  console.log("Usage: lattices window assign <wid> <layer-id>");
718
884
  return;
@@ -721,15 +887,15 @@ async function windowAssignCommand(wid, layerId) {
721
887
  const { daemonCall } = await getDaemonClient();
722
888
  await daemonCall("window.assignLayer", { wid: parseInt(wid), layer: layerId });
723
889
  console.log(`Tagged wid:${wid} → layer:${layerId}`);
724
- } catch (e) {
725
- console.log(`Error: ${e.message}`);
890
+ } catch (e: unknown) {
891
+ console.log(`Error: ${(e as Error).message}`);
726
892
  }
727
893
  }
728
894
 
729
- async function windowLayerMapCommand(jsonFlag) {
895
+ async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
730
896
  try {
731
897
  const { daemonCall } = await getDaemonClient();
732
- const map = await daemonCall("window.layerMap");
898
+ const map = await daemonCall("window.layerMap") as any;
733
899
  if (jsonFlag) {
734
900
  console.log(JSON.stringify(map, null, 2));
735
901
  return;
@@ -748,7 +914,7 @@ async function windowLayerMapCommand(jsonFlag) {
748
914
  }
749
915
  }
750
916
 
751
- async function focusCommand(session) {
917
+ async function focusCommand(session?: string): Promise<void> {
752
918
  if (!session) {
753
919
  console.log("Usage: lattices focus <session-name>");
754
920
  return;
@@ -757,16 +923,277 @@ async function focusCommand(session) {
757
923
  const { daemonCall } = await getDaemonClient();
758
924
  await daemonCall("window.focus", { session });
759
925
  console.log(`Focused: ${session}`);
760
- } catch (e) {
761
- console.log(`Error: ${e.message}`);
926
+ } catch (e: unknown) {
927
+ console.log(`Error: ${(e as Error).message}`);
928
+ }
929
+ }
930
+
931
+ // ── Search ───────────────────────────────────────────────────────────
932
+
933
+ interface SearchResult {
934
+ score: number;
935
+ window: any;
936
+ tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
937
+ reasons: string[];
938
+ }
939
+
940
+ function relativeTime(iso: string): string {
941
+ const ms = Date.now() - new Date(iso).getTime();
942
+ const s = Math.floor(ms / 1000);
943
+ if (s < 60) return "just now";
944
+ const m = Math.floor(s / 60);
945
+ if (m < 60) return `${m}m ago`;
946
+ const h = Math.floor(m / 60);
947
+ if (h < 24) return `${h}h ago`;
948
+ const d = Math.floor(h / 24);
949
+ return `${d}d ago`;
950
+ }
951
+
952
+ // Unified search via lattices.search daemon API.
953
+ // All search surfaces should go through this one function.
954
+ interface SearchOptions {
955
+ sources?: string[]; // e.g. ["titles", "apps", "cwd", "ocr"] — omit for smart default
956
+ after?: string; // ISO8601 — only windows interacted after this time
957
+ before?: string; // ISO8601 — only windows interacted before this time
958
+ recency?: boolean; // boost recently-focused windows (default true)
959
+ mode?: string; // legacy compat: "quick", "complete", "terminal"
960
+ }
961
+
962
+ async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
963
+ const { daemonCall } = await getDaemonClient();
964
+ const params: Record<string, any> = { query };
965
+ if (opts.sources) params.sources = opts.sources;
966
+ if (opts.after) params.after = opts.after;
967
+ if (opts.before) params.before = opts.before;
968
+ if (opts.recency !== undefined) params.recency = opts.recency;
969
+ if (opts.mode) params.mode = opts.mode; // legacy fallback
970
+ const hits = await daemonCall("lattices.search", params, 10000) as any[];
971
+ return hits.map((w: any) => ({
972
+ score: w.score || 0,
973
+ window: w,
974
+ tabs: (w.terminalTabs || []).map((t: any) => ({
975
+ tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
976
+ })),
977
+ reasons: w.matchSources || [],
978
+ }));
979
+ }
980
+
981
+ // Convenience aliases
982
+ async function deepSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["all"] }); }
983
+ async function terminalSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["terminals"] }); }
984
+
985
+ // Format and print search results
986
+ function printResults(ranked: SearchResult[]): void {
987
+ if (!ranked.length) return;
988
+ for (const r of ranked) {
989
+ const w = r.window;
990
+ const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
991
+ console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
992
+ for (const t of r.tabs) {
993
+ const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
994
+ const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
995
+ console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
996
+ }
997
+ if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
762
998
  }
999
+ console.log();
763
1000
  }
764
1001
 
765
- async function layerCommand(index) {
1002
+ // ── search command ───────────────────────────────────────────────────
1003
+
1004
+ async function searchCommand(query: string | undefined, flags: Set<string>, rawArgs: string[] = []): Promise<void> {
1005
+ if (!query) {
1006
+ console.log("Usage: lattices search <query> [--quick | --terminal | --all | --sources=... | --after=... | --before=... | --json | --wid]");
1007
+ return;
1008
+ }
1009
+
1010
+ // Build search options from flags
1011
+ const opts: SearchOptions = {};
1012
+
1013
+ // Source selection: explicit --sources, or legacy --quick/--terminal, or default
1014
+ const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
1015
+ if (sourcesFlag) {
1016
+ opts.sources = sourcesFlag.slice("--sources=".length).split(",");
1017
+ } else if (flags.has("--all")) {
1018
+ opts.sources = ["all"];
1019
+ } else if (flags.has("--quick")) {
1020
+ opts.sources = ["titles", "apps", "sessions"];
1021
+ } else if (flags.has("--terminal")) {
1022
+ opts.sources = ["terminals"];
1023
+ }
1024
+ // else: omit → smart default on daemon side
1025
+
1026
+ // Time filters
1027
+ const afterFlag = rawArgs.find(a => a.startsWith("--after="));
1028
+ if (afterFlag) opts.after = afterFlag.slice("--after=".length);
1029
+ const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
1030
+ if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
1031
+
1032
+ // No-recency flag
1033
+ if (flags.has("--no-recency")) opts.recency = false;
1034
+
1035
+ const ranked = await search(query, opts);
1036
+ const jsonOut = flags.has("--json");
1037
+ const widOnly = flags.has("--wid");
1038
+
1039
+ if (jsonOut) {
1040
+ console.log(JSON.stringify(ranked.map(r => ({
1041
+ wid: r.window.wid, app: r.window.app, title: r.window.title,
1042
+ score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
1043
+ })), null, 2));
1044
+ return;
1045
+ }
1046
+
1047
+ if (widOnly) {
1048
+ for (const r of ranked) console.log(r.window.wid);
1049
+ return;
1050
+ }
1051
+
1052
+ if (!ranked.length) {
1053
+ console.log(`No results for "${query}"`);
1054
+ return;
1055
+ }
1056
+
1057
+ printResults(ranked);
1058
+ }
1059
+
1060
+ // ── place command ────────────────────────────────────────────────────
1061
+
1062
+ async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
1063
+ if (!query) {
1064
+ console.log("Usage: lattices place <query> [position]");
1065
+ return;
1066
+ }
1067
+ try {
1068
+ const { daemonCall } = await getDaemonClient();
1069
+ const ranked = await deepSearch(query);
1070
+
1071
+ if (!ranked.length) {
1072
+ console.log(`No window matching "${query}"`);
1073
+ return;
1074
+ }
1075
+
1076
+ const pos = tilePosition || "bottom-right";
1077
+ const win = ranked[0].window;
1078
+ await daemonCall("window.focus", { wid: win.wid });
1079
+ await daemonCall("intents.execute", {
1080
+ intent: "tile_window",
1081
+ slots: { position: pos, wid: win.wid }
1082
+ }, 3000);
1083
+ console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
1084
+ } catch (e: unknown) {
1085
+ console.log(`Error: ${(e as Error).message}`);
1086
+ }
1087
+ }
1088
+
1089
+ async function sessionsCommand(jsonFlag: boolean): Promise<void> {
1090
+ try {
1091
+ const { daemonCall } = await getDaemonClient();
1092
+ const sessions = await daemonCall("tmux.sessions") as any[];
1093
+ if (jsonFlag) {
1094
+ console.log(JSON.stringify(sessions, null, 2));
1095
+ return;
1096
+ }
1097
+ if (!sessions.length) {
1098
+ console.log("No active sessions.");
1099
+ return;
1100
+ }
1101
+ console.log(`Sessions (${sessions.length}):\n`);
1102
+ for (const s of sessions) {
1103
+ const windows = s.windowCount || s.windows || "?";
1104
+ console.log(` \x1b[1m${s.name}\x1b[0m (${windows} windows)`);
1105
+ }
1106
+ } catch {
1107
+ console.log("Daemon not running. Start with: lattices app");
1108
+ }
1109
+ }
1110
+
1111
+ async function voiceCommand(subcommand?: string, ...rest: string[]): Promise<void> {
1112
+ const { daemonCall } = await getDaemonClient();
1113
+ try {
1114
+ switch (subcommand) {
1115
+ case "status": {
1116
+ const status = await daemonCall("voice.status") as any;
1117
+ console.log(`Provider: ${status.provider}`);
1118
+ console.log(`Available: ${status.available}`);
1119
+ console.log(`Listening: ${status.listening}`);
1120
+ if (status.lastTranscript) console.log(`Last: "${status.lastTranscript}"`);
1121
+ break;
1122
+ }
1123
+ case "simulate":
1124
+ case "sim": {
1125
+ const text = rest.join(" ");
1126
+ if (!text) {
1127
+ console.log("Usage: lattices voice simulate <text>");
1128
+ return;
1129
+ }
1130
+ const execute = !rest.includes("--dry-run");
1131
+ const dryFlag = rest.includes("--dry-run");
1132
+ const cleanText = dryFlag ? rest.filter(r => r !== "--dry-run").join(" ") : text;
1133
+ const result = await daemonCall("voice.simulate", { text: cleanText, execute }, 15000) as any;
1134
+ if (!result.parsed) {
1135
+ console.log(`\x1b[33mNo match:\x1b[0m "${cleanText}"`);
1136
+ return;
1137
+ }
1138
+ const slots = Object.entries(result.slots || {}).map(([k,v]) => `${k}: ${v}`).join(", ");
1139
+ const conf = result.confidence ? ` (${(result.confidence * 100).toFixed(0)}%)` : "";
1140
+ console.log(`\x1b[36m${result.intent}\x1b[0m${slots ? ` ${slots}` : ""}${conf}`);
1141
+ if (result.executed) {
1142
+ console.log(`\x1b[32mExecuted\x1b[0m`);
1143
+ } else if (result.error) {
1144
+ console.log(`\x1b[31mError:\x1b[0m ${result.error}`);
1145
+ }
1146
+ break;
1147
+ }
1148
+ case "intents": {
1149
+ const intents = await daemonCall("intents.list") as any[];
1150
+ for (const intent of intents) {
1151
+ const slots = intent.slots.map((s: any) => `${s.name}:${s.type}${s.required ? "*" : ""}`).join(", ");
1152
+ console.log(` \x1b[1m${intent.intent}\x1b[0m ${intent.description}`);
1153
+ if (slots) console.log(` slots: ${slots}`);
1154
+ console.log(` e.g. "${intent.examples[0]}"`);
1155
+ console.log();
1156
+ }
1157
+ break;
1158
+ }
1159
+ default:
1160
+ console.log("Usage: lattices voice <subcommand>\n");
1161
+ console.log(" status Show voice provider status");
1162
+ console.log(" simulate Parse and execute a voice command");
1163
+ console.log(" intents List all available intents");
1164
+ console.log("\nExamples:");
1165
+ console.log(' lattices voice simulate "tile this left"');
1166
+ console.log(' lattices voice simulate "focus chrome" --dry-run');
1167
+ }
1168
+ } catch (e: unknown) {
1169
+ console.log(`Error: ${(e as Error).message}`);
1170
+ }
1171
+ }
1172
+
1173
+ async function callCommand(method?: string, ...rest: string[]): Promise<void> {
1174
+ if (!method) {
1175
+ console.log("Usage: lattices call <method> [params-json]");
1176
+ console.log("\nExamples:");
1177
+ console.log(" lattices call daemon.status");
1178
+ console.log(" lattices call api.schema");
1179
+ console.log(' lattices call window.place \'{"session":"vox","placement":"left"}\'');
1180
+ return;
1181
+ }
1182
+ try {
1183
+ const { daemonCall } = await getDaemonClient();
1184
+ const params = rest[0] ? JSON.parse(rest[0]) : null;
1185
+ const result = await daemonCall(method, params, 15000);
1186
+ console.log(JSON.stringify(result, null, 2));
1187
+ } catch (e: unknown) {
1188
+ console.log(`Error: ${(e as Error).message}`);
1189
+ }
1190
+ }
1191
+
1192
+ async function layerCommand(index?: string): Promise<void> {
766
1193
  try {
767
1194
  const { daemonCall } = await getDaemonClient();
768
1195
  if (index === undefined || index === null || index === "") {
769
- const result = await daemonCall("layers.list");
1196
+ const result = await daemonCall("layers.list") as any;
770
1197
  if (!result.layers.length) {
771
1198
  console.log("No layers configured.");
772
1199
  return;
@@ -780,21 +1207,21 @@ async function layerCommand(index) {
780
1207
  }
781
1208
  const idx = parseInt(index, 10);
782
1209
  if (!isNaN(idx)) {
783
- await daemonCall("layer.switch", { index: idx });
784
- console.log(`Switched to layer ${idx}`);
1210
+ await daemonCall("layer.activate", { index: idx, mode: "launch" });
1211
+ console.log(`Activated layer ${idx}`);
785
1212
  } else {
786
- await daemonCall("layer.switch", { name: index });
787
- console.log(`Switched to layer "${index}"`);
1213
+ await daemonCall("layer.activate", { name: index, mode: "launch" });
1214
+ console.log(`Activated layer "${index}"`);
788
1215
  }
789
- } catch (e) {
790
- console.log(`Error: ${e.message}`);
1216
+ } catch (e: unknown) {
1217
+ console.log(`Error: ${(e as Error).message}`);
791
1218
  }
792
1219
  }
793
1220
 
794
- async function diagCommand(limit) {
1221
+ async function diagCommand(limit?: string): Promise<void> {
795
1222
  try {
796
1223
  const { daemonCall } = await getDaemonClient();
797
- const result = await daemonCall("diagnostics.list", { limit: parseInt(limit, 10) || 40 });
1224
+ const result = await daemonCall("diagnostics.list", { limit: parseInt(limit || "", 10) || 40 }) as any;
798
1225
  if (!result.entries || !result.entries.length) {
799
1226
  console.log("No diagnostic entries.");
800
1227
  return;
@@ -805,26 +1232,26 @@ async function diagCommand(limit) {
805
1232
  entry.level === "error" ? "\x1b[31m✗\x1b[0m" : "›";
806
1233
  console.log(` \x1b[90m${entry.time}\x1b[0m ${icon} ${entry.message}`);
807
1234
  }
808
- } catch (e) {
809
- console.log(`Error: ${e.message}`);
1235
+ } catch (e: unknown) {
1236
+ console.log(`Error: ${(e as Error).message}`);
810
1237
  }
811
1238
  }
812
1239
 
813
- async function distributeCommand() {
1240
+ async function distributeCommand(): Promise<void> {
814
1241
  try {
815
1242
  const { daemonCall } = await getDaemonClient();
816
- await daemonCall("layout.distribute");
1243
+ await daemonCall("space.optimize", { scope: "visible", strategy: "balanced" });
817
1244
  console.log("Distributed visible windows into grid");
818
1245
  } catch {
819
1246
  console.log("Daemon not running. Start with: lattices app");
820
1247
  }
821
1248
  }
822
1249
 
823
- async function daemonLsCommand() {
1250
+ async function daemonLsCommand(): Promise<boolean> {
824
1251
  try {
825
1252
  const { daemonCall, isDaemonRunning } = await getDaemonClient();
826
1253
  if (!(await isDaemonRunning())) return false;
827
- const sessions = await daemonCall("tmux.sessions");
1254
+ const sessions = await daemonCall("tmux.sessions") as any[];
828
1255
  if (!sessions.length) {
829
1256
  console.log("No active tmux sessions.");
830
1257
  return true;
@@ -832,7 +1259,7 @@ async function daemonLsCommand() {
832
1259
 
833
1260
  // Annotate sessions with workspace group info
834
1261
  const ws = readWorkspaceConfig();
835
- const sessionGroupMap = new Map();
1262
+ const sessionGroupMap = new Map<string, { group: string; tab: string }>();
836
1263
  if (ws?.groups) {
837
1264
  for (const g of ws.groups) {
838
1265
  for (const tab of g.tabs || []) {
@@ -858,14 +1285,14 @@ async function daemonLsCommand() {
858
1285
  }
859
1286
  }
860
1287
 
861
- async function daemonStatusInventory() {
1288
+ async function daemonStatusInventory(): Promise<boolean> {
862
1289
  try {
863
1290
  const { daemonCall, isDaemonRunning } = await getDaemonClient();
864
1291
  if (!(await isDaemonRunning())) return false;
865
- const inv = await daemonCall("tmux.inventory");
1292
+ const inv = await daemonCall("tmux.inventory") as any;
866
1293
 
867
1294
  // Build managed session name set
868
- const managed = new Map();
1295
+ const managed = new Map<string, string>();
869
1296
  const ws = readWorkspaceConfig();
870
1297
  if (ws?.groups) {
871
1298
  for (const g of ws.groups) {
@@ -879,7 +1306,7 @@ async function daemonStatusInventory() {
879
1306
  for (const s of inv.all) {
880
1307
  if (!managed.has(s.name)) {
881
1308
  // Check if it matches a scanned project (via daemon)
882
- const projects = await daemonCall("projects.list");
1309
+ const projects = await daemonCall("projects.list") as any[];
883
1310
  for (const p of projects) {
884
1311
  managed.set(p.sessionName, p.name);
885
1312
  }
@@ -887,7 +1314,7 @@ async function daemonStatusInventory() {
887
1314
  }
888
1315
  }
889
1316
 
890
- const managedSessions = inv.all.filter((s) => managed.has(s.name));
1317
+ const managedSessions = inv.all.filter((s: any) => managed.has(s.name));
891
1318
  const orphanSessions = inv.orphans;
892
1319
 
893
1320
  if (managedSessions.length > 0) {
@@ -926,14 +1353,14 @@ async function daemonStatusInventory() {
926
1353
 
927
1354
  // ── OCR commands ──────────────────────────────────────────────────────
928
1355
 
929
- async function scanCommand(sub, ...rest) {
1356
+ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
930
1357
  const { daemonCall } = await getDaemonClient();
931
1358
 
932
1359
  if (!sub || sub === "snapshot" || sub === "ls" || sub === "--full" || sub === "-f" || sub === "--json") {
933
1360
  const full = sub === "--full" || sub === "-f" || rest.includes("--full") || rest.includes("-f");
934
1361
  const json = sub === "--json" || rest.includes("--json");
935
1362
  try {
936
- const results = await daemonCall("ocr.snapshot", null, 5000);
1363
+ const results = await daemonCall("ocr.snapshot", null, 5000) as any[];
937
1364
  if (!results.length) {
938
1365
  console.log("No scan results yet. The first scan runs ~60s after launch.");
939
1366
  return;
@@ -957,7 +1384,7 @@ async function scanCommand(sub, ...rest) {
957
1384
  }
958
1385
  } else {
959
1386
  const maxPreview = 5;
960
- const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
1387
+ const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
961
1388
  for (const line of preview) {
962
1389
  console.log(` \x1b[90m${line}\x1b[0m`);
963
1390
  }
@@ -983,7 +1410,7 @@ async function scanCommand(sub, ...rest) {
983
1410
  return;
984
1411
  }
985
1412
  try {
986
- const results = await daemonCall("ocr.search", { query }, 5000);
1413
+ const results = await daemonCall("ocr.search", { query }, 5000) as any[];
987
1414
  if (!results.length) {
988
1415
  console.log(`No matches for "${query}".`);
989
1416
  return;
@@ -997,8 +1424,8 @@ async function scanCommand(sub, ...rest) {
997
1424
  console.log(` ${snippet}`);
998
1425
  console.log();
999
1426
  }
1000
- } catch (e) {
1001
- console.log(`Error: ${e.message}`);
1427
+ } catch (e: unknown) {
1428
+ console.log(`Error: ${(e as Error).message}`);
1002
1429
  }
1003
1430
  return;
1004
1431
  }
@@ -1006,9 +1433,9 @@ async function scanCommand(sub, ...rest) {
1006
1433
  if (sub === "recent" || sub === "log") {
1007
1434
  const full = rest.includes("--full") || rest.includes("-f");
1008
1435
  const numArg = rest.find(a => !a.startsWith("-"));
1009
- const limit = parseInt(numArg, 10) || 20;
1436
+ const limit = parseInt(numArg || "", 10) || 20;
1010
1437
  try {
1011
- const results = await daemonCall("ocr.recent", { limit }, 5000);
1438
+ const results = await daemonCall("ocr.recent", { limit }, 5000) as any[];
1012
1439
  if (!results.length) {
1013
1440
  console.log("No history yet. The first scan runs ~60s after launch.");
1014
1441
  return;
@@ -1026,7 +1453,7 @@ async function scanCommand(sub, ...rest) {
1026
1453
  }
1027
1454
  } else {
1028
1455
  const maxPreview = 5;
1029
- const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
1456
+ const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
1030
1457
  for (const line of preview) {
1031
1458
  console.log(` \x1b[90m${line}\x1b[0m`);
1032
1459
  }
@@ -1047,8 +1474,8 @@ async function scanCommand(sub, ...rest) {
1047
1474
  console.log("Triggering deep scan (Vision OCR)...");
1048
1475
  await daemonCall("ocr.scan", null, 30000);
1049
1476
  console.log("Done.");
1050
- } catch (e) {
1051
- console.log(`Error: ${e.message}`);
1477
+ } catch (e: unknown) {
1478
+ console.log(`Error: ${(e as Error).message}`);
1052
1479
  }
1053
1480
  return;
1054
1481
  }
@@ -1060,7 +1487,7 @@ async function scanCommand(sub, ...rest) {
1060
1487
  return;
1061
1488
  }
1062
1489
  try {
1063
- const results = await daemonCall("ocr.history", { wid }, 5000);
1490
+ const results = await daemonCall("ocr.history", { wid }, 5000) as any[];
1064
1491
  if (!results.length) {
1065
1492
  console.log(`No history for wid:${wid}.`);
1066
1493
  return;
@@ -1070,15 +1497,15 @@ async function scanCommand(sub, ...rest) {
1070
1497
  const ts = new Date(r.timestamp * 1000).toLocaleTimeString();
1071
1498
  const src = r.source === "accessibility" ? "\x1b[33mAX\x1b[0m" : "\x1b[35mOCR\x1b[0m";
1072
1499
  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);
1500
+ const preview = lines.slice(0, 2).map((l: string) => l.length > 80 ? l.slice(0, 77) + "..." : l);
1074
1501
  console.log(` \x1b[90m${ts}\x1b[0m ${src} \x1b[1m${r.app}\x1b[0m — "${r.title}"`);
1075
1502
  for (const line of preview) {
1076
1503
  console.log(` \x1b[90m${line}\x1b[0m`);
1077
1504
  }
1078
1505
  console.log();
1079
1506
  }
1080
- } catch (e) {
1081
- console.log(`Error: ${e.message}`);
1507
+ } catch (e: unknown) {
1508
+ console.log(`Error: ${(e as Error).message}`);
1082
1509
  }
1083
1510
  return;
1084
1511
  }
@@ -1097,7 +1524,7 @@ Usage:
1097
1524
  `);
1098
1525
  }
1099
1526
 
1100
- function printUsage() {
1527
+ function printUsage(): void {
1101
1528
  console.log(`lattices — Claude Code + dev server in tmux
1102
1529
 
1103
1530
  Usage:
@@ -1111,17 +1538,31 @@ Usage:
1111
1538
  lattices group [id] List tab groups or launch/attach a group
1112
1539
  lattices groups List all tab groups with status
1113
1540
  lattices tab <group> [tab] Switch tab within a group (by label or index)
1541
+ lattices search <query> Search windows by title, app, session, OCR
1542
+ lattices search <q> --deep Deep search: index + live terminal inspection
1543
+ lattices search <q> --wid Print matching window IDs only (pipeable)
1544
+ lattices search <q> --json JSON output
1545
+ lattices place <query> [pos] Deep search + focus + tile (default: bottom-right)
1546
+ lattices focus <session> Raise a session's window
1114
1547
  lattices windows [--json] List all desktop windows (daemon required)
1115
- lattices focus <session> Focus a session's terminal window (daemon required)
1548
+ lattices sessions [--json] List active tmux sessions via daemon
1116
1549
  lattices tile <position> Tile the frontmost window (left, right, top, etc.)
1117
1550
  lattices distribute Smart-grid all visible windows (daemon required)
1118
1551
  lattices layer [name|index] List layers or switch by name/index (daemon required)
1552
+ lattices voice status Voice provider status
1553
+ lattices voice simulate <t> Parse and execute a voice command
1554
+ lattices voice intents List all available intents
1555
+ lattices call <method> [p] Raw daemon API call (params as JSON)
1119
1556
  lattices scan Show text from all visible windows
1120
1557
  lattices scan --full Full text dump
1121
1558
  lattices scan search <q> Full-text search across scanned windows
1122
1559
  lattices scan recent [n] Show recent scans chronologically
1123
1560
  lattices scan deep Trigger a deep Vision OCR scan
1124
1561
  lattices scan history <wid> Scan timeline for a specific window
1562
+ lattices dev Run dev server (auto-detected)
1563
+ lattices dev build Build the project (swift/node/rust/go/make)
1564
+ lattices dev restart Build + restart (swift app) or just build
1565
+ lattices dev type Print detected project type
1125
1566
  lattices daemon status Show daemon status
1126
1567
  lattices diag [limit] Show diagnostic log entries
1127
1568
  lattices app Launch the menu bar companion app
@@ -1172,7 +1613,7 @@ Layouts:
1172
1613
  `);
1173
1614
  }
1174
1615
 
1175
- function initConfig() {
1616
+ function initConfig(): void {
1176
1617
  const dir = process.cwd();
1177
1618
  const configPath = resolve(dir, ".lattices.json");
1178
1619
 
@@ -1192,7 +1633,7 @@ function initConfig() {
1192
1633
  console.log(JSON.stringify(config, null, 2));
1193
1634
  }
1194
1635
 
1195
- function listSessions() {
1636
+ function listSessions(): void {
1196
1637
  const out = runQuiet(
1197
1638
  "tmux list-sessions -F '#{session_name} (#{session_windows} windows, created #{session_created_string})'"
1198
1639
  );
@@ -1203,7 +1644,7 @@ function listSessions() {
1203
1644
 
1204
1645
  // Annotate sessions that belong to tab groups
1205
1646
  const ws = readWorkspaceConfig();
1206
- const sessionGroupMap = new Map();
1647
+ const sessionGroupMap = new Map<string, { group: string; tab: string }>();
1207
1648
  if (ws?.groups) {
1208
1649
  for (const g of ws.groups) {
1209
1650
  for (const tab of g.tabs || []) {
@@ -1216,7 +1657,7 @@ function listSessions() {
1216
1657
  }
1217
1658
  }
1218
1659
 
1219
- const lines = out.split("\n").map((line) => {
1660
+ const lines = out.split("\n").map((line: string) => {
1220
1661
  const sessionName = line.split(" ")[0];
1221
1662
  const info = sessionGroupMap.get(sessionName);
1222
1663
  return info
@@ -1228,7 +1669,7 @@ function listSessions() {
1228
1669
  console.log(lines.join("\n"));
1229
1670
  }
1230
1671
 
1231
- function killSession(name) {
1672
+ function killSession(name?: string): void {
1232
1673
  if (!name) name = toSessionName(process.cwd());
1233
1674
  if (!sessionExists(name)) {
1234
1675
  console.log(`No session "${name}".`);
@@ -1240,7 +1681,14 @@ function killSession(name) {
1240
1681
 
1241
1682
  // ── Window tiling ────────────────────────────────────────────────────
1242
1683
 
1243
- function getScreenBounds() {
1684
+ interface ScreenBounds {
1685
+ x: number;
1686
+ y: number;
1687
+ w: number;
1688
+ h: number;
1689
+ }
1690
+
1691
+ function getScreenBounds(): ScreenBounds {
1244
1692
  // Get the visible area (excludes menu bar and dock) in AppleScript coordinates (top-left origin)
1245
1693
  const script = `
1246
1694
  tell application "Finder"
@@ -1255,7 +1703,7 @@ function getScreenBounds() {
1255
1703
  }
1256
1704
 
1257
1705
  // Presets return AppleScript bounds: [left, top, right, bottom] within the visible area
1258
- const tilePresets = {
1706
+ const tilePresets: Record<string, (s: ScreenBounds) => number[]> = {
1259
1707
  "left": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
1260
1708
  "left-half": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
1261
1709
  "right": (s) => [s.x + s.w / 2, s.y, s.x + s.w, s.y + s.h],
@@ -1282,7 +1730,7 @@ const tilePresets = {
1282
1730
  "right-third": (s) => [s.x + Math.round(s.w * 0.667), s.y, s.x + s.w, s.y + s.h],
1283
1731
  };
1284
1732
 
1285
- function tileWindow(position) {
1733
+ function tileWindow(position: string): void {
1286
1734
  const preset = tilePresets[position];
1287
1735
  if (!preset) {
1288
1736
  console.log(`Unknown position: ${position}`);
@@ -1302,7 +1750,7 @@ function tileWindow(position) {
1302
1750
  console.log(`Tiled → ${position}`);
1303
1751
  }
1304
1752
 
1305
- function createOrAttach() {
1753
+ function createOrAttach(): void {
1306
1754
  const dir = process.cwd();
1307
1755
  const name = toSessionName(dir);
1308
1756
 
@@ -1323,7 +1771,7 @@ function createOrAttach() {
1323
1771
  attach(name);
1324
1772
  }
1325
1773
 
1326
- function attach(name) {
1774
+ function attach(name: string): void {
1327
1775
  if (isInsideTmux()) {
1328
1776
  execSync(`tmux switch-client -t "${name}"`, { stdio: "inherit" });
1329
1777
  } else {
@@ -1333,7 +1781,7 @@ function attach(name) {
1333
1781
 
1334
1782
  // ── Status / Inventory ───────────────────────────────────────────────
1335
1783
 
1336
- function statusInventory() {
1784
+ function statusInventory(): void {
1337
1785
  // Query all tmux sessions
1338
1786
  const sessionsRaw = runQuiet(
1339
1787
  'tmux list-sessions -F "#{session_name}\t#{session_windows}\t#{session_attached}"'
@@ -1349,17 +1797,17 @@ function statusInventory() {
1349
1797
  );
1350
1798
 
1351
1799
  // Parse panes grouped by session
1352
- const panesBySession = new Map();
1800
+ const panesBySession = new Map<string, { title: string; cmd: string }[]>();
1353
1801
  if (panesRaw) {
1354
1802
  for (const line of panesRaw.split("\n").filter(Boolean)) {
1355
1803
  const [sess, title, cmd] = line.split("\t");
1356
1804
  if (!panesBySession.has(sess)) panesBySession.set(sess, []);
1357
- panesBySession.get(sess).push({ title, cmd });
1805
+ panesBySession.get(sess)!.push({ title, cmd });
1358
1806
  }
1359
1807
  }
1360
1808
 
1361
1809
  // Build managed session name set
1362
- const managed = new Map(); // name -> label
1810
+ const managed = new Map<string, string>(); // name -> label
1363
1811
 
1364
1812
  // From workspace groups
1365
1813
  const ws = readWorkspaceConfig();
@@ -1391,7 +1839,7 @@ function statusInventory() {
1391
1839
  }
1392
1840
 
1393
1841
  // Parse sessions and classify
1394
- const sessions = sessionsRaw.split("\n").filter(Boolean).map((line) => {
1842
+ const sessions = sessionsRaw.split("\n").filter(Boolean).map((line: string) => {
1395
1843
  const [name, windows, attached] = line.split("\t");
1396
1844
  return { name, windows: parseInt(windows) || 1, attached: attached !== "0" };
1397
1845
  });
@@ -1437,10 +1885,7 @@ function statusInventory() {
1437
1885
 
1438
1886
  // ── Main ─────────────────────────────────────────────────────────────
1439
1887
 
1440
- if (!hasTmux()) {
1441
- console.error("tmux is not installed. Install with: brew install tmux");
1442
- process.exit(1);
1443
- }
1888
+ requireTmux(command);
1444
1889
 
1445
1890
  switch (command) {
1446
1891
  case "init":
@@ -1509,9 +1954,25 @@ switch (command) {
1509
1954
  console.log(" lattices window map [--json] Show all layer tags");
1510
1955
  }
1511
1956
  break;
1957
+ case "search":
1958
+ case "s":
1959
+ await searchCommand(args[1], new Set(args.slice(2)));
1960
+ break;
1512
1961
  case "focus":
1513
1962
  await focusCommand(args[1]);
1514
1963
  break;
1964
+ case "place":
1965
+ await placeCommand(args[1], args[2]);
1966
+ break;
1967
+ case "sessions":
1968
+ await sessionsCommand(args[1] === "--json");
1969
+ break;
1970
+ case "voice":
1971
+ await voiceCommand(args[1], ...args.slice(2));
1972
+ break;
1973
+ case "call":
1974
+ await callCommand(args[1], ...args.slice(2));
1975
+ break;
1515
1976
  case "layer":
1516
1977
  case "layers":
1517
1978
  await layerCommand(args[1]);
@@ -1531,13 +1992,15 @@ switch (command) {
1531
1992
  console.log("Usage: lattices daemon status");
1532
1993
  }
1533
1994
  break;
1995
+ case "dev":
1996
+ await devCommand(args[1], ...args.slice(2));
1997
+ break;
1534
1998
  case "app": {
1535
1999
  // Forward to lattices-app script
1536
2000
  const { execFileSync } = await import("node:child_process");
1537
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
1538
- const appScript = resolve(__dirname2, "lattices-app.js");
2001
+ const appScript = resolve(import.meta.dir, "lattices-app.ts");
1539
2002
  try {
1540
- execFileSync("node", [appScript, ...args.slice(1)], { stdio: "inherit" });
2003
+ execFileSync("bun", [appScript, ...args.slice(1)], { stdio: "inherit" });
1541
2004
  } catch { /* exit code forwarded */ }
1542
2005
  break;
1543
2006
  }