@lattices/cli 0.4.14 → 0.6.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 (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
package/bin/lattices.ts CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { createHash } from "node:crypto";
4
4
  import { execSync } from "node:child_process";
5
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
- import { basename, resolve } from "node:path";
5
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
+ import { basename, dirname, isAbsolute, resolve } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
 
9
9
  // Daemon client (lazy-loaded to avoid blocking startup for TTY commands)
@@ -95,6 +95,53 @@ function esc(str: string): string {
95
95
  return str.replace(/'/g, "'\\''");
96
96
  }
97
97
 
98
+ function appleScriptString(str: string): string {
99
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
100
+ }
101
+
102
+ function slugify(str: string): string {
103
+ return str
104
+ .toLowerCase()
105
+ .replace(/\.app$/i, "")
106
+ .replace(/[^a-z0-9_-]+/g, "-")
107
+ .replace(/^-+|-+$/g, "") || "app";
108
+ }
109
+
110
+ function parseFlagValue(args: string[], name: string): string | undefined {
111
+ const prefix = `--${name}=`;
112
+ const exact = `--${name}`;
113
+ for (let i = 0; i < args.length; i++) {
114
+ if (args[i].startsWith(prefix)) return args[i].slice(prefix.length);
115
+ if (args[i] === exact) return args[i + 1];
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ function hasFlag(args: string[], name: string): boolean {
121
+ return args.includes(`--${name}`);
122
+ }
123
+
124
+ function nonFlagArgs(args: string[]): string[] {
125
+ const valueFlags = new Set([
126
+ "id", "state", "ttl", "ttlMs", "x", "y", "gap", "placement", "style", "name", "scale",
127
+ "hud-url", "hudUrl", "hud-html", "hudHTML", "hudHtml", "hud-title", "hudTitle",
128
+ "hud-width", "hudWidth", "hud-height", "hudHeight", "width", "height",
129
+ "manifest", "root", "max-depth", "maxDepth", "read-access", "readAccess",
130
+ "pause",
131
+ ]);
132
+ const out: string[] = [];
133
+ for (let i = 0; i < args.length; i++) {
134
+ const arg = args[i];
135
+ if (!arg.startsWith("--")) {
136
+ out.push(arg);
137
+ continue;
138
+ }
139
+ const flagName = arg.slice(2);
140
+ if (!arg.includes("=") && valueFlags.has(flagName)) i++;
141
+ }
142
+ return out;
143
+ }
144
+
98
145
  // ── Config ───────────────────────────────────────────────────────────
99
146
 
100
147
  function readConfig(dir: string): any | null {
@@ -559,19 +606,21 @@ function detectProjectType(dir: string): string | null {
559
606
  return null;
560
607
  }
561
608
 
609
+ async function forwardToLatticesDevHelper(dir: string, cmd: string, extraFlags: string[] = []): Promise<void> {
610
+ const localDevScript = resolve(dir, "bin/lattices-dev");
611
+ const devScript = existsSync(localDevScript) ? localDevScript : resolve(import.meta.dir, "lattices-dev");
612
+ const { execFileSync } = await import("node:child_process");
613
+ try {
614
+ execFileSync(devScript, [cmd, ...extraFlags], { stdio: "inherit" });
615
+ } catch {
616
+ /* exit code forwarded */
617
+ }
618
+ }
619
+
562
620
  async function devCommand(sub?: string, ...flags: string[]): Promise<void> {
563
621
  const dir = process.cwd();
564
622
  const type = detectProjectType(dir);
565
623
 
566
- // Helper to forward to lattices-app.ts
567
- async function forwardToAppScript(cmd: string, extraFlags: string[] = []): Promise<void> {
568
- const appScript = resolve(import.meta.dir, "lattices-app.ts");
569
- const { execFileSync } = await import("node:child_process");
570
- try {
571
- execFileSync("bun", [appScript, cmd, ...extraFlags], { stdio: "inherit" });
572
- } catch { /* exit code forwarded */ }
573
- }
574
-
575
624
  if (!sub) {
576
625
  // bare `lattices dev` — run dev server
577
626
  if (!type) {
@@ -580,7 +629,7 @@ async function devCommand(sub?: string, ...flags: string[]): Promise<void> {
580
629
  }
581
630
  console.log(`Detected: ${type} project`);
582
631
  if (type === "lattices-app") {
583
- await forwardToAppScript("restart", flags);
632
+ await forwardToLatticesDevHelper(dir, "restart", flags);
584
633
  } else if (type === "node") {
585
634
  const cmd = detectDevCommand(dir);
586
635
  if (cmd) {
@@ -604,13 +653,18 @@ async function devCommand(sub?: string, ...flags: string[]): Promise<void> {
604
653
  return;
605
654
  }
606
655
 
656
+ if (sub === "placement-smoke") {
657
+ await placementSmokeCommand(flags);
658
+ return;
659
+ }
660
+
607
661
  if (sub === "build") {
608
662
  if (!type) {
609
663
  console.log("No recognized project in current directory.");
610
664
  return;
611
665
  }
612
666
  if (type === "lattices-app") {
613
- await forwardToAppScript("build");
667
+ await forwardToLatticesDevHelper(dir, "build");
614
668
  } else if (type === "swift") {
615
669
  console.log("Building: swift build -c release");
616
670
  execSync("swift build -c release", { cwd: dir, stdio: "inherit" });
@@ -638,7 +692,7 @@ async function devCommand(sub?: string, ...flags: string[]): Promise<void> {
638
692
 
639
693
  if (sub === "restart") {
640
694
  if (type === "lattices-app") {
641
- await forwardToAppScript("restart", flags);
695
+ await forwardToLatticesDevHelper(dir, "restart", flags);
642
696
  } else {
643
697
  // For other project types, just rebuild
644
698
  await devCommand("build");
@@ -1095,6 +1149,102 @@ async function placeCommand(query?: string, tilePosition?: string): Promise<void
1095
1149
  }
1096
1150
  }
1097
1151
 
1152
+ function pause(ms: number): Promise<void> {
1153
+ return new Promise(resolve => setTimeout(resolve, ms));
1154
+ }
1155
+
1156
+ function receiptLine(receipt: any): string {
1157
+ const id = receipt?.action?.id || "action";
1158
+ const session = receipt?.session || receipt?.target?.session || "?";
1159
+ const wid = receipt?.wid ?? receipt?.target?.wid ?? "?";
1160
+ const status = receipt?.status || "?";
1161
+ const verified = receipt?.verified === true ? "true" : "false";
1162
+ const resolution = receipt?.targetResolution || "?";
1163
+ return ` ${id} session=${session} wid=${wid} status=${status} verified=${verified} resolution=${resolution}`;
1164
+ }
1165
+
1166
+ async function placementSmokeCommand(rawArgs: string[] = []): Promise<void> {
1167
+ const { daemonCall } = await getDaemonClient();
1168
+ const pauseMs = Number(parseFlagValue(rawArgs, "pause") || 1200);
1169
+ const positional = nonFlagArgs(rawArgs);
1170
+
1171
+ let sessions = positional.slice(0, 2);
1172
+ if (sessions.length < 2) {
1173
+ const tmuxSessions = await daemonCall("tmux.sessions") as any[];
1174
+ sessions = tmuxSessions
1175
+ .map(s => s?.name)
1176
+ .filter((name: unknown): name is string => typeof name === "string" && name.startsWith("lattices-place-"))
1177
+ .slice(0, 2);
1178
+ }
1179
+
1180
+ if (sessions.length < 2) {
1181
+ console.log("Need two named sessions. Usage: lattices dev placement-smoke <session-a> <session-b>");
1182
+ console.log("Tip: launch two small lattices fixture projects first, then rerun this command.");
1183
+ return;
1184
+ }
1185
+
1186
+ const [a, b] = sessions;
1187
+ console.log(`Placement smoke: ${a} + ${b}`);
1188
+
1189
+ for (const session of sessions) {
1190
+ const resolved = await daemonCall("window.resolve", {
1191
+ target: { kind: "session", session },
1192
+ placement: "left",
1193
+ }) as any;
1194
+ console.log(` resolve ${session}: wid=${resolved.wid ?? "?"} app=${resolved.app ?? "?"} resolution=${resolved.targetResolution ?? "?"}`);
1195
+ }
1196
+
1197
+ const beats = [
1198
+ {
1199
+ label: "beat 1: halves",
1200
+ actions: [
1201
+ { id: "a-left-half", type: "window.place", target: { kind: "session", session: a }, args: { placement: "left" } },
1202
+ { id: "b-right-half", type: "window.place", target: { kind: "session", session: b }, args: { placement: "right" } },
1203
+ ],
1204
+ },
1205
+ {
1206
+ label: "beat 2: 4x4 corners",
1207
+ actions: [
1208
+ { id: "a-top-left-4x4", type: "window.place", target: { kind: "session", session: a }, args: { placement: "grid:4x4:0,0" } },
1209
+ { id: "b-bottom-right-4x4", type: "window.place", target: { kind: "session", session: b }, args: { placement: "grid:4x4:3,3" } },
1210
+ ],
1211
+ },
1212
+ {
1213
+ label: "beat 3: workbench",
1214
+ actions: [
1215
+ {
1216
+ id: "a-workbench-left",
1217
+ type: "window.place",
1218
+ target: { kind: "session", session: a },
1219
+ args: { placement: { kind: "fractions", x: 0.02, y: 0.05, w: 0.62, h: 0.9 } },
1220
+ },
1221
+ {
1222
+ id: "b-console-right",
1223
+ type: "window.place",
1224
+ target: { kind: "session", session: b },
1225
+ args: { placement: { kind: "fractions", x: 0.67, y: 0.12, w: 0.3, h: 0.76 } },
1226
+ },
1227
+ ],
1228
+ },
1229
+ ];
1230
+
1231
+ for (const beat of beats) {
1232
+ console.log(`\n${beat.label}`);
1233
+ const result = await daemonCall("actions.execute", {
1234
+ source: "placement-smoke",
1235
+ actions: beat.actions,
1236
+ }, 15000) as any;
1237
+ console.log(` batch=${result.status || "?"} request=${result.requestId || "?"}`);
1238
+ for (const receipt of result.receipts || []) {
1239
+ console.log(receiptLine(receipt));
1240
+ }
1241
+ await pause(pauseMs);
1242
+ }
1243
+
1244
+ const focused = await daemonCall("window.focus", { session: a }, 5000) as any;
1245
+ console.log(`\nfocus ${a}: ok=${focused.ok === true} wid=${focused.wid ?? "?"} raised=${focused.raised === true}`);
1246
+ }
1247
+
1098
1248
  async function sessionsCommand(jsonFlag: boolean): Promise<void> {
1099
1249
  try {
1100
1250
  const { daemonCall } = await getDaemonClient();
@@ -1226,6 +1376,787 @@ async function callCommand(method?: string, ...rest: string[]): Promise<void> {
1226
1376
  }
1227
1377
  }
1228
1378
 
1379
+ interface AppActorAsset {
1380
+ id: string;
1381
+ appName: string;
1382
+ appPath: string;
1383
+ bundleIdentifier?: string;
1384
+ iconPath: string;
1385
+ assetDir: string;
1386
+ }
1387
+
1388
+ function plistValue(plistPath: string, key: string): string | undefined {
1389
+ const value = runQuiet(`/usr/libexec/PlistBuddy -c 'Print :${esc(key)}' '${esc(plistPath)}' 2>/dev/null`);
1390
+ return value?.trim() || undefined;
1391
+ }
1392
+
1393
+ function resolveApplication(appQuery: string): string | undefined {
1394
+ const directPath = appQuery.endsWith(".app") ? resolve(appQuery) : undefined;
1395
+ if (directPath && existsSync(directPath)) return directPath.replace(/\/$/, "");
1396
+
1397
+ const script = `POSIX path of (path to application "${appleScriptString(appQuery.replace(/\.app$/i, ""))}")`;
1398
+ const fromLaunchServices = runQuiet(`osascript -e '${esc(script)}' 2>/dev/null`);
1399
+ if (fromLaunchServices) return fromLaunchServices.trim().replace(/\/$/, "");
1400
+
1401
+ const appName = appQuery.endsWith(".app") ? appQuery : `${appQuery}.app`;
1402
+ const fromFind = runQuiet(
1403
+ `find /Applications /System/Applications '${esc(resolve(homedir(), "Applications"))}' -maxdepth 5 -iname '${esc(appName)}' -print -quit 2>/dev/null`
1404
+ );
1405
+ return fromFind?.trim().replace(/\/$/, "") || undefined;
1406
+ }
1407
+
1408
+ function resolveApplicationByBundleIdentifier(bundleIdentifier: string): string | undefined {
1409
+ const script = `POSIX path of (path to application id "${appleScriptString(bundleIdentifier)}")`;
1410
+ const fromLaunchServices = runQuiet(`osascript -e '${esc(script)}' 2>/dev/null`);
1411
+ return fromLaunchServices?.trim().replace(/\/$/, "") || undefined;
1412
+ }
1413
+
1414
+ function iconPathForApplication(appPath: string): string | undefined {
1415
+ const resourcesDir = resolve(appPath, "Contents", "Resources");
1416
+ const infoPlist = resolve(appPath, "Contents", "Info.plist");
1417
+ const iconFile = plistValue(infoPlist, "CFBundleIconFile");
1418
+ const candidates: string[] = [];
1419
+ if (iconFile) {
1420
+ candidates.push(resolve(resourcesDir, iconFile));
1421
+ if (!/\.[a-z0-9]+$/i.test(iconFile)) {
1422
+ candidates.push(resolve(resourcesDir, `${iconFile}.icns`));
1423
+ }
1424
+ }
1425
+ candidates.push(
1426
+ resolve(resourcesDir, "AppIcon.icns"),
1427
+ resolve(resourcesDir, "icon.icns"),
1428
+ resolve(resourcesDir, "electron.icns")
1429
+ );
1430
+ for (const candidate of candidates) {
1431
+ if (existsSync(candidate)) return candidate;
1432
+ }
1433
+ const firstIcns = runQuiet(`find '${esc(resourcesDir)}' -maxdepth 1 -iname '*.icns' -print -quit 2>/dev/null`);
1434
+ return firstIcns?.trim() || undefined;
1435
+ }
1436
+
1437
+ function ensureAppActorAsset(appQuery: string): AppActorAsset {
1438
+ const appPath = resolveApplication(appQuery);
1439
+ if (!appPath) {
1440
+ throw new Error(`Could not find application: ${appQuery}`);
1441
+ }
1442
+
1443
+ const appName = basename(appPath, ".app");
1444
+ const iconPath = iconPathForApplication(appPath);
1445
+ if (!iconPath) {
1446
+ throw new Error(`Could not find an icon resource in ${appPath}`);
1447
+ }
1448
+
1449
+ const id = `${slugify(appName)}-icon`;
1450
+ const assetDir = resolve(homedir(), ".codex", "pets", id);
1451
+ const spritesheetPath = resolve(assetDir, "spritesheet.png");
1452
+ mkdirSync(assetDir, { recursive: true });
1453
+ run(`sips -s format png -Z 192 '${esc(iconPath)}' --out '${esc(spritesheetPath)}' >/dev/null`);
1454
+
1455
+ const metadata = {
1456
+ id,
1457
+ displayName: `${appName} Icon`,
1458
+ description: `A one-frame overlay actor made from the ${appName} application icon.`,
1459
+ spritesheetPath: "spritesheet.png",
1460
+ states: {
1461
+ idle: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1462
+ thinking: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1463
+ working: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1464
+ listening: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1465
+ waiting: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1466
+ ready: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1467
+ },
1468
+ };
1469
+ writeFileSync(resolve(assetDir, "pet.json"), `${JSON.stringify(metadata, null, 2)}\n`);
1470
+
1471
+ const bundleIdentifier = plistValue(resolve(appPath, "Contents", "Info.plist"), "CFBundleIdentifier");
1472
+ return { id, appName, appPath, bundleIdentifier, iconPath, assetDir };
1473
+ }
1474
+
1475
+ function ensureIconActorAsset(idSeed: string, displayName: string, iconPath: string): string {
1476
+ if (!existsSync(iconPath)) {
1477
+ throw new Error(`HUD icon does not exist: ${iconPath}`);
1478
+ }
1479
+
1480
+ const id = `${slugify(idSeed)}-hud-icon`;
1481
+ const assetDir = resolve(homedir(), ".codex", "pets", id);
1482
+ const spritesheetPath = resolve(assetDir, "spritesheet.png");
1483
+ mkdirSync(assetDir, { recursive: true });
1484
+ run(`sips -s format png -Z 192 '${esc(iconPath)}' --out '${esc(spritesheetPath)}' >/dev/null`);
1485
+
1486
+ const metadata = {
1487
+ id,
1488
+ displayName: `${displayName} HUD Icon`,
1489
+ description: `A one-frame overlay actor icon for the ${displayName} HUD.`,
1490
+ spritesheetPath: "spritesheet.png",
1491
+ states: {
1492
+ idle: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1493
+ thinking: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1494
+ working: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1495
+ listening: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1496
+ waiting: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1497
+ ready: { row: 0, frames: 1, frameWidth: 192, frameHeight: 192 },
1498
+ },
1499
+ };
1500
+ writeFileSync(resolve(assetDir, "pet.json"), `${JSON.stringify(metadata, null, 2)}\n`);
1501
+ return id;
1502
+ }
1503
+
1504
+ function actorUsage(): void {
1505
+ console.log(`Usage:
1506
+ lattices actor app <app-name> [message] [--state=idle] [--x=520 --y=340] [--show-label]
1507
+ lattices actor switcher [app-name ...] [--x=420 --y=220 --gap=270] [--show-label]
1508
+ lattices actor hud <actor-id> <url> [--hud-width=360 --hud-height=240]
1509
+ lattices actor show|hide|toggle|status
1510
+
1511
+ Examples:
1512
+ lattices actor app Codex "Building the release"
1513
+ lattices actor app Talkie "Hover for latest state" --hud-url=http://localhost:5173
1514
+ lattices actor hud switch-talkie http://localhost:5173
1515
+ lattices actor switcher Codex Talkie
1516
+ lattices actor toggle
1517
+ lattices actor switcher "Google Chrome" Codex Talkie --show-label --scale=0.8
1518
+ `);
1519
+ }
1520
+
1521
+ async function actorCommand(sub?: string, ...rest: string[]): Promise<void> {
1522
+ if (sub === "app") {
1523
+ await actorAppCommand(rest);
1524
+ return;
1525
+ }
1526
+ if (sub === "switcher") {
1527
+ await actorSwitcherCommand(rest);
1528
+ return;
1529
+ }
1530
+ if (sub === "hud") {
1531
+ await actorHUDCommand(rest);
1532
+ return;
1533
+ }
1534
+ if (sub === "show" || sub === "hide" || sub === "toggle" || sub === "status") {
1535
+ await actorVisibilityCommand(sub, rest);
1536
+ return;
1537
+ }
1538
+ actorUsage();
1539
+ }
1540
+
1541
+ function actorHUDOptions(rest: string[]): Record<string, unknown> {
1542
+ const hudUrl = parseFlagValue(rest, "hud-url") || parseFlagValue(rest, "hudUrl");
1543
+ const hudHTML = parseFlagValue(rest, "hud-html") || parseFlagValue(rest, "hudHTML") || parseFlagValue(rest, "hudHtml");
1544
+ const hudTitle = parseFlagValue(rest, "hud-title") || parseFlagValue(rest, "hudTitle");
1545
+ const hudWidth = parseFlagValue(rest, "hud-width") || parseFlagValue(rest, "hudWidth") || parseFlagValue(rest, "width");
1546
+ const hudHeight = parseFlagValue(rest, "hud-height") || parseFlagValue(rest, "hudHeight") || parseFlagValue(rest, "height");
1547
+ return {
1548
+ ...(hudUrl ? { hudUrl } : {}),
1549
+ ...(hudHTML ? { hudHTML } : {}),
1550
+ ...(hudTitle ? { hudTitle } : {}),
1551
+ ...(hudWidth ? { hudWidth: Number(hudWidth) } : {}),
1552
+ ...(hudHeight ? { hudHeight: Number(hudHeight) } : {}),
1553
+ };
1554
+ }
1555
+
1556
+ function shouldHideActorLabel(rest: string[]): boolean {
1557
+ if (hasFlag(rest, "show-label") || hasFlag(rest, "showLabel")) return false;
1558
+ return true;
1559
+ }
1560
+
1561
+ async function actorHUDCommand(rest: string[]): Promise<void> {
1562
+ const positional = nonFlagArgs(rest);
1563
+ const id = positional[0];
1564
+ if (!id) {
1565
+ actorUsage();
1566
+ return;
1567
+ }
1568
+
1569
+ const { daemonCall } = await getDaemonClient();
1570
+ const url = positional[1];
1571
+ const clear = hasFlag(rest, "clear");
1572
+ const result = await daemonCall("overlay.actor.hud", {
1573
+ id,
1574
+ clear,
1575
+ ...(url && !clear ? { hudUrl: url } : {}),
1576
+ ...actorHUDOptions(rest),
1577
+ }, 15000) as any;
1578
+
1579
+ if (hasFlag(rest, "json")) {
1580
+ console.log(JSON.stringify(result, null, 2));
1581
+ } else if (clear) {
1582
+ console.log(`Cleared HUD for ${id}.`);
1583
+ } else {
1584
+ console.log(`Attached hover HUD to ${id}.`);
1585
+ }
1586
+ }
1587
+
1588
+ async function actorVisibilityCommand(action: string, rest: string[]): Promise<void> {
1589
+ const { daemonCall } = await getDaemonClient();
1590
+ const result = await daemonCall("overlay.actor.visibility", {
1591
+ action,
1592
+ feedback: !hasFlag(rest, "quiet") && action !== "status",
1593
+ }, 15000) as any;
1594
+
1595
+ if (hasFlag(rest, "json")) {
1596
+ console.log(JSON.stringify(result, null, 2));
1597
+ return;
1598
+ }
1599
+
1600
+ const state = result.visible ? "shown" : "hidden";
1601
+ const count = Number(result.actorCount ?? 0);
1602
+ console.log(`Actor layer ${state} (${count} actor${count === 1 ? "" : "s"}).`);
1603
+ }
1604
+
1605
+ async function actorAppCommand(rest: string[]): Promise<void> {
1606
+ const positional = nonFlagArgs(rest);
1607
+ const appQuery = positional[0];
1608
+ if (!appQuery) {
1609
+ actorUsage();
1610
+ return;
1611
+ }
1612
+ const message = positional.slice(1).join(" ") || `Tap to switch to ${appQuery}.`;
1613
+ const asset = ensureAppActorAsset(appQuery);
1614
+ const { daemonCall } = await getDaemonClient();
1615
+ const id = parseFlagValue(rest, "id") || `app-${slugify(asset.appName)}`;
1616
+ const state = parseFlagValue(rest, "state") || "idle";
1617
+ const ttlMs = Number(parseFlagValue(rest, "ttl") || parseFlagValue(rest, "ttlMs") || 0);
1618
+ const x = Number(parseFlagValue(rest, "x") || 520);
1619
+ const y = Number(parseFlagValue(rest, "y") || 340);
1620
+ const placement = parseFlagValue(rest, "placement") || "point";
1621
+ const style = parseFlagValue(rest, "style") || "playful";
1622
+ const dismissible = hasFlag(rest, "dismissible");
1623
+ const labelHidden = shouldHideActorLabel(rest);
1624
+ const closeOnActivate = hasFlag(rest, "close-on-activate") || hasFlag(rest, "closeOnActivate");
1625
+ const scale = Number(parseFlagValue(rest, "scale") || 1);
1626
+
1627
+ const result = await daemonCall("overlay.actor.publish", {
1628
+ id,
1629
+ renderer: "sprite",
1630
+ asset: asset.id,
1631
+ state,
1632
+ name: parseFlagValue(rest, "name") || asset.appName,
1633
+ message,
1634
+ placement,
1635
+ x,
1636
+ y,
1637
+ style,
1638
+ ttlMs,
1639
+ dismissible,
1640
+ labelHidden,
1641
+ closeOnActivate,
1642
+ scale,
1643
+ ...actorHUDOptions(rest),
1644
+ targetApp: asset.appName,
1645
+ targetBundleId: asset.bundleIdentifier,
1646
+ targetAppPath: asset.appPath,
1647
+ }, 15000) as any;
1648
+
1649
+ if (!hasFlag(rest, "no-move")) {
1650
+ await daemonCall("overlay.actor.moveTo", {
1651
+ id,
1652
+ x: x + 40,
1653
+ y: y + 50,
1654
+ durationMs: 700,
1655
+ easing: "spring",
1656
+ }, 15000);
1657
+ }
1658
+
1659
+ if (hasFlag(rest, "json")) {
1660
+ console.log(JSON.stringify({ ...result, asset: asset.id, appPath: asset.appPath }, null, 2));
1661
+ } else {
1662
+ console.log(`Published ${asset.appName} actor (${id}). Click it to switch to ${asset.appName}.`);
1663
+ }
1664
+ }
1665
+
1666
+ async function actorSwitcherCommand(rest: string[]): Promise<void> {
1667
+ const appNames = nonFlagArgs(rest);
1668
+ const apps = appNames.length ? appNames : ["Codex", "Talkie"];
1669
+ const { daemonCall } = await getDaemonClient();
1670
+ const startX = Number(parseFlagValue(rest, "x") || 420);
1671
+ const y = Number(parseFlagValue(rest, "y") || 220);
1672
+ const gap = Number(parseFlagValue(rest, "gap") || 270);
1673
+ const ttlMs = Number(parseFlagValue(rest, "ttl") || parseFlagValue(rest, "ttlMs") || 0);
1674
+ const style = parseFlagValue(rest, "style") || "info";
1675
+ const dismissible = hasFlag(rest, "dismissible");
1676
+ const labelHidden = shouldHideActorLabel(rest);
1677
+ const closeOnActivate = hasFlag(rest, "close-on-activate") || hasFlag(rest, "closeOnActivate");
1678
+ const scale = Number(parseFlagValue(rest, "scale") || 1);
1679
+ const results: any[] = [];
1680
+
1681
+ for (let i = 0; i < apps.length; i++) {
1682
+ const asset = ensureAppActorAsset(apps[i]);
1683
+ const id = `switch-${slugify(asset.appName)}`;
1684
+ const x = startX + i * gap;
1685
+ const result = await daemonCall("overlay.actor.publish", {
1686
+ id,
1687
+ renderer: "sprite",
1688
+ asset: asset.id,
1689
+ state: "ready",
1690
+ name: asset.appName,
1691
+ message: `Tap to switch to ${asset.appName}.`,
1692
+ placement: "point",
1693
+ x,
1694
+ y,
1695
+ style,
1696
+ ttlMs,
1697
+ dismissible,
1698
+ labelHidden,
1699
+ closeOnActivate,
1700
+ scale,
1701
+ ...actorHUDOptions(rest),
1702
+ targetApp: asset.appName,
1703
+ targetBundleId: asset.bundleIdentifier,
1704
+ targetAppPath: asset.appPath,
1705
+ }, 15000) as any;
1706
+ results.push({ ...result, asset: asset.id, appPath: asset.appPath });
1707
+ await daemonCall("overlay.actor.moveTo", {
1708
+ id,
1709
+ x: x + 28,
1710
+ y: y + 36,
1711
+ durationMs: 650,
1712
+ easing: "spring",
1713
+ }, 15000);
1714
+ }
1715
+
1716
+ if (hasFlag(rest, "json")) {
1717
+ console.log(JSON.stringify(results, null, 2));
1718
+ } else {
1719
+ console.log(`Published app switcher for ${apps.join(", ")}.`);
1720
+ }
1721
+ }
1722
+
1723
+ type HUDPathField = string | {
1724
+ path?: string;
1725
+ format?: string;
1726
+ schema?: string;
1727
+ presentation?: string;
1728
+ title?: string;
1729
+ description?: string;
1730
+ pollMs?: number;
1731
+ };
1732
+
1733
+ interface HUDManifest {
1734
+ version?: number;
1735
+ manifestVersion?: number;
1736
+ id?: string;
1737
+ name?: string;
1738
+ bundleId?: string;
1739
+ bundleIdentifier?: string;
1740
+ app?: string;
1741
+ appPath?: string;
1742
+ icon?: string;
1743
+ entry?: string;
1744
+ readAccess?: string | string[];
1745
+ state?: HUDPathField;
1746
+ events?: HUDPathField | HUDPathField[];
1747
+ log?: HUDPathField;
1748
+ logs?: HUDPathField[];
1749
+ sources?: HUDPathField[] | Record<string, HUDPathField>;
1750
+ surface?: {
1751
+ width?: number;
1752
+ height?: number;
1753
+ title?: string;
1754
+ transparent?: boolean;
1755
+ };
1756
+ actor?: {
1757
+ id?: string;
1758
+ message?: string;
1759
+ state?: string;
1760
+ x?: number;
1761
+ y?: number;
1762
+ placement?: string;
1763
+ style?: string;
1764
+ scale?: number;
1765
+ labelHidden?: boolean;
1766
+ closeOnActivate?: boolean;
1767
+ click?: string | { type?: string };
1768
+ };
1769
+ }
1770
+
1771
+ interface ResolvedHUDManifest {
1772
+ manifestPath: string;
1773
+ rootDir: string;
1774
+ manifest: HUDManifest;
1775
+ id: string;
1776
+ name: string;
1777
+ entry: string;
1778
+ iconPath?: string;
1779
+ appPath?: string;
1780
+ bundleIdentifier?: string;
1781
+ readAccessPath?: string;
1782
+ }
1783
+
1784
+ interface HUDRegistryEntry {
1785
+ id: string;
1786
+ name?: string;
1787
+ bundleIdentifier?: string;
1788
+ manifestPath: string;
1789
+ registeredAt: string;
1790
+ lastPublishedAt?: string;
1791
+ }
1792
+
1793
+ interface HUDRegistry {
1794
+ version: 1;
1795
+ entries: HUDRegistryEntry[];
1796
+ }
1797
+
1798
+ function hudUsage(): void {
1799
+ console.log(`Usage:
1800
+ lattices hud register [manifest] [--publish] Register .lattices/hud/manifest.json
1801
+ lattices hud publish [manifest-or-id] Publish one HUD actor now
1802
+ lattices hud sync Publish all registered HUD actors
1803
+ lattices hud list List registered HUDs
1804
+ lattices hud discover [root] [--register] Find HUD manifests under a folder
1805
+
1806
+ Manifest:
1807
+ .lattices/hud/manifest.json
1808
+
1809
+ Examples:
1810
+ lattices hud register .lattices/hud/manifest.json --publish
1811
+ lattices hud publish talkie --x=520 --y=340
1812
+ lattices hud sync
1813
+ `);
1814
+ }
1815
+
1816
+ function hudRegistryPath(): string {
1817
+ return resolve(homedir(), ".lattices", "huds.json");
1818
+ }
1819
+
1820
+ function readHUDRegistry(): HUDRegistry {
1821
+ const path = hudRegistryPath();
1822
+ if (!existsSync(path)) return { version: 1, entries: [] };
1823
+ try {
1824
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<HUDRegistry>;
1825
+ return {
1826
+ version: 1,
1827
+ entries: Array.isArray(parsed.entries) ? parsed.entries : [],
1828
+ };
1829
+ } catch (e: unknown) {
1830
+ throw new Error(`Invalid HUD registry ${path}: ${(e as Error).message}`);
1831
+ }
1832
+ }
1833
+
1834
+ function writeHUDRegistry(registry: HUDRegistry): void {
1835
+ const path = hudRegistryPath();
1836
+ mkdirSync(dirname(path), { recursive: true });
1837
+ writeFileSync(path, `${JSON.stringify(registry, null, 2)}\n`);
1838
+ }
1839
+
1840
+ function isDirectory(path: string): boolean {
1841
+ try {
1842
+ return statSync(path).isDirectory();
1843
+ } catch {
1844
+ return false;
1845
+ }
1846
+ }
1847
+
1848
+ function isURLLike(value: string): boolean {
1849
+ return /^[a-z][a-z0-9+.-]*:/i.test(value);
1850
+ }
1851
+
1852
+ function resolveHUDPath(rootDir: string, value: HUDPathField | undefined, fallback?: string): string | undefined {
1853
+ const raw = typeof value === "string" ? value : value?.path;
1854
+ const path = raw || fallback;
1855
+ if (!path) return undefined;
1856
+ if (isURLLike(path)) return path;
1857
+ if (path.startsWith("~/")) return resolve(homedir(), path.slice(2));
1858
+ return isAbsolute(path) ? path : resolve(rootDir, path);
1859
+ }
1860
+
1861
+ function resolveHUDReadAccess(rootDir: string, manifest: HUDManifest, rest: string[] = []): string {
1862
+ const flagValue = parseFlagValue(rest, "read-access") || parseFlagValue(rest, "readAccess");
1863
+ const declared = flagValue
1864
+ ?? (Array.isArray(manifest.readAccess) ? manifest.readAccess[0] : manifest.readAccess);
1865
+ if (!declared) return rootDir;
1866
+ if (isURLLike(declared)) return rootDir;
1867
+ if (declared.startsWith("~/")) return resolve(homedir(), declared.slice(2));
1868
+ return isAbsolute(declared) ? declared : resolve(rootDir, declared);
1869
+ }
1870
+
1871
+ function resolveHUDManifestInput(input?: string): string {
1872
+ if (!input) {
1873
+ const defaultPath = resolve(process.cwd(), ".lattices", "hud", "manifest.json");
1874
+ if (existsSync(defaultPath)) return defaultPath;
1875
+ throw new Error("No manifest provided and .lattices/hud/manifest.json was not found.");
1876
+ }
1877
+
1878
+ const candidate = resolve(input);
1879
+ if (existsSync(candidate)) {
1880
+ return isDirectory(candidate) ? resolve(candidate, "manifest.json") : candidate;
1881
+ }
1882
+
1883
+ const registry = readHUDRegistry();
1884
+ const entry = registry.entries.find((item) => item.id === input);
1885
+ if (entry) return entry.manifestPath;
1886
+
1887
+ throw new Error(`HUD manifest or registered id not found: ${input}`);
1888
+ }
1889
+
1890
+ function readHUDManifest(input?: string): ResolvedHUDManifest {
1891
+ const manifestPath = resolveHUDManifestInput(input);
1892
+ if (!existsSync(manifestPath)) {
1893
+ throw new Error(`HUD manifest does not exist: ${manifestPath}`);
1894
+ }
1895
+
1896
+ const rootDir = dirname(manifestPath);
1897
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as HUDManifest;
1898
+ const id = manifest.actor?.id || manifest.id;
1899
+ if (!id) throw new Error(`HUD manifest is missing id: ${manifestPath}`);
1900
+
1901
+ const name = manifest.name || id;
1902
+ const entry = resolveHUDPath(rootDir, manifest.entry, "./index.html");
1903
+ if (!entry) throw new Error(`HUD manifest is missing entry: ${manifestPath}`);
1904
+ if (!isURLLike(entry) && !existsSync(entry)) {
1905
+ throw new Error(`HUD entry does not exist: ${entry}`);
1906
+ }
1907
+
1908
+ const iconPath = resolveHUDPath(rootDir, manifest.icon);
1909
+ const appPath = resolveHUDPath(rootDir, manifest.appPath)
1910
+ ?? (manifest.bundleId || manifest.bundleIdentifier
1911
+ ? resolveApplicationByBundleIdentifier(manifest.bundleId || manifest.bundleIdentifier || "")
1912
+ : undefined)
1913
+ ?? (manifest.app ? resolveApplication(manifest.app) : undefined);
1914
+ const bundleIdentifier = manifest.bundleId
1915
+ ?? manifest.bundleIdentifier
1916
+ ?? (appPath ? plistValue(resolve(appPath, "Contents", "Info.plist"), "CFBundleIdentifier") : undefined);
1917
+
1918
+ return {
1919
+ manifestPath,
1920
+ rootDir,
1921
+ manifest,
1922
+ id,
1923
+ name,
1924
+ entry,
1925
+ iconPath: iconPath && !isURLLike(iconPath) ? iconPath : undefined,
1926
+ appPath: appPath && !isURLLike(appPath) ? appPath : undefined,
1927
+ bundleIdentifier,
1928
+ readAccessPath: resolveHUDReadAccess(rootDir, manifest),
1929
+ };
1930
+ }
1931
+
1932
+ function numberFlag(rest: string[], name: string, fallback: number): number {
1933
+ const raw = parseFlagValue(rest, name);
1934
+ if (!raw) return fallback;
1935
+ const value = Number(raw);
1936
+ return Number.isFinite(value) ? value : fallback;
1937
+ }
1938
+
1939
+ function numberFlagAny(rest: string[], names: string[], fallback: number): number {
1940
+ for (const name of names) {
1941
+ const raw = parseFlagValue(rest, name);
1942
+ if (!raw) continue;
1943
+ const value = Number(raw);
1944
+ if (Number.isFinite(value)) return value;
1945
+ }
1946
+ return fallback;
1947
+ }
1948
+
1949
+ function hudActorAsset(resolved: ResolvedHUDManifest): string | undefined {
1950
+ if (resolved.iconPath) {
1951
+ return ensureIconActorAsset(resolved.id, resolved.name, resolved.iconPath);
1952
+ }
1953
+
1954
+ const appQuery = resolved.appPath || resolved.manifest.app;
1955
+ if (!appQuery) return undefined;
1956
+
1957
+ try {
1958
+ return ensureAppActorAsset(appQuery).id;
1959
+ } catch {
1960
+ return undefined;
1961
+ }
1962
+ }
1963
+
1964
+ function hudClickType(manifest: HUDManifest): string {
1965
+ const click = manifest.actor?.click;
1966
+ if (!click) return "activateApp";
1967
+ return typeof click === "string" ? click : click.type || "activateApp";
1968
+ }
1969
+
1970
+ function hudPublishPayload(resolved: ResolvedHUDManifest, rest: string[], index = 0): Record<string, unknown> {
1971
+ const manifest = resolved.manifest;
1972
+ const actor = manifest.actor ?? {};
1973
+ const surface = manifest.surface ?? {};
1974
+ const targetEnabled = hudClickType(manifest) !== "none";
1975
+ const asset = hudActorAsset(resolved);
1976
+ const x = numberFlag(rest, "x", actor.x ?? 420 + index * 112);
1977
+ const y = numberFlag(rest, "y", actor.y ?? 220);
1978
+
1979
+ return {
1980
+ id: resolved.id,
1981
+ renderer: "sprite",
1982
+ ...(asset ? { asset } : {}),
1983
+ state: parseFlagValue(rest, "state") || actor.state || "ready",
1984
+ name: parseFlagValue(rest, "name") || resolved.name,
1985
+ message: actor.message || `Hover for ${resolved.name} status.`,
1986
+ placement: parseFlagValue(rest, "placement") || actor.placement || "point",
1987
+ x,
1988
+ y,
1989
+ style: parseFlagValue(rest, "style") || actor.style || "info",
1990
+ labelHidden: actor.labelHidden ?? true,
1991
+ closeOnActivate: actor.closeOnActivate ?? false,
1992
+ scale: numberFlag(rest, "scale", actor.scale ?? 1),
1993
+ hudUrl: resolved.entry,
1994
+ hudTitle: surface.title || resolved.name,
1995
+ hudWidth: numberFlagAny(rest, ["hud-width", "hudWidth", "width"], surface.width ?? 380),
1996
+ hudHeight: numberFlagAny(rest, ["hud-height", "hudHeight", "height"], surface.height ?? 260),
1997
+ hudReadAccess: resolveHUDReadAccess(resolved.rootDir, manifest, rest),
1998
+ ...(targetEnabled && resolved.bundleIdentifier ? { targetBundleId: resolved.bundleIdentifier } : {}),
1999
+ ...(targetEnabled && resolved.appPath ? { targetAppPath: resolved.appPath } : {}),
2000
+ ...(targetEnabled && manifest.app ? { targetApp: manifest.app } : {}),
2001
+ };
2002
+ }
2003
+
2004
+ function upsertHUDRegistryEntry(resolved: ResolvedHUDManifest, published = false): HUDRegistryEntry {
2005
+ const registry = readHUDRegistry();
2006
+ const now = new Date().toISOString();
2007
+ const existing = registry.entries.find((entry) => entry.id === resolved.id);
2008
+ const next: HUDRegistryEntry = {
2009
+ id: resolved.id,
2010
+ name: resolved.name,
2011
+ bundleIdentifier: resolved.bundleIdentifier,
2012
+ manifestPath: resolved.manifestPath,
2013
+ registeredAt: existing?.registeredAt ?? now,
2014
+ lastPublishedAt: published ? now : existing?.lastPublishedAt,
2015
+ };
2016
+ registry.entries = [
2017
+ next,
2018
+ ...registry.entries.filter((entry) => entry.id !== resolved.id),
2019
+ ].sort((a, b) => a.id.localeCompare(b.id));
2020
+ writeHUDRegistry(registry);
2021
+ return next;
2022
+ }
2023
+
2024
+ async function publishHUDManifest(resolved: ResolvedHUDManifest, rest: string[], index = 0): Promise<Record<string, unknown>> {
2025
+ const { daemonCall } = await getDaemonClient();
2026
+ const payload = hudPublishPayload(resolved, rest, index);
2027
+ const result = await daemonCall("overlay.actor.publish", payload, 15000) as Record<string, unknown>;
2028
+ if (!hasFlag(rest, "no-move")) {
2029
+ await daemonCall("overlay.actor.moveTo", {
2030
+ id: resolved.id,
2031
+ x: Number(payload.x) + 24,
2032
+ y: Number(payload.y) + 30,
2033
+ durationMs: 600,
2034
+ easing: "spring",
2035
+ }, 15000);
2036
+ }
2037
+ upsertHUDRegistryEntry(resolved, true);
2038
+ return result;
2039
+ }
2040
+
2041
+ async function hudRegisterCommand(rest: string[]): Promise<void> {
2042
+ const manifestArg = nonFlagArgs(rest)[0] || parseFlagValue(rest, "manifest");
2043
+ const resolved = readHUDManifest(manifestArg);
2044
+ const entry = upsertHUDRegistryEntry(resolved, false);
2045
+
2046
+ if (hasFlag(rest, "publish")) {
2047
+ await publishHUDManifest(resolved, rest);
2048
+ }
2049
+
2050
+ const published = hasFlag(rest, "publish");
2051
+ if (hasFlag(rest, "json")) {
2052
+ console.log(JSON.stringify(entry, null, 2));
2053
+ } else {
2054
+ console.log(`${published ? "Registered and published" : "Registered"} HUD ${resolved.id} -> ${resolved.manifestPath}`);
2055
+ }
2056
+ }
2057
+
2058
+ async function hudPublishCommand(rest: string[]): Promise<void> {
2059
+ const manifestArg = nonFlagArgs(rest)[0] || parseFlagValue(rest, "manifest");
2060
+ const resolved = readHUDManifest(manifestArg);
2061
+ const result = await publishHUDManifest(resolved, rest);
2062
+
2063
+ if (hasFlag(rest, "json")) {
2064
+ console.log(JSON.stringify({ ...result, manifestPath: resolved.manifestPath }, null, 2));
2065
+ } else {
2066
+ console.log(`Published HUD actor ${resolved.id}. Hover it for ${resolved.name}.`);
2067
+ }
2068
+ }
2069
+
2070
+ async function hudSyncCommand(rest: string[]): Promise<void> {
2071
+ const registry = readHUDRegistry();
2072
+ const results: Record<string, unknown>[] = [];
2073
+ for (let i = 0; i < registry.entries.length; i++) {
2074
+ const resolved = readHUDManifest(registry.entries[i].id);
2075
+ results.push(await publishHUDManifest(resolved, rest, i));
2076
+ }
2077
+
2078
+ if (hasFlag(rest, "json")) {
2079
+ console.log(JSON.stringify(results, null, 2));
2080
+ } else {
2081
+ console.log(`Published ${results.length} registered HUD actor${results.length === 1 ? "" : "s"}.`);
2082
+ }
2083
+ }
2084
+
2085
+ function hudListCommand(rest: string[]): void {
2086
+ const registry = readHUDRegistry();
2087
+ if (hasFlag(rest, "json")) {
2088
+ console.log(JSON.stringify(registry, null, 2));
2089
+ return;
2090
+ }
2091
+ if (!registry.entries.length) {
2092
+ console.log("No registered HUDs. Run lattices hud register .lattices/hud/manifest.json");
2093
+ return;
2094
+ }
2095
+ console.log("Registered HUDs:\n");
2096
+ for (const entry of registry.entries) {
2097
+ console.log(` ${entry.id}${entry.name ? ` (${entry.name})` : ""}`);
2098
+ console.log(` manifest: ${entry.manifestPath}`);
2099
+ if (entry.bundleIdentifier) console.log(` bundle: ${entry.bundleIdentifier}`);
2100
+ if (entry.lastPublishedAt) console.log(` shown: ${entry.lastPublishedAt}`);
2101
+ console.log();
2102
+ }
2103
+ }
2104
+
2105
+ function hudDiscoverCommand(rest: string[]): void {
2106
+ const root = resolve(nonFlagArgs(rest)[0] || parseFlagValue(rest, "root") || process.cwd());
2107
+ const maxDepth = Number(parseFlagValue(rest, "max-depth") || parseFlagValue(rest, "maxDepth") || 6);
2108
+ const out = runQuiet(`find '${esc(root)}' -maxdepth ${maxDepth} -path '*/.lattices/hud/manifest.json' -print 2>/dev/null`);
2109
+ const manifests = out ? out.split("\n").filter(Boolean) : [];
2110
+
2111
+ if (hasFlag(rest, "register")) {
2112
+ for (const manifestPath of manifests) {
2113
+ upsertHUDRegistryEntry(readHUDManifest(manifestPath), false);
2114
+ }
2115
+ }
2116
+
2117
+ if (hasFlag(rest, "json")) {
2118
+ console.log(JSON.stringify(manifests, null, 2));
2119
+ return;
2120
+ }
2121
+
2122
+ if (!manifests.length) {
2123
+ console.log(`No HUD manifests found under ${root}`);
2124
+ return;
2125
+ }
2126
+ for (const manifestPath of manifests) console.log(manifestPath);
2127
+ if (hasFlag(rest, "register")) {
2128
+ console.log(`\nRegistered ${manifests.length} HUD manifest${manifests.length === 1 ? "" : "s"}.`);
2129
+ }
2130
+ }
2131
+
2132
+ async function hudCommand(sub?: string, ...rest: string[]): Promise<void> {
2133
+ try {
2134
+ switch (sub) {
2135
+ case "register":
2136
+ await hudRegisterCommand(rest);
2137
+ return;
2138
+ case "publish":
2139
+ case "show":
2140
+ await hudPublishCommand(rest);
2141
+ return;
2142
+ case "sync":
2143
+ await hudSyncCommand(rest);
2144
+ return;
2145
+ case "list":
2146
+ case "ls":
2147
+ hudListCommand(rest);
2148
+ return;
2149
+ case "discover":
2150
+ hudDiscoverCommand(rest);
2151
+ return;
2152
+ default:
2153
+ hudUsage();
2154
+ }
2155
+ } catch (e: unknown) {
2156
+ console.log(`Error: ${(e as Error).message}`);
2157
+ }
2158
+ }
2159
+
1229
2160
  async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
1230
2161
  try {
1231
2162
  const { daemonCall } = await getDaemonClient();
@@ -1749,6 +2680,12 @@ Usage:
1749
2680
  lattices voice status Voice provider status
1750
2681
  lattices voice simulate <t> Parse and execute a voice command
1751
2682
  lattices voice intents List all available intents
2683
+ lattices actor app <app> [message] Show a clickable app-icon actor
2684
+ lattices actor switcher [apps...] Show a clickable app switcher row
2685
+ lattices actor hud <id> <url> Attach a hover web HUD to an actor
2686
+ lattices actor toggle Hide/show the sticky actor layer
2687
+ lattices hud register [manifest] Register a .lattices/hud/manifest.json
2688
+ lattices hud publish [id|manifest] Publish a registered/static HUD actor
1752
2689
  lattices assistant plan <t> Preview the TS assistant planner
1753
2690
  lattices call <method> [p] Raw daemon API call (params as JSON)
1754
2691
  lattices scan Show text from all visible windows
@@ -1760,11 +2697,12 @@ Usage:
1760
2697
  lattices dev Run dev server (auto-detected)
1761
2698
  lattices dev build Build the project (swift/node/rust/go/make)
1762
2699
  lattices dev restart Build + restart (swift app) or just build
2700
+ lattices dev placement-smoke [a] [b] Move two named sessions through verified placements
1763
2701
  lattices dev type Print detected project type
1764
2702
  lattices mouse Find mouse — sonar pulse at cursor position
1765
2703
  lattices mouse summon Summon mouse to screen center
1766
2704
  lattices daemon status Show daemon status
1767
- lattices diag [limit] Show diagnostic log entries
2705
+ lattices logs [limit] Show activity log entries (aliases: log, activity, diag)
1768
2706
  lattices app Launch the menu bar companion app
1769
2707
  lattices app update Download the latest menu bar app and relaunch
1770
2708
  lattices app build Rebuild the menu bar app
@@ -2288,6 +3226,14 @@ switch (command) {
2288
3226
  case "voice":
2289
3227
  await voiceCommand(args[1], ...args.slice(2));
2290
3228
  break;
3229
+ case "actor":
3230
+ case "actors":
3231
+ await actorCommand(args[1], ...args.slice(2));
3232
+ break;
3233
+ case "hud":
3234
+ case "huds":
3235
+ await hudCommand(args[1], ...args.slice(2));
3236
+ break;
2291
3237
  case "assistant":
2292
3238
  await assistantCommand(args[1], ...args.slice(2));
2293
3239
  break;
@@ -2300,6 +3246,9 @@ switch (command) {
2300
3246
  break;
2301
3247
  case "diag":
2302
3248
  case "diagnostics":
3249
+ case "log":
3250
+ case "logs":
3251
+ case "activity":
2303
3252
  await diagCommand(args[1]);
2304
3253
  break;
2305
3254
  case "scan":
@@ -2320,8 +3269,20 @@ switch (command) {
2320
3269
  await devCommand(args[1], ...args.slice(2));
2321
3270
  break;
2322
3271
  case "app": {
2323
- // Forward to lattices-app script
2324
3272
  const { execFileSync } = await import("node:child_process");
3273
+ const dir = process.cwd();
3274
+ const first = args[1];
3275
+ const appSubcommand = first && !first.startsWith("-") ? first : "launch";
3276
+ const appFlags = first && !first.startsWith("-") ? args.slice(2) : args.slice(1);
3277
+ const devAppCommands = new Set(["launch", "start", "build", "restart", "quit", "stop"]);
3278
+
3279
+ if (detectProjectType(dir) === "lattices-app" && devAppCommands.has(appSubcommand)) {
3280
+ console.log("Using local dev app bundle so macOS permissions stay attached across rebuilds.");
3281
+ await forwardToLatticesDevHelper(dir, appSubcommand, appFlags);
3282
+ break;
3283
+ }
3284
+
3285
+ // Forward release/package app commands to lattices-app script.
2325
3286
  const appScript = resolve(import.meta.dir, "lattices-app.ts");
2326
3287
  try {
2327
3288
  execFileSync("bun", [appScript, ...args.slice(1)], { stdio: "inherit" });