@lattices/cli 0.6.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.
Files changed (44) hide show
  1. package/README.md +13 -4
  2. package/apps/mac/Info.plist +4 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -2
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  6. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +11 -0
  7. package/apps/mac/Lattices.entitlements +6 -0
  8. package/bin/assistant-intelligence.ts +41 -3
  9. package/bin/cli/capture.ts +252 -0
  10. package/bin/cli/daemon.ts +22 -0
  11. package/bin/cli/helpers.ts +105 -0
  12. package/bin/cli/layer.ts +178 -0
  13. package/bin/cli/runs.ts +43 -0
  14. package/bin/cli/search.ts +141 -0
  15. package/bin/cli/session.ts +32 -0
  16. package/bin/client.ts +2 -1
  17. package/bin/cua.ts +26 -0
  18. package/bin/infer.ts +22 -4
  19. package/bin/keychain.ts +75 -0
  20. package/bin/lattices-app.ts +111 -12
  21. package/bin/lattices-build-env.ts +77 -0
  22. package/bin/lattices-dev +29 -2
  23. package/bin/lattices.ts +729 -769
  24. package/docs/api.md +496 -3
  25. package/docs/app.md +5 -4
  26. package/docs/assistant-knowledge.md +130 -0
  27. package/docs/config.md +5 -0
  28. package/docs/hyperspace-grid-snappiness.md +210 -0
  29. package/docs/layers.md +53 -0
  30. package/docs/mouse-gestures.md +40 -3
  31. package/docs/ocr.md +3 -0
  32. package/docs/prompts/hands-off-system.md +9 -1
  33. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  34. package/docs/proposals/{LAT-006-mira-in-lattices.md → LAT-006-runs-and-capture-in-lattices.md} +83 -70
  35. package/docs/quickstart.md +3 -1
  36. package/docs/reference/dewey.config.ts +1 -1
  37. package/docs/release.md +4 -3
  38. package/docs/terminal-kit.md +87 -0
  39. package/docs/tiling-reference.md +5 -3
  40. package/docs/voice.md +3 -3
  41. package/package.json +27 -5
  42. package/packages/npm/sdk/cua.d.mts +1 -0
  43. package/packages/npm/sdk/cua.d.ts +188 -0
  44. package/packages/npm/sdk/cua.mjs +376 -0
