@arach/lattices 0.2.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -86
- package/apps/mac/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
- package/apps/mac/Lattices.entitlements +21 -0
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Resources/tap.wav +0 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/assistant-intelligence.ts +912 -0
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +17 -0
- package/bin/cua.ts +26 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +96 -0
- package/bin/handsoff-worker.ts +531 -0
- package/bin/infer.ts +424 -0
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +655 -0
- package/bin/lattices-build +125 -0
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +362 -0
- package/bin/lattices.ts +3260 -0
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +233 -0
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +1041 -47
- package/docs/app.md +96 -13
- package/docs/assistant-knowledge.md +130 -0
- package/docs/companion-deck.md +209 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/concepts.md +13 -12
- package/docs/config.md +83 -10
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +176 -28
- package/docs/mouse-gestures.md +244 -0
- package/docs/ocr.md +21 -9
- package/docs/overview.md +42 -23
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +382 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +8 -12
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/release.md +172 -0
- package/docs/repo-structure.md +100 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +224 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice-error-model.md +73 -0
- package/docs/voice.md +221 -0
- package/package.json +69 -16
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
- package/app/Lattices.app/Contents/Info.plist +0 -24
- package/app/Package.swift +0 -13
- package/app/Sources/ActionRow.swift +0 -61
- package/app/Sources/App.swift +0 -10
- package/app/Sources/AppDelegate.swift +0 -234
- package/app/Sources/AppShellView.swift +0 -62
- package/app/Sources/AppTypeClassifier.swift +0 -70
- package/app/Sources/AppWindowShell.swift +0 -63
- package/app/Sources/CheatSheetHUD.swift +0 -332
- package/app/Sources/CommandModeState.swift +0 -1362
- package/app/Sources/CommandModeView.swift +0 -1405
- package/app/Sources/CommandModeWindow.swift +0 -192
- package/app/Sources/CommandPaletteView.swift +0 -307
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/DaemonProtocol.swift +0 -101
- package/app/Sources/DaemonServer.swift +0 -414
- package/app/Sources/DesktopModel.swift +0 -121
- package/app/Sources/DesktopModelTypes.swift +0 -71
- package/app/Sources/DiagnosticLog.swift +0 -271
- package/app/Sources/EventBus.swift +0 -30
- package/app/Sources/HotkeyManager.swift +0 -250
- package/app/Sources/HotkeyStore.swift +0 -338
- package/app/Sources/InventoryManager.swift +0 -35
- package/app/Sources/InventoryPath.swift +0 -43
- package/app/Sources/KeyRecorderView.swift +0 -210
- package/app/Sources/LatticesApi.swift +0 -1125
- package/app/Sources/MainView.swift +0 -467
- package/app/Sources/MainWindow.swift +0 -83
- package/app/Sources/OcrModel.swift +0 -309
- package/app/Sources/OcrStore.swift +0 -295
- package/app/Sources/OmniSearchState.swift +0 -283
- package/app/Sources/OmniSearchView.swift +0 -288
- package/app/Sources/OmniSearchWindow.swift +0 -105
- package/app/Sources/OrphanRow.swift +0 -129
- package/app/Sources/PaletteCommand.swift +0 -419
- package/app/Sources/PermissionChecker.swift +0 -125
- package/app/Sources/Preferences.swift +0 -92
- package/app/Sources/ProcessModel.swift +0 -199
- package/app/Sources/ProcessQuery.swift +0 -151
- package/app/Sources/Project.swift +0 -28
- package/app/Sources/ProjectRow.swift +0 -368
- package/app/Sources/ProjectScanner.swift +0 -121
- package/app/Sources/ScreenMapState.swift +0 -2387
- package/app/Sources/ScreenMapView.swift +0 -2820
- package/app/Sources/ScreenMapWindowController.swift +0 -89
- package/app/Sources/SessionManager.swift +0 -72
- package/app/Sources/SettingsView.swift +0 -1053
- package/app/Sources/SettingsWindow.swift +0 -20
- package/app/Sources/TabGroupRow.swift +0 -178
- package/app/Sources/Terminal.swift +0 -259
- package/app/Sources/TerminalQuery.swift +0 -156
- package/app/Sources/TerminalSynthesizer.swift +0 -200
- package/app/Sources/Theme.swift +0 -163
- package/app/Sources/TilePickerView.swift +0 -209
- package/app/Sources/TmuxModel.swift +0 -53
- package/app/Sources/TmuxQuery.swift +0 -81
- package/app/Sources/WindowTiler.swift +0 -1755
- package/app/Sources/WorkspaceManager.swift +0 -434
- package/bin/lattices-app.js +0 -221
- package/bin/lattices.js +0 -1418
package/bin/infer.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
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
|
+
import { getKeychainSecret } from "./keychain";
|
|
20
|
+
|
|
21
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export type ProviderName = "groq" | "openai" | "anthropic" | "google" | "xai" | "minimax";
|
|
24
|
+
|
|
25
|
+
export interface InferOptions {
|
|
26
|
+
provider?: ProviderName;
|
|
27
|
+
model?: string;
|
|
28
|
+
system?: string;
|
|
29
|
+
messages?: ModelMessage[];
|
|
30
|
+
temperature?: number;
|
|
31
|
+
maxTokens?: number;
|
|
32
|
+
/** Tag for logging — e.g. "hands-off", "voice-fallback" */
|
|
33
|
+
tag?: string;
|
|
34
|
+
/** Abort signal for cancellation/timeout */
|
|
35
|
+
abortSignal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface InferResult {
|
|
39
|
+
text: string;
|
|
40
|
+
provider: ProviderName;
|
|
41
|
+
model: string;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
usage?: {
|
|
44
|
+
promptTokens?: number;
|
|
45
|
+
completionTokens?: number;
|
|
46
|
+
totalTokens?: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Default models per provider ────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const PROVIDER_NAMES: ProviderName[] = ["groq", "openai", "anthropic", "google", "xai", "minimax"];
|
|
53
|
+
const VOICE_PROVIDER_PRIORITY: ProviderName[] = ["xai", "groq", "openai", "google", "anthropic", "minimax"];
|
|
54
|
+
|
|
55
|
+
const DEFAULT_MODELS: Record<ProviderName, string> = {
|
|
56
|
+
groq: "llama-3.3-70b-versatile",
|
|
57
|
+
openai: "gpt-4o-mini",
|
|
58
|
+
anthropic: "claude-sonnet-4-6",
|
|
59
|
+
google: "gemini-2.0-flash",
|
|
60
|
+
xai: "grok-4.20-reasoning",
|
|
61
|
+
minimax: "MiniMax-M2.5-highspeed",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Voice paths use the same models as default — earlier we forced groq to
|
|
65
|
+
// llama-3.1-8b-instant for latency, but its 6k TPM cap couldn't fit a real
|
|
66
|
+
// desktop snapshot (saw 7174-token requests rejected). 70B versatile fits
|
|
67
|
+
// 128k context and Groq still serves it fast.
|
|
68
|
+
const VOICE_DEFAULT_MODELS: Record<ProviderName, string> = {
|
|
69
|
+
...DEFAULT_MODELS,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ── Credential loading ─────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
interface CredentialStore {
|
|
75
|
+
groq?: string;
|
|
76
|
+
openai?: string;
|
|
77
|
+
anthropic?: string;
|
|
78
|
+
google?: string;
|
|
79
|
+
xai?: string;
|
|
80
|
+
minimax?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let _cachedCreds: CredentialStore | null = null;
|
|
84
|
+
let _cachedLocalEnv: Record<string, string> | null = null;
|
|
85
|
+
|
|
86
|
+
function parseDotEnv(content: string): Record<string, string> {
|
|
87
|
+
const env: Record<string, string> = {};
|
|
88
|
+
|
|
89
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
90
|
+
const line = rawLine.trim();
|
|
91
|
+
if (!line || line.startsWith("#")) continue;
|
|
92
|
+
|
|
93
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
94
|
+
if (!match) continue;
|
|
95
|
+
|
|
96
|
+
const [, key, rawValue] = match;
|
|
97
|
+
let value = rawValue.trim();
|
|
98
|
+
const quote = value[0];
|
|
99
|
+
if ((quote === `"` || quote === `'`) && value.endsWith(quote)) {
|
|
100
|
+
value = value.slice(1, -1);
|
|
101
|
+
} else {
|
|
102
|
+
value = value.replace(/\s+#.*$/, "").trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
env[key] = value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return env;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadLocalEnv(): Record<string, string> {
|
|
112
|
+
if (_cachedLocalEnv) return _cachedLocalEnv;
|
|
113
|
+
|
|
114
|
+
const repoRoot = join(import.meta.dir, "..");
|
|
115
|
+
const candidates = [
|
|
116
|
+
join(repoRoot, ".env"),
|
|
117
|
+
join(repoRoot, ".env.local"),
|
|
118
|
+
join(process.cwd(), ".env"),
|
|
119
|
+
join(process.cwd(), ".env.local"),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const env: Record<string, string> = {};
|
|
123
|
+
for (const file of Array.from(new Set(candidates))) {
|
|
124
|
+
if (!existsSync(file)) continue;
|
|
125
|
+
try {
|
|
126
|
+
Object.assign(env, parseDotEnv(readFileSync(file, "utf-8")));
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_cachedLocalEnv = env;
|
|
131
|
+
return env;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getInferenceEnv(name: string): string | undefined {
|
|
135
|
+
return process.env[name] || loadLocalEnv()[name];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function firstInferenceEnv(names: string[]): string | undefined {
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
const value = getInferenceEnv(name);
|
|
141
|
+
if (value) return value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeProvider(value: string | undefined): ProviderName | undefined {
|
|
146
|
+
const provider = value?.trim().toLowerCase();
|
|
147
|
+
return PROVIDER_NAMES.includes(provider as ProviderName) ? (provider as ProviderName) : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assignGrokAlias(creds: CredentialStore) {
|
|
151
|
+
const key = getInferenceEnv("GROK_API_KEY");
|
|
152
|
+
if (!key) return;
|
|
153
|
+
|
|
154
|
+
// People often say/type "Grok" when they mean Groq. Use the key shape to
|
|
155
|
+
// route the alias without making xAI and Groq credentials interchangeable.
|
|
156
|
+
if (!creds.groq && key.startsWith("gsk_")) creds.groq = key;
|
|
157
|
+
if (!creds.xai && key.startsWith("xai-")) creds.xai = key;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function loadCredentials(): CredentialStore {
|
|
161
|
+
if (_cachedCreds) return _cachedCreds;
|
|
162
|
+
|
|
163
|
+
const creds: CredentialStore = {};
|
|
164
|
+
|
|
165
|
+
// Layer 1: env vars (highest priority)
|
|
166
|
+
const groqKey = getInferenceEnv("GROQ_API_KEY");
|
|
167
|
+
const openaiKey = getInferenceEnv("OPENAI_API_KEY");
|
|
168
|
+
const anthropicKey = getInferenceEnv("ANTHROPIC_API_KEY");
|
|
169
|
+
const googleKey = getInferenceEnv("GOOGLE_GENERATIVE_AI_API_KEY");
|
|
170
|
+
// SUPERGROK_API_KEY (SuperGrok Heavy tier) takes precedence over the
|
|
171
|
+
// standard XAI_API_KEY when both are present.
|
|
172
|
+
const xaiKey =
|
|
173
|
+
getInferenceEnv("SUPERGROK_API_KEY") || getInferenceEnv("XAI_API_KEY");
|
|
174
|
+
const minimaxKey = getInferenceEnv("MINIMAX_API_KEY");
|
|
175
|
+
if (groqKey) creds.groq = groqKey;
|
|
176
|
+
if (openaiKey) creds.openai = openaiKey;
|
|
177
|
+
if (anthropicKey) creds.anthropic = anthropicKey;
|
|
178
|
+
if (googleKey) creds.google = googleKey;
|
|
179
|
+
if (xaiKey) creds.xai = xaiKey;
|
|
180
|
+
if (minimaxKey) creds.minimax = minimaxKey;
|
|
181
|
+
assignGrokAlias(creds);
|
|
182
|
+
|
|
183
|
+
// Layer 2: ~/.lattices/inference.json
|
|
184
|
+
const latticesConfig = join(homedir(), ".lattices", "inference.json");
|
|
185
|
+
if (existsSync(latticesConfig)) {
|
|
186
|
+
try {
|
|
187
|
+
const cfg = JSON.parse(readFileSync(latticesConfig, "utf-8"));
|
|
188
|
+
if (cfg.keys) {
|
|
189
|
+
if (!creds.groq && cfg.keys.groq) creds.groq = cfg.keys.groq;
|
|
190
|
+
if (!creds.openai && cfg.keys.openai) creds.openai = cfg.keys.openai;
|
|
191
|
+
if (!creds.anthropic && cfg.keys.anthropic) creds.anthropic = cfg.keys.anthropic;
|
|
192
|
+
if (!creds.google && cfg.keys.google) creds.google = cfg.keys.google;
|
|
193
|
+
if (!creds.xai && cfg.keys.xai) creds.xai = cfg.keys.xai;
|
|
194
|
+
if (!creds.minimax && cfg.keys.minimax) creds.minimax = cfg.keys.minimax;
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Layer 3: ~/.config/speakeasy/settings.json (fallback)
|
|
200
|
+
const speakeasyConfig = join(homedir(), ".config", "speakeasy", "settings.json");
|
|
201
|
+
if (existsSync(speakeasyConfig)) {
|
|
202
|
+
try {
|
|
203
|
+
const cfg = JSON.parse(readFileSync(speakeasyConfig, "utf-8"));
|
|
204
|
+
const p = cfg.providers || {};
|
|
205
|
+
if (!creds.groq && p.groq?.apiKey) creds.groq = p.groq.apiKey;
|
|
206
|
+
if (!creds.openai && p.openai?.apiKey) creds.openai = p.openai.apiKey;
|
|
207
|
+
if (!creds.anthropic && p.anthropic?.apiKey) creds.anthropic = p.anthropic.apiKey;
|
|
208
|
+
if (!creds.google && p.gemini?.apiKey) creds.google = p.gemini.apiKey;
|
|
209
|
+
if (!creds.xai && p.xai?.apiKey) creds.xai = p.xai.apiKey;
|
|
210
|
+
if (!creds.minimax && p.minimax?.apiKey) creds.minimax = p.minimax.apiKey;
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Layer 4 — macOS keychain via built-in `/usr/bin/security` under the
|
|
215
|
+
// `lattices.inference` service. One read per missing provider, cached
|
|
216
|
+
// in `_cachedCreds` for the process lifetime. Keys never touch disk.
|
|
217
|
+
// Portable across machines (no external CLI dep).
|
|
218
|
+
if (!creds.xai) creds.xai = getKeychainSecret("xai");
|
|
219
|
+
if (!creds.groq) creds.groq = getKeychainSecret("groq");
|
|
220
|
+
if (!creds.openai) creds.openai = getKeychainSecret("openai");
|
|
221
|
+
if (!creds.anthropic) creds.anthropic = getKeychainSecret("anthropic");
|
|
222
|
+
if (!creds.google) creds.google = getKeychainSecret("google");
|
|
223
|
+
if (!creds.minimax) creds.minimax = getKeychainSecret("minimax");
|
|
224
|
+
|
|
225
|
+
_cachedCreds = creds;
|
|
226
|
+
return creds;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Clear cached credentials (call if config changes at runtime) */
|
|
230
|
+
export function clearCredentialCache() {
|
|
231
|
+
_cachedCreds = null;
|
|
232
|
+
_cachedLocalEnv = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** List which providers have credentials available */
|
|
236
|
+
export function availableProviders(): ProviderName[] {
|
|
237
|
+
const creds = loadCredentials();
|
|
238
|
+
return (Object.keys(creds) as ProviderName[]).filter((k) => !!creds[k]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Voice/hands-off defaults favor the lowest-latency configured provider. */
|
|
242
|
+
export function resolveVoiceInferenceOptions(): { provider: ProviderName; model: string } {
|
|
243
|
+
const configuredProvider = normalizeProvider(firstInferenceEnv([
|
|
244
|
+
"LATTICES_VOICE_PROVIDER",
|
|
245
|
+
"LATTICES_HANDSOFF_PROVIDER",
|
|
246
|
+
"LATTICES_INFER_PROVIDER",
|
|
247
|
+
]));
|
|
248
|
+
|
|
249
|
+
const creds = loadCredentials();
|
|
250
|
+
const provider = configuredProvider
|
|
251
|
+
?? VOICE_PROVIDER_PRIORITY.find((name) => !!creds[name])
|
|
252
|
+
?? "groq";
|
|
253
|
+
|
|
254
|
+
const model = firstInferenceEnv([
|
|
255
|
+
"LATTICES_VOICE_MODEL",
|
|
256
|
+
"LATTICES_HANDSOFF_MODEL",
|
|
257
|
+
"LATTICES_INFER_MODEL",
|
|
258
|
+
]) ?? VOICE_DEFAULT_MODELS[provider];
|
|
259
|
+
|
|
260
|
+
return { provider, model };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Provider factory ───────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
function getModel(provider: ProviderName, modelId: string) {
|
|
266
|
+
const creds = loadCredentials();
|
|
267
|
+
|
|
268
|
+
switch (provider) {
|
|
269
|
+
case "groq": {
|
|
270
|
+
const groq = createOpenAI({
|
|
271
|
+
baseURL: "https://api.groq.com/openai/v1",
|
|
272
|
+
apiKey: creds.groq,
|
|
273
|
+
});
|
|
274
|
+
return groq(modelId);
|
|
275
|
+
}
|
|
276
|
+
case "openai": {
|
|
277
|
+
const openai = createOpenAI({ apiKey: creds.openai });
|
|
278
|
+
return openai(modelId);
|
|
279
|
+
}
|
|
280
|
+
case "anthropic": {
|
|
281
|
+
const anthropic = createAnthropic({ apiKey: creds.anthropic });
|
|
282
|
+
return anthropic(modelId);
|
|
283
|
+
}
|
|
284
|
+
case "google": {
|
|
285
|
+
const google = createGoogleGenerativeAI({ apiKey: creds.google });
|
|
286
|
+
return google(modelId);
|
|
287
|
+
}
|
|
288
|
+
case "xai": {
|
|
289
|
+
const xai = createXai({ apiKey: creds.xai });
|
|
290
|
+
return xai(modelId);
|
|
291
|
+
}
|
|
292
|
+
case "minimax": {
|
|
293
|
+
// MiniMax uses OpenAI-compatible chat completions API
|
|
294
|
+
const minimax = createOpenAI({
|
|
295
|
+
baseURL: "https://api.minimax.io/v1",
|
|
296
|
+
apiKey: creds.minimax,
|
|
297
|
+
});
|
|
298
|
+
return minimax.chat(modelId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Logging ────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
function log(tag: string, msg: string) {
|
|
306
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
307
|
+
console.error(`[${ts}] infer${tag ? `/${tag}` : ""}: ${msg}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Main inference function ────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Run inference against any supported provider.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* // Simple
|
|
317
|
+
* const { text } = await infer("What windows do I have?", { provider: "groq" })
|
|
318
|
+
*
|
|
319
|
+
* // With system prompt and messages
|
|
320
|
+
* const { text } = await infer("tile chrome left", {
|
|
321
|
+
* provider: "groq",
|
|
322
|
+
* system: "You are a workspace assistant...",
|
|
323
|
+
* tag: "hands-off",
|
|
324
|
+
* })
|
|
325
|
+
*
|
|
326
|
+
* // With conversation history
|
|
327
|
+
* const { text } = await infer("now the other one right", {
|
|
328
|
+
* provider: "groq",
|
|
329
|
+
* messages: [
|
|
330
|
+
* { role: "user", content: "tile chrome left" },
|
|
331
|
+
* { role: "assistant", content: '{"actions":[...]}' },
|
|
332
|
+
* ],
|
|
333
|
+
* })
|
|
334
|
+
*/
|
|
335
|
+
export async function infer(
|
|
336
|
+
prompt: string,
|
|
337
|
+
options: InferOptions = {}
|
|
338
|
+
): Promise<InferResult> {
|
|
339
|
+
const provider = options.provider ?? "groq";
|
|
340
|
+
const modelId = options.model ?? DEFAULT_MODELS[provider];
|
|
341
|
+
const tag = options.tag ?? "";
|
|
342
|
+
|
|
343
|
+
// Check credentials
|
|
344
|
+
const creds = loadCredentials();
|
|
345
|
+
if (!creds[provider]) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`No API key for provider "${provider}". Set it in env, .env.local, ~/.lattices/inference.json, or ~/.config/speakeasy/settings.json`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const model = getModel(provider, modelId);
|
|
352
|
+
|
|
353
|
+
// Build messages
|
|
354
|
+
const messages: ModelMessage[] = [
|
|
355
|
+
...(options.messages ?? []),
|
|
356
|
+
{ role: "user", content: prompt },
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
log(tag, `→ ${provider}/${modelId} (${prompt.length} chars)`);
|
|
360
|
+
const start = performance.now();
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const result = await generateText({
|
|
364
|
+
model,
|
|
365
|
+
system: options.system,
|
|
366
|
+
messages,
|
|
367
|
+
temperature: options.temperature ?? 0.3,
|
|
368
|
+
maxOutputTokens: options.maxTokens ?? 1024,
|
|
369
|
+
abortSignal: options.abortSignal,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const durationMs = Math.round(performance.now() - start);
|
|
373
|
+
|
|
374
|
+
const usage = result.usage
|
|
375
|
+
? {
|
|
376
|
+
promptTokens: result.usage.inputTokens,
|
|
377
|
+
completionTokens: result.usage.outputTokens,
|
|
378
|
+
totalTokens: result.usage.totalTokens,
|
|
379
|
+
}
|
|
380
|
+
: undefined;
|
|
381
|
+
|
|
382
|
+
log(
|
|
383
|
+
tag,
|
|
384
|
+
`← ${durationMs}ms | ${usage?.totalTokens ?? "?"} tokens | ${result.text.length} chars`
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
text: result.text,
|
|
389
|
+
provider,
|
|
390
|
+
model: modelId,
|
|
391
|
+
durationMs,
|
|
392
|
+
usage,
|
|
393
|
+
};
|
|
394
|
+
} catch (err: any) {
|
|
395
|
+
const durationMs = Math.round(performance.now() - start);
|
|
396
|
+
log(tag, `✗ ${durationMs}ms | ${err.message ?? err}`);
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Convenience: infer with automatic JSON parsing ─────────────────
|
|
402
|
+
|
|
403
|
+
export async function inferJSON<T = any>(
|
|
404
|
+
prompt: string,
|
|
405
|
+
options: InferOptions = {}
|
|
406
|
+
): Promise<{ data: T; raw: InferResult }> {
|
|
407
|
+
const result = await infer(prompt, options);
|
|
408
|
+
|
|
409
|
+
// Extract JSON from response (handle markdown fences)
|
|
410
|
+
let cleaned = result.text
|
|
411
|
+
.replace(/```json\s*/g, "")
|
|
412
|
+
.replace(/```\s*/g, "")
|
|
413
|
+
.trim();
|
|
414
|
+
|
|
415
|
+
const start = cleaned.indexOf("{");
|
|
416
|
+
const end = cleaned.lastIndexOf("}");
|
|
417
|
+
if (start === -1 || end === -1) {
|
|
418
|
+
throw new Error(`No JSON found in response: ${result.text.slice(0, 200)}`);
|
|
419
|
+
}
|
|
420
|
+
cleaned = cleaned.slice(start, end + 1);
|
|
421
|
+
|
|
422
|
+
const data = JSON.parse(cleaned) as T;
|
|
423
|
+
return { data, raw: result };
|
|
424
|
+
}
|
package/bin/keychain.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny Lattices keychain helper.
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes generic passwords under the `lattices.inference` service
|
|
5
|
+
* via the built-in macOS `/usr/bin/security` CLI. No external dependencies,
|
|
6
|
+
* universally available on macOS (so portable across user machines without
|
|
7
|
+
* the user installing anything personal).
|
|
8
|
+
*
|
|
9
|
+
* Items are stored as a single keychain entry per provider — account = provider
|
|
10
|
+
* name (xai, groq, openai, anthropic, google, minimax). The macOS keychain
|
|
11
|
+
* does the encrypt-at-rest + ACL work; this file is only a thin shell-out.
|
|
12
|
+
*
|
|
13
|
+
* Usage in code:
|
|
14
|
+
* const key = getKeychainSecret("xai");
|
|
15
|
+
* setKeychainSecret("xai", "xai-foo...");
|
|
16
|
+
* deleteKeychainSecret("xai");
|
|
17
|
+
*
|
|
18
|
+
* Usage from a terminal (no Lattices wrapper needed — pure macOS):
|
|
19
|
+
* security add-generic-password -s lattices.inference -a xai -w <key> -U
|
|
20
|
+
* security find-generic-password -s lattices.inference -a xai -w
|
|
21
|
+
* security delete-generic-password -s lattices.inference -a xai
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from "child_process";
|
|
25
|
+
|
|
26
|
+
export const KEYCHAIN_SERVICE = "lattices.inference";
|
|
27
|
+
const SECURITY_BIN = "/usr/bin/security";
|
|
28
|
+
const TIMEOUT_MS = 1500;
|
|
29
|
+
|
|
30
|
+
export function getKeychainSecret(account: string): string | undefined {
|
|
31
|
+
try {
|
|
32
|
+
const value = execFileSync(
|
|
33
|
+
SECURITY_BIN,
|
|
34
|
+
["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"],
|
|
35
|
+
{ encoding: "utf-8", timeout: TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"] },
|
|
36
|
+
).trim();
|
|
37
|
+
return value || undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setKeychainSecret(account: string, value: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
// -U updates if the item already exists; otherwise adds. The value is
|
|
46
|
+
// passed via env to keep it out of `ps`/argv.
|
|
47
|
+
execFileSync(
|
|
48
|
+
SECURITY_BIN,
|
|
49
|
+
["add-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w", value, "-U"],
|
|
50
|
+
{ timeout: TIMEOUT_MS, stdio: ["ignore", "ignore", "ignore"] },
|
|
51
|
+
);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function deleteKeychainSecret(account: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
execFileSync(
|
|
61
|
+
SECURITY_BIN,
|
|
62
|
+
["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account],
|
|
63
|
+
{ timeout: TIMEOUT_MS, stdio: ["ignore", "ignore", "ignore"] },
|
|
64
|
+
);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listKeychainAccounts(): string[] {
|
|
72
|
+
// `security dump-keychain` is heavy; instead probe each known account.
|
|
73
|
+
// Callers pass the candidate list explicitly to keep this stateless.
|
|
74
|
+
return [];
|
|
75
|
+
}
|