@lattices/cli 0.4.9 → 0.4.11

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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
  31. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  32. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  35. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  36. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  37. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  38. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  40. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  41. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  42. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  43. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  44. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  45. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  46. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  47. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
  48. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
  49. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  50. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  51. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  52. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  53. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  54. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  55. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  56. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  57. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  58. package/bin/handsoff-infer.ts +14 -5
  59. package/bin/handsoff-worker.ts +11 -7
  60. package/bin/infer.ts +406 -0
  61. package/bin/lattices-app.ts +57 -7
  62. package/bin/lattices-dev +40 -1
  63. package/bin/lattices.ts +1 -1
  64. package/docs/agent-execution-plan.md +9 -9
  65. package/docs/api.md +119 -0
  66. package/docs/app.md +1 -0
  67. package/docs/companion-deck.md +1 -1
  68. package/docs/gesture-customization-proposal.md +520 -0
  69. package/docs/mouse-gestures.md +79 -0
  70. package/docs/overview.md +2 -2
  71. package/docs/presentation-execution-review.md +9 -9
  72. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  73. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  74. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  75. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  76. package/docs/reference/dewey.config.ts +74 -0
  77. package/docs/reference/install-agent.md +79 -0
  78. package/docs/repo-structure.md +100 -0
  79. package/docs/voice-error-model.md +7 -7
  80. package/docs/voice.md +18 -0
  81. package/package.json +23 -13
  82. package/swift/Package.swift +20 -0
  83. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  84. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  85. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  86. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  87. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  88. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  89. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  90. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  91. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  92. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  93. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  94. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  95. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  96. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  97. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  98. /package/{app → apps/mac}/Info.plist +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  100. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  101. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  102. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  103. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  107. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  108. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