@@ -0,0 +1,105 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export interface ExecOpts {
4
+ encoding?: string;
5
+ stdio?: string | string[];
6
+ cwd?: string;
7
+ [key: string]: any;
8
+ }
9
+
10
+ export function run(cmd: string, opts: ExecOpts = {}): string {
11
+ return execSync(cmd, { encoding: "utf8", ...opts } as any).trim();
12
+ }
13
+
14
+ export function runQuiet(cmd: string): string | null {
15
+ try {
16
+ return run(cmd, { stdio: "pipe" });
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function parseFlagValue(args: string[], name: string): string | undefined {
23
+ const prefix = `--${name}=`;
24
+ const exact = `--${name}`;
25
+ for (let i = 0; i < args.length; i++) {
26
+ if (args[i].startsWith(prefix)) return args[i].slice(prefix.length);
27
+ if (args[i] === exact) return args[i + 1];
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ export function parseOptionalNumber(args: string[], ...names: string[]): number | undefined {
33
+ for (const name of names) {
34
+ const raw = parseFlagValue(args, name);
35
+ if (raw === undefined || raw === "") continue;
36
+ const value = Number(raw);
37
+ if (Number.isFinite(value)) return value;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ export function hasFlag(args: string[], name: string): boolean {
43
+ return args.includes(`--${name}`);
44
+ }
45
+
46
+ export function nonFlagArgs(args: string[]): string[] {
47
+ const valueFlags = new Set([
48
+ "id", "state", "ttl", "ttlMs", "x", "y", "gap", "placement", "style", "name", "scale",
49
+ "hud-url", "hudUrl", "hud-html", "hudHTML", "hudHtml", "hud-title", "hudTitle",
50
+ "hud-width", "hudWidth", "hud-height", "hudHeight", "width", "height",
51
+ "manifest", "root", "max-depth", "maxDepth", "read-access", "readAccess",
52
+ "pause", "limit", "session", "app", "name", "bundle-id", "bundleId", "bundleIdentifier",
53
+ "path", "app-path", "appPath", "title", "filename", "run-id", "runId",
54
+ "text", "tty", "wid", "treatment", "mode", "phase", "transport", "capture",
55
+ "appearance", "cursor-style", "cursorStyle", "shape", "marker-shape", "markerShape",
56
+ "cursor-shape", "cursorShape", "angle-deg", "angleDeg", "rotation-deg", "rotationDeg",
57
+ "rotation", "angle", "color", "duration-ms", "durationMs",
58
+ "type-interval-ms", "typeIntervalMs", "typing-interval-ms", "typingIntervalMs", "label",
59
+ "caption", "treatment-label", "treatmentLabel", "variant",
60
+ "caption-title", "captionTitle", "caption-body", "captionBody",
61
+ "caption-detail", "captionDetail", "caption-tags", "captionTags",
62
+ "caption-mode", "captionMode", "caption-eyebrow", "captionEyebrow",
63
+ "caption-lead-ms", "captionLeadMs", "caption-sound", "captionSound",
64
+ "caption-placement", "captionPlacement", "caption-margin", "captionMargin",
65
+ "caption-x", "captionX", "caption-y", "captionY",
66
+ "caption-x-ratio", "captionXRatio", "caption-y-ratio", "captionYRatio",
67
+ "caption-left-ratio", "captionLeftRatio", "caption-top-ratio", "captionTopRatio",
68
+ "sound", "sfx",
69
+ "trail", "effect", "path-style", "pathStyle", "motion", "easing", "velocity",
70
+ "trajectory", "curve", "arc", "glow", "bloom", "idle", "settle", "presence",
71
+ "edge", "edge-effect", "edgeEffect", "arrival",
72
+ "fps", "w", "h", "stop-file", "stopFile", "finished-file", "finishedFile",
73
+ "timeout-ms", "timeoutMs", "duration",
74
+ "x", "y", "x-ratio", "xRatio", "y-ratio", "yRatio",
75
+ "relative-x", "relativeX", "relative-y", "relativeY",
76
+ "window-x", "windowX", "window-y", "windowY", "button",
77
+ ]);
78
+ const out: string[] = [];
79
+ for (let i = 0; i < args.length; i++) {
80
+ const arg = args[i];
81
+ if (!arg.startsWith("--")) {
82
+ out.push(arg);
83
+ continue;
84
+ }
85
+ const flagName = arg.slice(2);
86
+ if (!arg.includes("=") && valueFlags.has(flagName)) i++;
87
+ }
88
+ return out;
89
+ }
90
+
91
+ export function relativeTime(iso: string): string {
92
+ const ms = Date.now() - new Date(iso).getTime();
93
+ const s = Math.floor(ms / 1000);
94
+ if (s < 60) return "just now";
95
+ const m = Math.floor(s / 60);
96
+ if (m < 60) return `${m}m ago`;
97
+ const h = Math.floor(m / 60);
98
+ if (h < 24) return `${h}h ago`;
99
+ const d = Math.floor(h / 24);
100
+ return `${d}d ago`;
101
+ }
102
+
103
+ export function pause(ms: number): Promise<void> {
104
+ return new Promise(resolve => setTimeout(resolve, ms));
105
+ }
@@ -0,0 +1,178 @@
1
+ import { withDaemon, type DaemonClient } from "./daemon.ts";
2
+
3
+ export async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
4
+ await withDaemon(async (client) => {
5
+ const { daemonCall } = client;
6
+
7
+ if (sub === "create") {
8
+ await layerCreateCommand(client, rest);
9
+ return;
10
+ }
11
+ if (sub === "snap") {
12
+ await layerSnapCommand(client, rest[0]);
13
+ return;
14
+ }
15
+ if (sub === "session" || sub === "sessions") {
16
+ await layerSessionCommand(client, rest[0]);
17
+ return;
18
+ }
19
+ if (sub === "clear") {
20
+ await daemonCall("session.layers.clear");
21
+ console.log("Cleared all session layers.");
22
+ return;
23
+ }
24
+ if (sub === "delete" || sub === "rm") {
25
+ if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
26
+ await daemonCall("session.layers.delete", { name: rest[0] });
27
+ console.log(`Deleted session layer "${rest[0]}".`);
28
+ return;
29
+ }
30
+
31
+ if (sub === undefined || sub === null || sub === "") {
32
+ const result = await daemonCall("layers.list") as any;
33
+ if (!result.layers.length) {
34
+ console.log("No layers configured.");
35
+ return;
36
+ }
37
+ console.log("Layers:\n");
38
+ for (const layer of result.layers) {
39
+ const active = layer.index === result.active ? " \x1b[32m● active\x1b[0m" : "";
40
+ console.log(` [${layer.index}] ${layer.label} (${layer.projectCount} projects)${active}`);
41
+ }
42
+ return;
43
+ }
44
+ const idx = parseInt(sub, 10);
45
+ if (!isNaN(idx)) {
46
+ await daemonCall("layer.activate", { index: idx, mode: "launch" });
47
+ console.log(`Activated layer ${idx}`);
48
+ } else {
49
+ await daemonCall("layer.activate", { name: sub, mode: "launch" });
50
+ console.log(`Activated layer "${sub}"`);
51
+ }
52
+ });
53
+ }
54
+
55
+ // ── Layer create: build a session layer from window specs ────────────
56
+ // Usage: lattices layer create <name> [wid:123 wid:456 ...]
57
+ // lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
58
+ export async function layerCreateCommand(client: DaemonClient, args: string[]): Promise<void> {
59
+ const { daemonCall } = client;
60
+ const name = args[0];
61
+ if (!name) {
62
+ console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
63
+ return;
64
+ }
65
+
66
+ const jsonIdx = args.indexOf("--json");
67
+ if (jsonIdx !== -1 && args[jsonIdx + 1]) {
68
+ // JSON mode: parse window specs with tile positions
69
+ const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
70
+ wid?: number; app?: string; title?: string; tile?: string;
71
+ }>;
72
+
73
+ // Collect wids, resolve app-based specs
74
+ const windowIds: number[] = [];
75
+ const windows: Array<{ app: string; contentHint?: string }> = [];
76
+ const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
77
+
78
+ for (const spec of specs) {
79
+ if (spec.wid) {
80
+ windowIds.push(spec.wid);
81
+ if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
82
+ } else if (spec.app) {
83
+ windows.push({ app: spec.app, contentHint: spec.title });
84
+ if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
85
+ }
86
+ }
87
+
88
+ await daemonCall("session.layers.create", {
89
+ name,
90
+ ...(windowIds.length ? { windowIds } : {}),
91
+ ...(windows.length ? { windows } : {}),
92
+ }) as any;
93
+
94
+ console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
95
+
96
+ // Apply tile positions
97
+ for (const t of tiles) {
98
+ try {
99
+ await daemonCall("window.place", {
100
+ ...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
101
+ placement: t.tile,
102
+ });
103
+ } catch { /* window may not be resolved yet */ }
104
+ }
105
+
106
+ if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
107
+ return;
108
+ }
109
+
110
+ // Simple wid mode: lattices layer create <name> wid:123 wid:456
111
+ const wids = args.slice(1)
112
+ .filter(a => a.startsWith("wid:"))
113
+ .map(a => parseInt(a.slice(4), 10))
114
+ .filter(n => !isNaN(n));
115
+
116
+ await daemonCall("session.layers.create", {
117
+ name,
118
+ ...(wids.length ? { windowIds: wids } : {}),
119
+ }) as any;
120
+
121
+ console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
122
+ }
123
+
124
+ // ── Layer snap: snapshot current visible windows into a session layer ─
125
+ export async function layerSnapCommand(client: DaemonClient, name?: string): Promise<void> {
126
+ const { daemonCall } = client;
127
+ const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
128
+
129
+ // Get all current windows
130
+ const windows = await daemonCall("windows.list") as any[];
131
+ const visibleWids = windows
132
+ .filter((w: any) => !w.isMinimized && w.app !== "lattices")
133
+ .map((w: any) => w.wid);
134
+
135
+ if (!visibleWids.length) {
136
+ console.log("No visible windows to snapshot.");
137
+ return;
138
+ }
139
+
140
+ await daemonCall("session.layers.create", {
141
+ name: layerName,
142
+ windowIds: visibleWids,
143
+ });
144
+
145
+ console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
146
+ }
147
+
148
+ // ── Layer session: list or switch session layers ─────────────────────
149
+ export async function layerSessionCommand(client: DaemonClient, nameOrIndex?: string): Promise<void> {
150
+ const { daemonCall } = client;
151
+ const result = await daemonCall("session.layers.list") as any;
152
+
153
+ if (!nameOrIndex) {
154
+ // List session layers
155
+ if (!result.layers.length) {
156
+ console.log("No session layers. Create one with: lattices layer create <name>");
157
+ return;
158
+ }
159
+ console.log("Session layers:\n");
160
+ for (let i = 0; i < result.layers.length; i++) {
161
+ const l = result.layers[i];
162
+ const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
163
+ const winCount = l.windows?.length || 0;
164
+ console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
165
+ }
166
+ return;
167
+ }
168
+
169
+ // Switch by index or name
170
+ const idx = parseInt(nameOrIndex, 10);
171
+ if (!isNaN(idx)) {
172
+ await daemonCall("session.layers.switch", { index: idx });
173
+ console.log(`Switched to session layer ${idx}.`);
174
+ } else {
175
+ await daemonCall("session.layers.switch", { name: nameOrIndex });
176
+ console.log(`Switched to session layer "${nameOrIndex}".`);
177
+ }
178
+ }
@@ -0,0 +1,43 @@
1
+ import { hasFlag, nonFlagArgs, parseFlagValue } from "./helpers.ts";
2
+ import { withDaemon } from "./daemon.ts";
3
+
4
+ function runLine(run: any): string {
5
+ const count = Array.isArray(run.artifacts) ? run.artifacts.length : 0;
6
+ const completed = run.completedAt ? ` completed=${run.completedAt}` : "";
7
+ return ` ${run.id} ${run.state || "?"} artifacts=${count} ${run.title || "Untitled run"}${completed}`;
8
+ }
9
+
10
+ export async function runsCommand(rawArgs: string[] = []): Promise<void> {
11
+ const jsonFlag = hasFlag(rawArgs, "json");
12
+ const positional = nonFlagArgs(rawArgs);
13
+ const sub = positional[0];
14
+
15
+ await withDaemon(async ({ daemonCall }) => {
16
+ if (sub && sub !== "list") {
17
+ const run = await daemonCall("runs.get", { id: sub }) as any;
18
+ if (jsonFlag) {
19
+ console.log(JSON.stringify(run, null, 2));
20
+ return;
21
+ }
22
+ console.log(runLine(run));
23
+ console.log(` artifacts: ${run.artifactDirectoryPath}`);
24
+ for (const artifact of run.artifacts || []) {
25
+ console.log(` ${artifact.kind} ${artifact.path}`);
26
+ }
27
+ return;
28
+ }
29
+
30
+ const limit = Number(parseFlagValue(rawArgs, "limit") || 20);
31
+ const runs = await daemonCall("runs.list", { limit }) as any[];
32
+ if (jsonFlag) {
33
+ console.log(JSON.stringify(runs, null, 2));
34
+ return;
35
+ }
36
+ if (!runs.length) {
37
+ console.log("No runs yet.");
38
+ return;
39
+ }
40
+ console.log(`Runs (${runs.length}):\n`);
41
+ for (const run of runs) console.log(runLine(run));
42
+ });
43
+ }
@@ -0,0 +1,141 @@
1
+ import { relativeTime } from "./helpers.ts";
2
+ import { withDaemon, type DaemonClient } from "./daemon.ts";
3
+
4
+ export interface SearchResult {
5
+ score: number;
6
+ window: any;
7
+ tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
8
+ reasons: string[];
9
+ }
10
+
11
+ export interface SearchOptions {
12
+ sources?: string[];
13
+ after?: string;
14
+ before?: string;
15
+ recency?: boolean;
16
+ mode?: string;
17
+ }
18
+
19
+ async function searchWithClient(client: DaemonClient, query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
20
+ const { daemonCall } = client;
21
+ const params: Record<string, any> = { query };
22
+ if (opts.sources) params.sources = opts.sources;
23
+ if (opts.after) params.after = opts.after;
24
+ if (opts.before) params.before = opts.before;
25
+ if (opts.recency !== undefined) params.recency = opts.recency;
26
+ if (opts.mode) params.mode = opts.mode;
27
+ const hits = await daemonCall("lattices.search", params, 10000) as any[];
28
+ return hits.map((w: any) => ({
29
+ score: w.score || 0,
30
+ window: w,
31
+ tabs: (w.terminalTabs || []).map((t: any) => ({
32
+ tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
33
+ })),
34
+ reasons: w.matchSources || [],
35
+ }));
36
+ }
37
+
38
+ export async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
39
+ return withDaemon(client => searchWithClient(client, query, opts));
40
+ }
41
+
42
+ export async function deepSearch(query: string): Promise<SearchResult[]> {
43
+ return search(query, { sources: ["all"] });
44
+ }
45
+
46
+ export function printResults(ranked: SearchResult[]): void {
47
+ if (!ranked.length) return;
48
+ for (const r of ranked) {
49
+ const w = r.window;
50
+ const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
51
+ console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
52
+ for (const t of r.tabs) {
53
+ const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
54
+ const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
55
+ console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
56
+ }
57
+ if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
58
+ }
59
+ console.log();
60
+ }
61
+
62
+ export async function searchCommand(
63
+ query: string | undefined,
64
+ flags: Set<string>,
65
+ rawArgs: string[] = []
66
+ ): Promise<void> {
67
+ if (!query) {
68
+ console.log("Usage: lattices search <query> [--quick | --terminal | --all | --deep | --sources=... | --after=... | --before=... | --json | --wid]");
69
+ return;
70
+ }
71
+
72
+ const opts: SearchOptions = {};
73
+
74
+ const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
75
+ if (sourcesFlag) {
76
+ opts.sources = sourcesFlag.slice("--sources=".length).split(",");
77
+ } else if (flags.has("--all") || flags.has("--deep")) {
78
+ opts.sources = ["all"];
79
+ } else if (flags.has("--quick")) {
80
+ opts.sources = ["titles", "apps", "sessions"];
81
+ } else if (flags.has("--terminal")) {
82
+ opts.sources = ["terminals"];
83
+ }
84
+
85
+ const afterFlag = rawArgs.find(a => a.startsWith("--after="));
86
+ if (afterFlag) opts.after = afterFlag.slice("--after=".length);
87
+ const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
88
+ if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
89
+
90
+ if (flags.has("--no-recency")) opts.recency = false;
91
+
92
+ const ranked = await search(query, opts);
93
+ const jsonOut = flags.has("--json");
94
+ const widOnly = flags.has("--wid");
95
+
96
+ if (jsonOut) {
97
+ console.log(JSON.stringify(ranked.map(r => ({
98
+ wid: r.window.wid, app: r.window.app, title: r.window.title,
99
+ score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
100
+ })), null, 2));
101
+ return;
102
+ }
103
+
104
+ if (widOnly) {
105
+ for (const r of ranked) console.log(r.window.wid);
106
+ return;
107
+ }
108
+
109
+ if (!ranked.length) {
110
+ console.log(`No results for "${query}"`);
111
+ return;
112
+ }
113
+
114
+ printResults(ranked);
115
+ }
116
+
117
+ export async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
118
+ if (!query) {
119
+ console.log("Usage: lattices place <query> [position]");
120
+ return;
121
+ }
122
+
123
+ await withDaemon(async (client) => {
124
+ const { daemonCall } = client;
125
+ const ranked = await searchWithClient(client, query, { sources: ["all"] });
126
+
127
+ if (!ranked.length) {
128
+ console.log(`No window matching "${query}"`);
129
+ return;
130
+ }
131
+
132
+ const pos = tilePosition || "bottom-right";
133
+ const win = ranked[0].window;
134
+ await daemonCall("window.focus", { wid: win.wid });
135
+ await daemonCall("intents.execute", {
136
+ intent: "tile_window",
137
+ slots: { position: pos, wid: win.wid }
138
+ }, 3000);
139
+ console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
140
+ });
141
+ }
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+ import { basename, resolve } from "node:path";
3
+ import { runQuiet } from "./helpers.ts";
4
+
5
+ export function pathHash(dir: string): string {
6
+ return createHash("sha256").update(resolve(dir)).digest("hex").slice(0, 6);
7
+ }
8
+
9
+ export function toSessionName(dir: string): string {
10
+ const base = basename(dir).replace(/[^a-zA-Z0-9_-]/g, "-");
11
+ return `${base}-${pathHash(dir)}`;
12
+ }
13
+
14
+ export function esc(str: string): string {
15
+ return str.replace(/'/g, "'\\''");
16
+ }
17
+
18
+ export function slugify(str: string): string {
19
+ return str
20
+ .toLowerCase()
21
+ .replace(/\.app$/i, "")
22
+ .replace(/[^a-z0-9_-]+/g, "-")
23
+ .replace(/^-+|-+$/g, "") || "app";
24
+ }
25
+
26
+ export function sessionExists(name: string): boolean {
27
+ return runQuiet(`tmux has-session -t "${name}" 2>&1`) !== null;
28
+ }
29
+
30
+ export function toGroupSessionName(groupId: string): string {
31
+ return `lattices-group-${groupId}`;
32
+ }
package/bin/client.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Public API — re-exports from daemon-client for a cleaner import path.
2
- // Usage: import { daemonCall, isDaemonRunning } from '@lattices/cli'
2
+ // Usage: import { daemonCall, isDaemonRunning } from '@arach/lattices'
3
+ // Also published as @lattices/cli for back-compat.
3
4
 
4
5
  export { daemonCall, isDaemonRunning } from "./daemon-client.ts";
5
6
  export {
package/bin/cua.ts ADDED
@@ -0,0 +1,26 @@
1
+ export {
2
+ click,
3
+ computerClickParamsSchema,
4
+ computerClickTransportSchema,
5
+ computerMagicCursorParamsSchema,
6
+ computerTreatmentSchema,
7
+ createCuaClient,
8
+ cua,
9
+ cursorEdgeSchema,
10
+ cursorGlowSchema,
11
+ cursorIdleSchema,
12
+ cursorMotionSchema,
13
+ cursorShapeSchema,
14
+ cursorSizeSchema,
15
+ cursorStyleSchema,
16
+ cursorTrailSchema,
17
+ cursorTrajectorySchema,
18
+ magicCursor,
19
+ type ComputerClickParams,
20
+ type ComputerMagicCursorParams,
21
+ type CursorEdge,
22
+ type CursorGlow,
23
+ type CursorIdle,
24
+ type CuaClient,
25
+ type CuaClientOptions,
26
+ } from "../packages/npm/sdk/cua.mjs";
package/bin/infer.ts CHANGED
@@ -16,6 +16,7 @@ import { createXai } from "@ai-sdk/xai";
16
16
  import { readFileSync, existsSync } from "fs";
17
17
  import { homedir } from "os";
18
18
  import { join } from "path";
19
+ import { getKeychainSecret } from "./keychain";
19
20
 
20
21
  // ── Types ──────────────────────────────────────────────────────────
21
22
 
@@ -49,20 +50,23 @@ export interface InferResult {
49
50
  // ── Default models per provider ────────────────────────────────────
50
51
 
51
52
  const PROVIDER_NAMES: ProviderName[] = ["groq", "openai", "anthropic", "google", "xai", "minimax"];
52
- const VOICE_PROVIDER_PRIORITY: ProviderName[] = ["groq", "xai", "openai", "google", "anthropic", "minimax"];
53
+ const VOICE_PROVIDER_PRIORITY: ProviderName[] = ["xai", "groq", "openai", "google", "anthropic", "minimax"];
53
54
 
54
55
  const DEFAULT_MODELS: Record<ProviderName, string> = {
55
56
  groq: "llama-3.3-70b-versatile",
56
57
  openai: "gpt-4o-mini",
57
58
  anthropic: "claude-sonnet-4-6",
58
59
  google: "gemini-2.0-flash",
59
- xai: "grok-4-1-fast-non-reasoning",
60
+ xai: "grok-4.20-reasoning",
60
61
  minimax: "MiniMax-M2.5-highspeed",
61
62
  };
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.
63
68
  const VOICE_DEFAULT_MODELS: Record<ProviderName, string> = {
64
69
  ...DEFAULT_MODELS,
65
- groq: "llama-3.1-8b-instant",
66
70
  };
67
71
 
68
72
  // ── Credential loading ─────────────────────────────────────────────
@@ -163,7 +167,10 @@ function loadCredentials(): CredentialStore {
163
167
  const openaiKey = getInferenceEnv("OPENAI_API_KEY");
164
168
  const anthropicKey = getInferenceEnv("ANTHROPIC_API_KEY");
165
169
  const googleKey = getInferenceEnv("GOOGLE_GENERATIVE_AI_API_KEY");
166
- const xaiKey = getInferenceEnv("XAI_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");
167
174
  const minimaxKey = getInferenceEnv("MINIMAX_API_KEY");
168
175
  if (groqKey) creds.groq = groqKey;
169
176
  if (openaiKey) creds.openai = openaiKey;
@@ -204,6 +211,17 @@ function loadCredentials(): CredentialStore {
204
211
  } catch {}
205
212
  }
206
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
+
207
225
  _cachedCreds = creds;
208
226
  return creds;
209
227
  }
@@ -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
+ }