@lattices/cli 0.4.14 → 0.5.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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  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/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. 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" });