package/bin/infer.ts ADDED
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Lattices inference wrapper — thin layer over Vercel AI SDK.
3
+ *
4
+ * Features:
5
+ * - Multi-provider: groq, openai, anthropic, google, xai
6
+ * - Credential loading: env vars → .env.local/.env → ~/.lattices/inference.json → ~/.config/speakeasy/settings.json
7
+ * - Instrumented: every call logged with timing, model, token usage
8
+ * - Simple API: `await infer("do something", { provider: "groq" })`
9
+ */
10
+
11
+ import { generateText, type ModelMessage } from "ai";
12
+ import { createOpenAI } from "@ai-sdk/openai";
13
+ import { createAnthropic } from "@ai-sdk/anthropic";
14
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
15
+ import { createXai } from "@ai-sdk/xai";
16
+ import { readFileSync, existsSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { join } from "path";
19
+
20
+ // ── Types ──────────────────────────────────────────────────────────
21
+
22
+ export type ProviderName = "groq" | "openai" | "anthropic" | "google" | "xai" | "minimax";
23
+
24
+ export interface InferOptions {
25
+ provider?: ProviderName;
26
+ model?: string;
27
+ system?: string;
28
+ messages?: ModelMessage[];
29
+ temperature?: number;
30
+ maxTokens?: number;
31
+ /** Tag for logging — e.g. "hands-off", "voice-fallback" */
32
+ tag?: string;
33
+ /** Abort signal for cancellation/timeout */
34
+ abortSignal?: AbortSignal;
35
+ }
36
+
37
+ export interface InferResult {
38
+ text: string;
39
+ provider: ProviderName;
40
+ model: string;
41
+ durationMs: number;
42
+ usage?: {
43
+ promptTokens?: number;
44
+ completionTokens?: number;
45
+ totalTokens?: number;
46
+ };
47
+ }
48
+
49
+ // ── Default models per provider ────────────────────────────────────
50
+
51
+ const PROVIDER_NAMES: ProviderName[] = ["groq", "openai", "anthropic", "google", "xai", "minimax"];
52
+ const VOICE_PROVIDER_PRIORITY: ProviderName[] = ["groq", "xai", "openai", "google", "anthropic", "minimax"];
53
+
54
+ const DEFAULT_MODELS: Record<ProviderName, string> = {
55
+ groq: "llama-3.3-70b-versatile",
56
+ openai: "gpt-4o-mini",
57
+ anthropic: "claude-sonnet-4-6",
58
+ google: "gemini-2.0-flash",
59
+ xai: "grok-4-1-fast-non-reasoning",
60
+ minimax: "MiniMax-M2.5-highspeed",
61
+ };
62
+
63
+ const VOICE_DEFAULT_MODELS: Record<ProviderName, string> = {
64
+ ...DEFAULT_MODELS,
65
+ groq: "llama-3.1-8b-instant",
66
+ };
67
+
68
+ // ── Credential loading ─────────────────────────────────────────────
69
+
70
+ interface CredentialStore {
71
+ groq?: string;
72
+ openai?: string;
73
+ anthropic?: string;
74
+ google?: string;
75
+ xai?: string;
76
+ minimax?: string;
77
+ }
78
+
79
+ let _cachedCreds: CredentialStore | null = null;
80
+ let _cachedLocalEnv: Record<string, string> | null = null;
81
+
82
+ function parseDotEnv(content: string): Record<string, string> {
83
+ const env: Record<string, string> = {};
84
+
85
+ for (const rawLine of content.split(/\r?\n/)) {
86
+ const line = rawLine.trim();
87
+ if (!line || line.startsWith("#")) continue;
88
+
89
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
90
+ if (!match) continue;
91
+
92
+ const [, key, rawValue] = match;
93
+ let value = rawValue.trim();
94
+ const quote = value[0];
95
+ if ((quote === `"` || quote === `'`) && value.endsWith(quote)) {
96
+ value = value.slice(1, -1);
97
+ } else {
98
+ value = value.replace(/\s+#.*$/, "").trim();
99
+ }
100
+
101
+ env[key] = value;
102
+ }
103
+
104
+ return env;
105
+ }
106
+
107
+ function loadLocalEnv(): Record<string, string> {
108
+ if (_cachedLocalEnv) return _cachedLocalEnv;
109
+
110
+ const repoRoot = join(import.meta.dir, "..");
111
+ const candidates = [
112
+ join(repoRoot, ".env"),
113
+ join(repoRoot, ".env.local"),
114
+ join(process.cwd(), ".env"),
115
+ join(process.cwd(), ".env.local"),
116
+ ];
117
+
118
+ const env: Record<string, string> = {};
119
+ for (const file of Array.from(new Set(candidates))) {
120
+ if (!existsSync(file)) continue;
121
+ try {
122
+ Object.assign(env, parseDotEnv(readFileSync(file, "utf-8")));
123
+ } catch {}
124
+ }
125
+
126
+ _cachedLocalEnv = env;
127
+ return env;
128
+ }
129
+
130
+ export function getInferenceEnv(name: string): string | undefined {
131
+ return process.env[name] || loadLocalEnv()[name];
132
+ }
133
+
134
+ function firstInferenceEnv(names: string[]): string | undefined {
135
+ for (const name of names) {
136
+ const value = getInferenceEnv(name);
137
+ if (value) return value;
138
+ }
139
+ }
140
+
141
+ function normalizeProvider(value: string | undefined): ProviderName | undefined {
142
+ const provider = value?.trim().toLowerCase();
143
+ return PROVIDER_NAMES.includes(provider as ProviderName) ? (provider as ProviderName) : undefined;
144
+ }
145
+
146
+ function assignGrokAlias(creds: CredentialStore) {
147
+ const key = getInferenceEnv("GROK_API_KEY");
148
+ if (!key) return;
149
+
150
+ // People often say/type "Grok" when they mean Groq. Use the key shape to
151
+ // route the alias without making xAI and Groq credentials interchangeable.
152
+ if (!creds.groq && key.startsWith("gsk_")) creds.groq = key;
153
+ if (!creds.xai && key.startsWith("xai-")) creds.xai = key;
154
+ }
155
+
156
+ function loadCredentials(): CredentialStore {
157
+ if (_cachedCreds) return _cachedCreds;
158
+
159
+ const creds: CredentialStore = {};
160
+
161
+ // Layer 1: env vars (highest priority)
162
+ const groqKey = getInferenceEnv("GROQ_API_KEY");
163
+ const openaiKey = getInferenceEnv("OPENAI_API_KEY");
164
+ const anthropicKey = getInferenceEnv("ANTHROPIC_API_KEY");
165
+ const googleKey = getInferenceEnv("GOOGLE_GENERATIVE_AI_API_KEY");
166
+ const xaiKey = getInferenceEnv("XAI_API_KEY");
167
+ const minimaxKey = getInferenceEnv("MINIMAX_API_KEY");
168
+ if (groqKey) creds.groq = groqKey;
169
+ if (openaiKey) creds.openai = openaiKey;
170
+ if (anthropicKey) creds.anthropic = anthropicKey;
171
+ if (googleKey) creds.google = googleKey;
172
+ if (xaiKey) creds.xai = xaiKey;
173
+ if (minimaxKey) creds.minimax = minimaxKey;
174
+ assignGrokAlias(creds);
175
+
176
+ // Layer 2: ~/.lattices/inference.json
177
+ const latticesConfig = join(homedir(), ".lattices", "inference.json");
178
+ if (existsSync(latticesConfig)) {
179
+ try {
180
+ const cfg = JSON.parse(readFileSync(latticesConfig, "utf-8"));
181
+ if (cfg.keys) {
182
+ if (!creds.groq && cfg.keys.groq) creds.groq = cfg.keys.groq;
183
+ if (!creds.openai && cfg.keys.openai) creds.openai = cfg.keys.openai;
184
+ if (!creds.anthropic && cfg.keys.anthropic) creds.anthropic = cfg.keys.anthropic;
185
+ if (!creds.google && cfg.keys.google) creds.google = cfg.keys.google;
186
+ if (!creds.xai && cfg.keys.xai) creds.xai = cfg.keys.xai;
187
+ if (!creds.minimax && cfg.keys.minimax) creds.minimax = cfg.keys.minimax;
188
+ }
189
+ } catch {}
190
+ }
191
+
192
+ // Layer 3: ~/.config/speakeasy/settings.json (fallback)
193
+ const speakeasyConfig = join(homedir(), ".config", "speakeasy", "settings.json");
194
+ if (existsSync(speakeasyConfig)) {
195
+ try {
196
+ const cfg = JSON.parse(readFileSync(speakeasyConfig, "utf-8"));
197
+ const p = cfg.providers || {};
198
+ if (!creds.groq && p.groq?.apiKey) creds.groq = p.groq.apiKey;
199
+ if (!creds.openai && p.openai?.apiKey) creds.openai = p.openai.apiKey;
200
+ if (!creds.anthropic && p.anthropic?.apiKey) creds.anthropic = p.anthropic.apiKey;
201
+ if (!creds.google && p.gemini?.apiKey) creds.google = p.gemini.apiKey;
202
+ if (!creds.xai && p.xai?.apiKey) creds.xai = p.xai.apiKey;
203
+ if (!creds.minimax && p.minimax?.apiKey) creds.minimax = p.minimax.apiKey;
204
+ } catch {}
205
+ }
206
+
207
+ _cachedCreds = creds;
208
+ return creds;
209
+ }
210
+
211
+ /** Clear cached credentials (call if config changes at runtime) */
212
+ export function clearCredentialCache() {
213
+ _cachedCreds = null;
214
+ _cachedLocalEnv = null;
215
+ }
216
+
217
+ /** List which providers have credentials available */
218
+ export function availableProviders(): ProviderName[] {
219
+ const creds = loadCredentials();
220
+ return (Object.keys(creds) as ProviderName[]).filter((k) => !!creds[k]);
221
+ }
222
+
223
+ /** Voice/hands-off defaults favor the lowest-latency configured provider. */
224
+ export function resolveVoiceInferenceOptions(): { provider: ProviderName; model: string } {
225
+ const configuredProvider = normalizeProvider(firstInferenceEnv([
226
+ "LATTICES_VOICE_PROVIDER",
227
+ "LATTICES_HANDSOFF_PROVIDER",
228
+ "LATTICES_INFER_PROVIDER",
229
+ ]));
230
+
231
+ const creds = loadCredentials();
232
+ const provider = configuredProvider
233
+ ?? VOICE_PROVIDER_PRIORITY.find((name) => !!creds[name])
234
+ ?? "groq";
235
+
236
+ const model = firstInferenceEnv([
237
+ "LATTICES_VOICE_MODEL",
238
+ "LATTICES_HANDSOFF_MODEL",
239
+ "LATTICES_INFER_MODEL",
240
+ ]) ?? VOICE_DEFAULT_MODELS[provider];
241
+
242
+ return { provider, model };
243
+ }
244
+
245
+ // ── Provider factory ───────────────────────────────────────────────
246
+
247
+ function getModel(provider: ProviderName, modelId: string) {
248
+ const creds = loadCredentials();
249
+
250
+ switch (provider) {
251
+ case "groq": {
252
+ const groq = createOpenAI({
253
+ baseURL: "https://api.groq.com/openai/v1",
254
+ apiKey: creds.groq,
255
+ });
256
+ return groq(modelId);
257
+ }
258
+ case "openai": {
259
+ const openai = createOpenAI({ apiKey: creds.openai });
260
+ return openai(modelId);
261
+ }
262
+ case "anthropic": {
263
+ const anthropic = createAnthropic({ apiKey: creds.anthropic });
264
+ return anthropic(modelId);
265
+ }
266
+ case "google": {
267
+ const google = createGoogleGenerativeAI({ apiKey: creds.google });
268
+ return google(modelId);
269
+ }
270
+ case "xai": {
271
+ const xai = createXai({ apiKey: creds.xai });
272
+ return xai(modelId);
273
+ }
274
+ case "minimax": {
275
+ // MiniMax uses OpenAI-compatible chat completions API
276
+ const minimax = createOpenAI({
277
+ baseURL: "https://api.minimax.io/v1",
278
+ apiKey: creds.minimax,
279
+ });
280
+ return minimax.chat(modelId);
281
+ }
282
+ }
283
+ }
284
+
285
+ // ── Logging ────────────────────────────────────────────────────────
286
+
287
+ function log(tag: string, msg: string) {
288
+ const ts = new Date().toISOString().slice(11, 23);
289
+ console.error(`[${ts}] infer${tag ? `/${tag}` : ""}: ${msg}`);
290
+ }
291
+
292
+ // ── Main inference function ────────────────────────────────────────
293
+
294
+ /**
295
+ * Run inference against any supported provider.
296
+ *
297
+ * @example
298
+ * // Simple
299
+ * const { text } = await infer("What windows do I have?", { provider: "groq" })
300
+ *
301
+ * // With system prompt and messages
302
+ * const { text } = await infer("tile chrome left", {
303
+ * provider: "groq",
304
+ * system: "You are a workspace assistant...",
305
+ * tag: "hands-off",
306
+ * })
307
+ *
308
+ * // With conversation history
309
+ * const { text } = await infer("now the other one right", {
310
+ * provider: "groq",
311
+ * messages: [
312
+ * { role: "user", content: "tile chrome left" },
313
+ * { role: "assistant", content: '{"actions":[...]}' },
314
+ * ],
315
+ * })
316
+ */
317
+ export async function infer(
318
+ prompt: string,
319
+ options: InferOptions = {}
320
+ ): Promise<InferResult> {
321
+ const provider = options.provider ?? "groq";
322
+ const modelId = options.model ?? DEFAULT_MODELS[provider];
323
+ const tag = options.tag ?? "";
324
+
325
+ // Check credentials
326
+ const creds = loadCredentials();
327
+ if (!creds[provider]) {
328
+ throw new Error(
329
+ `No API key for provider "${provider}". Set it in env, .env.local, ~/.lattices/inference.json, or ~/.config/speakeasy/settings.json`
330
+ );
331
+ }
332
+
333
+ const model = getModel(provider, modelId);
334
+
335
+ // Build messages
336
+ const messages: ModelMessage[] = [
337
+ ...(options.messages ?? []),
338
+ { role: "user", content: prompt },
339
+ ];
340
+
341
+ log(tag, `→ ${provider}/${modelId} (${prompt.length} chars)`);
342
+ const start = performance.now();
343
+
344
+ try {
345
+ const result = await generateText({
346
+ model,
347
+ system: options.system,
348
+ messages,
349
+ temperature: options.temperature ?? 0.3,
350
+ maxOutputTokens: options.maxTokens ?? 1024,
351
+ abortSignal: options.abortSignal,
352
+ });
353
+
354
+ const durationMs = Math.round(performance.now() - start);
355
+
356
+ const usage = result.usage
357
+ ? {
358
+ promptTokens: result.usage.inputTokens,
359
+ completionTokens: result.usage.outputTokens,
360
+ totalTokens: result.usage.totalTokens,
361
+ }
362
+ : undefined;
363
+
364
+ log(
365
+ tag,
366
+ `← ${durationMs}ms | ${usage?.totalTokens ?? "?"} tokens | ${result.text.length} chars`
367
+ );
368
+
369
+ return {
370
+ text: result.text,
371
+ provider,
372
+ model: modelId,
373
+ durationMs,
374
+ usage,
375
+ };
376
+ } catch (err: any) {
377
+ const durationMs = Math.round(performance.now() - start);
378
+ log(tag, `✗ ${durationMs}ms | ${err.message ?? err}`);
379
+ throw err;
380
+ }
381
+ }
382
+
383
+ // ── Convenience: infer with automatic JSON parsing ─────────────────
384
+
385
+ export async function inferJSON<T = any>(
386
+ prompt: string,
387
+ options: InferOptions = {}
388
+ ): Promise<{ data: T; raw: InferResult }> {
389
+ const result = await infer(prompt, options);
390
+
391
+ // Extract JSON from response (handle markdown fences)
392
+ let cleaned = result.text
393
+ .replace(/```json\s*/g, "")
394
+ .replace(/```\s*/g, "")
395
+ .trim();
396
+
397
+ const start = cleaned.indexOf("{");
398
+ const end = cleaned.lastIndexOf("}");
399
+ if (start === -1 || end === -1) {
400
+ throw new Error(`No JSON found in response: ${result.text.slice(0, 200)}`);
401
+ }
402
+ cleaned = cleaned.slice(start, end + 1);
403
+
404
+ const data = JSON.parse(cleaned) as T;
405
+ return { data, raw: result };
406
+ }
@@ -8,15 +8,15 @@ import { get } from "node:https";
8
8
  import type { IncomingMessage } from "node:http";
9
9
 
10
10
  const __dirname = import.meta.dir;
11
- const appDir = resolve(__dirname, "../app");
11
+ const appDir = resolve(__dirname, "../apps/mac");
12
12
  const cliRoot = resolve(__dirname, "..");
13
13
  const bundlePath = resolve(appDir, "Lattices.app");
14
14
  const binaryDir = resolve(bundlePath, "Contents/MacOS");
15
15
  const binaryPath = resolve(binaryDir, "Lattices");
16
- const entitlementsPath = resolve(__dirname, "../app/Lattices.entitlements");
16
+ const entitlementsPath = resolve(__dirname, "../apps/mac/Lattices.entitlements");
17
17
  const resourcesDir = resolve(bundlePath, "Contents/Resources");
18
18
  const iconPath = resolve(__dirname, "../assets/AppIcon.icns");
19
- const tapSoundPath = resolve(__dirname, "../app/Resources/tap.wav");
19
+ const tapSoundPath = resolve(__dirname, "../apps/mac/Resources/tap.wav");
20
20
 
21
21
  const REPO = "arach/lattices";
22
22
  const RELEASE_APP_ASSET_NAMES = ["Lattices.dmg"];
@@ -68,6 +68,27 @@ function packageVersion(): string {
68
68
  }
69
69
  }
70
70
 
71
+ function gitRevision(): string {
72
+ try {
73
+ return execSync("git rev-parse --short HEAD", {
74
+ cwd: cliRoot,
75
+ encoding: "utf8",
76
+ stdio: ["ignore", "pipe", "ignore"],
77
+ }).trim();
78
+ } catch {
79
+ return "unknown";
80
+ }
81
+ }
82
+
83
+ function xmlEscape(value: string): string {
84
+ return value
85
+ .replaceAll("&", "&amp;")
86
+ .replaceAll("<", "&lt;")
87
+ .replaceAll(">", "&gt;")
88
+ .replaceAll('"', "&quot;")
89
+ .replaceAll("'", "&apos;");
90
+ }
91
+
71
92
  function launch(extraArgs: string[] = []): void {
72
93
  if (isRunning()) {
73
94
  console.log("lattices app is already running.");
@@ -133,9 +154,35 @@ function signBundle(): void {
133
154
  } catch {}
134
155
  }
135
156
 
136
- function writeInfoPlist(): void {
157
+ type BundleBuildMetadata = {
158
+ channel?: "dev" | "release";
159
+ track?: string;
160
+ revision?: string;
161
+ timestamp?: string;
162
+ };
163
+
164
+ function buildMetadataPlist(metadata: BundleBuildMetadata): string {
165
+ if (metadata.channel !== "dev") return "";
166
+
167
+ const track = metadata.track ?? "latest";
168
+ const revision = metadata.revision ?? gitRevision();
169
+ const timestamp = metadata.timestamp ?? new Date().toISOString();
170
+
171
+ return ` <key>LatticesBuildChannel</key>
172
+ <string>dev</string>
173
+ <key>LatticesBuildTrack</key>
174
+ <string>${xmlEscape(track)}</string>
175
+ <key>LatticesBuildRevision</key>
176
+ <string>${xmlEscape(revision)}</string>
177
+ <key>LatticesBuildTimestamp</key>
178
+ <string>${xmlEscape(timestamp)}</string>
179
+ `;
180
+ }
181
+
182
+ function writeInfoPlist(metadata: BundleBuildMetadata = {}): void {
137
183
  mkdirSync(resolve(bundlePath, "Contents"), { recursive: true });
138
184
  const version = packageVersion();
185
+ const buildMetadata = buildMetadataPlist(metadata);
139
186
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
140
187
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
141
188
  <plist version="1.0">
@@ -167,7 +214,7 @@ function writeInfoPlist(): void {
167
214
  <string>${version}</string>
168
215
  <key>CFBundleShortVersionString</key>
169
216
  <string>${version}</string>
170
- <key>LSMinimumSystemVersion</key>
217
+ ${buildMetadata} <key>LSMinimumSystemVersion</key>
171
218
  <string>13.0</string>
172
219
  <key>LSUIElement</key>
173
220
  <true/>
@@ -209,7 +256,7 @@ function buildFromSource(): boolean {
209
256
 
210
257
  mkdirSync(binaryDir, { recursive: true });
211
258
  execSync(`cp '${builtPath}' '${binaryPath}'`);
212
- writeInfoPlist();
259
+ writeInfoPlist({ channel: "dev", track: "latest" });
213
260
  syncBundleResources();
214
261
 
215
262
  // Re-sign the bundle so macOS TCC recognizes a stable identity across rebuilds.
@@ -381,7 +428,10 @@ if (cmd === "build") {
381
428
  console.error("Swift is required. Install with: xcode-select --install");
382
429
  process.exit(1);
383
430
  }
384
- buildFromSource();
431
+ if (!buildFromSource()) {
432
+ console.error("Build failed.");
433
+ process.exit(1);
434
+ }
385
435
  } else if (cmd === "quit") {
386
436
  if (quit()) {
387
437
  console.log("lattices app stopped.");
package/bin/lattices-dev CHANGED
@@ -4,7 +4,7 @@
4
4
  set -euo pipefail
5
5
 
6
6
  SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0")"
7
- APP_DIR="$(cd "$(dirname "$SCRIPT_PATH")/../app" && pwd)"
7
+ APP_DIR="$(cd "$(dirname "$SCRIPT_PATH")/../apps/mac" && pwd)"
8
8
  ROOT="$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)"
9
9
  LOG_FILE="$HOME/.lattices/lattices.log"
10
10
  BINARY="$APP_DIR/.build/release/Lattices"
@@ -15,6 +15,8 @@ ENTITLEMENTS="$APP_DIR/Lattices.entitlements"
15
15
  ICON="$ROOT/assets/AppIcon.icns"
16
16
  TAP_SOUND="$APP_DIR/Resources/tap.wav"
17
17
  VERSION="$(node -p "require('$ROOT/package.json').version" 2>/dev/null || echo '0.1.0')"
18
+ GIT_REVISION="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
19
+ BUILD_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
18
20
 
19
21
  red() { printf "\033[31m%s\033[0m\n" "$*"; }
20
22
  green() { printf "\033[32m%s\033[0m\n" "$*"; }
@@ -93,6 +95,14 @@ write_info_plist() {
93
95
  <string>$VERSION</string>
94
96
  <key>CFBundleShortVersionString</key>
95
97
  <string>$VERSION</string>
98
+ <key>LatticesBuildChannel</key>
99
+ <string>dev</string>
100
+ <key>LatticesBuildTrack</key>
101
+ <string>latest</string>
102
+ <key>LatticesBuildRevision</key>
103
+ <string>$GIT_REVISION</string>
104
+ <key>LatticesBuildTimestamp</key>
105
+ <string>$BUILD_TIMESTAMP</string>
96
106
  <key>LSMinimumSystemVersion</key>
97
107
  <string>13.0</string>
98
108
  <key>LSUIElement</key>
@@ -177,6 +187,31 @@ cmd_clear_logs() {
177
187
  fi
178
188
  }
179
189
 
190
+ cmd_link() {
191
+ if ! command -v bun >/dev/null 2>&1; then
192
+ red "bun not found. Install: curl -fsSL https://bun.sh/install | bash"
193
+ exit 1
194
+ fi
195
+ cd "$ROOT"
196
+ bun link
197
+ bun link @lattices/cli
198
+ if command -v lattices >/dev/null 2>&1; then
199
+ green "Linked. \`lattices\` -> $(command -v lattices)"
200
+ else
201
+ red "Linked, but \`lattices\` is not on PATH. Add bun's global bin to PATH:"
202
+ dim " export PATH=\"\$HOME/.bun/bin:\$PATH\""
203
+ fi
204
+ }
205
+
206
+ cmd_unlink() {
207
+ if ! command -v bun >/dev/null 2>&1; then
208
+ red "bun not found."
209
+ exit 1
210
+ fi
211
+ bun remove -g @lattices/cli >/dev/null 2>&1 || true
212
+ green "Unlinked."
213
+ }
214
+
180
215
  cmd_status() {
181
216
  if pgrep -x Lattices >/dev/null 2>&1; then
182
217
  local pid=$(pgrep -x Lattices)
@@ -198,6 +233,8 @@ cmd_status() {
198
233
  cmd_help() {
199
234
  echo "lattices-dev — Lattices development commands"
200
235
  echo ""
236
+ echo " link Symlink \`lattices\` to this checkout (bun link)"
237
+ echo " unlink Remove the bun link"
201
238
  echo " restart Quit + rebuild + relaunch"
202
239
  echo " build Build release binary"
203
240
  echo " quit Stop the running app"
@@ -210,6 +247,8 @@ cmd_help() {
210
247
  }
211
248
 
212
249
  case "${1:-help}" in
250
+ link) cmd_link ;;
251
+ unlink) cmd_unlink ;;
213
252
  restart) cmd_restart ;;
214
253
  build) cmd_build ;;
215
254
  quit|stop) cmd_quit ;;
package/bin/lattices.ts CHANGED
@@ -549,7 +549,7 @@ function resolvePanes(dir: string): PaneConfig[] {
549
549
 
550
550
  function detectProjectType(dir: string): string | null {
551
551
  // Check for lattices-style hybrid project (Swift app + Node CLI)
552
- if (existsSync(resolve(dir, "app/Package.swift")) && existsSync(resolve(dir, "bin/lattices-app.ts")))
552
+ if (existsSync(resolve(dir, "apps/mac/Package.swift")) && existsSync(resolve(dir, "bin/lattices-app.ts")))
553
553
  return "lattices-app";
554
554
  if (existsSync(resolve(dir, "Package.swift"))) return "swift";
555
555
  if (existsSync(resolve(dir, "Cargo.toml"))) return "rust";
@@ -443,10 +443,10 @@ Those existing RPC names can remain stable while their internals are replaced.
443
443
 
444
444
  Files likely involved:
445
445
 
446
- - `app/Sources/LatticesApi.swift`
447
- - new planner/executor files under `app/Sources/`
448
- - `app/Sources/WindowTiler.swift`
449
- - `app/Sources/WorkspaceManager.swift`
446
+ - `apps/mac/Sources/LatticesApi.swift`
447
+ - new planner/executor files under `apps/mac/Sources/`
448
+ - `apps/mac/Sources/WindowTiler.swift`
449
+ - `apps/mac/Sources/WorkspaceManager.swift`
450
450
 
451
451
  Deliverables:
452
452
 
@@ -474,9 +474,9 @@ Deliverables:
474
474
 
475
475
  Files likely involved:
476
476
 
477
- - `app/Sources/VoiceIntentResolver.swift`
478
- - `app/Sources/IntentEngine.swift`
479
- - `app/Sources/HandsOffSession.swift`
477
+ - `apps/mac/Sources/VoiceIntentResolver.swift`
478
+ - `apps/mac/Sources/IntentEngine.swift`
479
+ - `apps/mac/Sources/HandsOffSession.swift`
480
480
 
481
481
  Deliverables:
482
482
 
@@ -488,8 +488,8 @@ Deliverables:
488
488
 
489
489
  Files likely involved:
490
490
 
491
- - `app/Sources/HUDController.swift`
492
- - `app/Sources/PaletteCommand.swift`
491
+ - `apps/mac/Sources/HUDController.swift`
492
+ - `apps/mac/Sources/PaletteCommand.swift`
493
493
 
494
494
  Deliverables:
495
495