@lattices/cli 0.5.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 (46) hide show
  1. package/README.md +14 -5
  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/proposals/LAT-007-unified-app-shell.md +128 -0
  36. package/docs/quickstart.md +3 -1
  37. package/docs/reference/dewey.config.ts +1 -1
  38. package/docs/release.md +4 -3
  39. package/docs/repo-structure.md +1 -0
  40. package/docs/terminal-kit.md +87 -0
  41. package/docs/tiling-reference.md +5 -3
  42. package/docs/voice.md +3 -3
  43. package/package.json +29 -5
  44. package/packages/npm/sdk/cua.d.mts +1 -0
  45. package/packages/npm/sdk/cua.d.ts +188 -0
  46. package/packages/npm/sdk/cua.mjs +376 -0
package/bin/lattices.ts CHANGED
@@ -1,10 +1,30 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { createHash } from "node:crypto";
4
3
  import { execSync } from "node:child_process";
5
4
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
5
  import { basename, dirname, isAbsolute, resolve } from "node:path";
7
6
  import { homedir } from "node:os";
7
+ import { withDaemon, type DaemonClient } from "./cli/daemon.ts";
8
+ import {
9
+ hasFlag,
10
+ nonFlagArgs,
11
+ parseFlagValue,
12
+ parseOptionalNumber,
13
+ pause,
14
+ run,
15
+ runQuiet,
16
+ } from "./cli/helpers.ts";
17
+ import { searchCommand, placeCommand } from "./cli/search.ts";
18
+ import { captureCommand } from "./cli/capture.ts";
19
+ import { layerCommand } from "./cli/layer.ts";
20
+ import { runsCommand } from "./cli/runs.ts";
21
+ import {
22
+ esc,
23
+ sessionExists,
24
+ slugify,
25
+ toGroupSessionName,
26
+ toSessionName,
27
+ } from "./cli/session.ts";
8
28
 
9
29
  // Daemon client (lazy-loaded to avoid blocking startup for TTY commands)
10
30
  let _daemonClient: typeof import("./daemon-client.ts") | undefined;
@@ -20,25 +40,6 @@ const command: string | undefined = args[0];
20
40
 
21
41
  // ── Helpers ──────────────────────────────────────────────────────────
22
42
 
23
- interface ExecOpts {
24
- encoding?: string;
25
- stdio?: string | string[];
26
- cwd?: string;
27
- [key: string]: any;
28
- }
29
-
30
- function run(cmd: string, opts: ExecOpts = {}): string {
31
- return execSync(cmd, { encoding: "utf8", ...opts } as any).trim();
32
- }
33
-
34
- function runQuiet(cmd: string): string | null {
35
- try {
36
- return run(cmd, { stdio: "pipe" });
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
43
  function hasTmux(): boolean {
43
44
  return runQuiet("which tmux") !== null;
44
45
  }
@@ -78,70 +79,10 @@ function isInsideTmux(): boolean {
78
79
  return !!process.env.TMUX;
79
80
  }
80
81
 
81
- function sessionExists(name: string): boolean {
82
- return runQuiet(`tmux has-session -t "${name}" 2>&1`) !== null;
83
- }
84
-
85
- function pathHash(dir: string): string {
86
- return createHash("sha256").update(resolve(dir)).digest("hex").slice(0, 6);
87
- }
88
-
89
- function toSessionName(dir: string): string {
90
- const base = basename(dir).replace(/[^a-zA-Z0-9_-]/g, "-");
91
- return `${base}-${pathHash(dir)}`;
92
- }
93
-
94
- function esc(str: string): string {
95
- return str.replace(/'/g, "'\\''");
96
- }
97
-
98
82
  function appleScriptString(str: string): string {
99
83
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
100
84
  }
101
85
 
102
- function slugify(str: string): string {
103
- return str
104
- .toLowerCase()
105
- .replace(/\.app$/i, "")
106
- .replace(/[^a-z0-9_-]+/g, "-")
107
- .replace(/^-+|-+$/g, "") || "app";
108
- }
109
-
110
- function parseFlagValue(args: string[], name: string): string | undefined {
111
- const prefix = `--${name}=`;
112
- const exact = `--${name}`;
113
- for (let i = 0; i < args.length; i++) {
114
- if (args[i].startsWith(prefix)) return args[i].slice(prefix.length);
115
- if (args[i] === exact) return args[i + 1];
116
- }
117
- return undefined;
118
- }
119
-
120
- function hasFlag(args: string[], name: string): boolean {
121
- return args.includes(`--${name}`);
122
- }
123
-
124
- function nonFlagArgs(args: string[]): string[] {
125
- const valueFlags = new Set([
126
- "id", "state", "ttl", "ttlMs", "x", "y", "gap", "placement", "style", "name", "scale",
127
- "hud-url", "hudUrl", "hud-html", "hudHTML", "hudHtml", "hud-title", "hudTitle",
128
- "hud-width", "hudWidth", "hud-height", "hudHeight", "width", "height",
129
- "manifest", "root", "max-depth", "maxDepth", "read-access", "readAccess",
130
- "pause",
131
- ]);
132
- const out: string[] = [];
133
- for (let i = 0; i < args.length; i++) {
134
- const arg = args[i];
135
- if (!arg.startsWith("--")) {
136
- out.push(arg);
137
- continue;
138
- }
139
- const flagName = arg.slice(2);
140
- if (!arg.includes("=") && valueFlags.has(flagName)) i++;
141
- }
142
- return out;
143
- }
144
-
145
86
  // ── Config ───────────────────────────────────────────────────────────
146
87
 
147
88
  function readConfig(dir: string): any | null {
@@ -170,10 +111,6 @@ function readWorkspaceConfig(): any | null {
170
111
  }
171
112
  }
172
113
 
173
- function toGroupSessionName(groupId: string): string {
174
- return `lattices-group-${groupId}`;
175
- }
176
-
177
114
  /** Get ordered pane IDs for a specific window within a session */
178
115
  function getPaneIdsForWindow(sessionName: string, windowIndex: number): string[] {
179
116
  const out = runQuiet(
@@ -883,15 +820,16 @@ function restartPane(target?: string): void {
883
820
  // ── Daemon-aware commands ────────────────────────────────────────────
884
821
 
885
822
  async function mouseCommand(sub?: string): Promise<void> {
886
- const { daemonCall } = await getDaemonClient();
887
- if (sub === "summon") {
888
- const result = await daemonCall("mouse.summon") as any;
889
- console.log(`🎯 Mouse summoned to (${result.x}, ${result.y})`);
890
- } else {
891
- // Default: find
892
- const result = await daemonCall("mouse.find") as any;
893
- console.log(`🔍 Mouse at (${result.x}, ${result.y})`);
894
- }
823
+ await withDaemon(async ({ daemonCall }) => {
824
+ if (sub === "summon") {
825
+ const result = await daemonCall("mouse.summon") as any;
826
+ console.log(`🎯 Mouse summoned to (${result.x}, ${result.y})`);
827
+ } else {
828
+ // Default: find
829
+ const result = await daemonCall("mouse.find") as any;
830
+ console.log(`🔍 Mouse at (${result.x}, ${result.y})`);
831
+ }
832
+ });
895
833
  }
896
834
 
897
835
  async function daemonStatusCommand(): Promise<void> {
@@ -907,7 +845,7 @@ async function daemonStatusCommand(): Promise<void> {
907
845
  console.log(` uptime: ${uptimeStr}`);
908
846
  console.log(` clients: ${status.clientCount}`);
909
847
  console.log(` windows: ${status.windowCount}`);
910
- console.log(` tmux: ${status.tmuxSessionCount} sessions`);
848
+ console.log(` sessions: ${status.tmuxSessionCount}`);
911
849
  console.log(` version: ${status.version}`);
912
850
  } catch {
913
851
  console.log("\x1b[90m○\x1b[0m Daemon not running (start with: lattices app)");
@@ -915,8 +853,7 @@ async function daemonStatusCommand(): Promise<void> {
915
853
  }
916
854
 
917
855
  async function windowsCommand(jsonFlag: boolean): Promise<void> {
918
- try {
919
- const { daemonCall } = await getDaemonClient();
856
+ await withDaemon(async ({ daemonCall }) => {
920
857
  const windows = await daemonCall("windows.list") as any[];
921
858
  if (jsonFlag) {
922
859
  console.log(JSON.stringify(windows, null, 2));
@@ -936,9 +873,7 @@ async function windowsCommand(jsonFlag: boolean): Promise<void> {
936
873
  console.log(` ${Math.round(w.frame.w)}×${Math.round(w.frame.h)} at (${Math.round(w.frame.x)},${Math.round(w.frame.y)})`);
937
874
  console.log();
938
875
  }
939
- } catch {
940
- console.log("Daemon not running. Start with: lattices app");
941
- }
876
+ });
942
877
  }
943
878
 
944
879
  async function windowAssignCommand(wid?: string, layerId?: string): Promise<void> {
@@ -946,18 +881,14 @@ async function windowAssignCommand(wid?: string, layerId?: string): Promise<void
946
881
  console.log("Usage: lattices window assign <wid> <layer-id>");
947
882
  return;
948
883
  }
949
- try {
950
- const { daemonCall } = await getDaemonClient();
884
+ await withDaemon(async ({ daemonCall }) => {
951
885
  await daemonCall("window.assignLayer", { wid: parseInt(wid), layer: layerId });
952
886
  console.log(`Tagged wid:${wid} → layer:${layerId}`);
953
- } catch (e: unknown) {
954
- console.log(`Error: ${(e as Error).message}`);
955
- }
887
+ });
956
888
  }
957
889
 
958
890
  async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
959
- try {
960
- const { daemonCall } = await getDaemonClient();
891
+ await withDaemon(async ({ daemonCall }) => {
961
892
  const map = await daemonCall("window.layerMap") as any;
962
893
  if (jsonFlag) {
963
894
  console.log(JSON.stringify(map, null, 2));
@@ -972,9 +903,7 @@ async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
972
903
  for (const [wid, layer] of entries) {
973
904
  console.log(` wid:${wid} → ${layer}`);
974
905
  }
975
- } catch {
976
- console.log("Daemon not running. Start with: lattices app");
977
- }
906
+ });
978
907
  }
979
908
 
980
909
  async function focusCommand(session?: string): Promise<void> {
@@ -982,175 +911,10 @@ async function focusCommand(session?: string): Promise<void> {
982
911
  console.log("Usage: lattices focus <session-name>");
983
912
  return;
984
913
  }
985
- try {
986
- const { daemonCall } = await getDaemonClient();
914
+ await withDaemon(async ({ daemonCall }) => {
987
915
  await daemonCall("window.focus", { session });
988
916
  console.log(`Focused: ${session}`);
989
- } catch (e: unknown) {
990
- console.log(`Error: ${(e as Error).message}`);
991
- }
992
- }
993
-
994
- // ── Search ───────────────────────────────────────────────────────────
995
-
996
- interface SearchResult {
997
- score: number;
998
- window: any;
999
- tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
1000
- reasons: string[];
1001
- }
1002
-
1003
- function relativeTime(iso: string): string {
1004
- const ms = Date.now() - new Date(iso).getTime();
1005
- const s = Math.floor(ms / 1000);
1006
- if (s < 60) return "just now";
1007
- const m = Math.floor(s / 60);
1008
- if (m < 60) return `${m}m ago`;
1009
- const h = Math.floor(m / 60);
1010
- if (h < 24) return `${h}h ago`;
1011
- const d = Math.floor(h / 24);
1012
- return `${d}d ago`;
1013
- }
1014
-
1015
- // Unified search via lattices.search daemon API.
1016
- // All search surfaces should go through this one function.
1017
- interface SearchOptions {
1018
- sources?: string[]; // e.g. ["titles", "apps", "cwd", "ocr"] — omit for smart default
1019
- after?: string; // ISO8601 — only windows interacted after this time
1020
- before?: string; // ISO8601 — only windows interacted before this time
1021
- recency?: boolean; // boost recently-focused windows (default true)
1022
- mode?: string; // legacy compat: "quick", "complete", "terminal"
1023
- }
1024
-
1025
- async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
1026
- const { daemonCall } = await getDaemonClient();
1027
- const params: Record<string, any> = { query };
1028
- if (opts.sources) params.sources = opts.sources;
1029
- if (opts.after) params.after = opts.after;
1030
- if (opts.before) params.before = opts.before;
1031
- if (opts.recency !== undefined) params.recency = opts.recency;
1032
- if (opts.mode) params.mode = opts.mode; // legacy fallback
1033
- const hits = await daemonCall("lattices.search", params, 10000) as any[];
1034
- return hits.map((w: any) => ({
1035
- score: w.score || 0,
1036
- window: w,
1037
- tabs: (w.terminalTabs || []).map((t: any) => ({
1038
- tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
1039
- })),
1040
- reasons: w.matchSources || [],
1041
- }));
1042
- }
1043
-
1044
- // Convenience aliases
1045
- async function deepSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["all"] }); }
1046
- async function terminalSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["terminals"] }); }
1047
-
1048
- // Format and print search results
1049
- function printResults(ranked: SearchResult[]): void {
1050
- if (!ranked.length) return;
1051
- for (const r of ranked) {
1052
- const w = r.window;
1053
- const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
1054
- console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
1055
- for (const t of r.tabs) {
1056
- const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
1057
- const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
1058
- console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
1059
- }
1060
- if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
1061
- }
1062
- console.log();
1063
- }
1064
-
1065
- // ── search command ───────────────────────────────────────────────────
1066
-
1067
- async function searchCommand(query: string | undefined, flags: Set<string>, rawArgs: string[] = []): Promise<void> {
1068
- if (!query) {
1069
- console.log("Usage: lattices search <query> [--quick | --terminal | --all | --sources=... | --after=... | --before=... | --json | --wid]");
1070
- return;
1071
- }
1072
-
1073
- // Build search options from flags
1074
- const opts: SearchOptions = {};
1075
-
1076
- // Source selection: explicit --sources, or legacy --quick/--terminal, or default
1077
- const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
1078
- if (sourcesFlag) {
1079
- opts.sources = sourcesFlag.slice("--sources=".length).split(",");
1080
- } else if (flags.has("--all")) {
1081
- opts.sources = ["all"];
1082
- } else if (flags.has("--quick")) {
1083
- opts.sources = ["titles", "apps", "sessions"];
1084
- } else if (flags.has("--terminal")) {
1085
- opts.sources = ["terminals"];
1086
- }
1087
- // else: omit → smart default on daemon side
1088
-
1089
- // Time filters
1090
- const afterFlag = rawArgs.find(a => a.startsWith("--after="));
1091
- if (afterFlag) opts.after = afterFlag.slice("--after=".length);
1092
- const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
1093
- if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
1094
-
1095
- // No-recency flag
1096
- if (flags.has("--no-recency")) opts.recency = false;
1097
-
1098
- const ranked = await search(query, opts);
1099
- const jsonOut = flags.has("--json");
1100
- const widOnly = flags.has("--wid");
1101
-
1102
- if (jsonOut) {
1103
- console.log(JSON.stringify(ranked.map(r => ({
1104
- wid: r.window.wid, app: r.window.app, title: r.window.title,
1105
- score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
1106
- })), null, 2));
1107
- return;
1108
- }
1109
-
1110
- if (widOnly) {
1111
- for (const r of ranked) console.log(r.window.wid);
1112
- return;
1113
- }
1114
-
1115
- if (!ranked.length) {
1116
- console.log(`No results for "${query}"`);
1117
- return;
1118
- }
1119
-
1120
- printResults(ranked);
1121
- }
1122
-
1123
- // ── place command ────────────────────────────────────────────────────
1124
-
1125
- async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
1126
- if (!query) {
1127
- console.log("Usage: lattices place <query> [position]");
1128
- return;
1129
- }
1130
- try {
1131
- const { daemonCall } = await getDaemonClient();
1132
- const ranked = await deepSearch(query);
1133
-
1134
- if (!ranked.length) {
1135
- console.log(`No window matching "${query}"`);
1136
- return;
1137
- }
1138
-
1139
- const pos = tilePosition || "bottom-right";
1140
- const win = ranked[0].window;
1141
- await daemonCall("window.focus", { wid: win.wid });
1142
- await daemonCall("intents.execute", {
1143
- intent: "tile_window",
1144
- slots: { position: pos, wid: win.wid }
1145
- }, 3000);
1146
- console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
1147
- } catch (e: unknown) {
1148
- console.log(`Error: ${(e as Error).message}`);
1149
- }
1150
- }
1151
-
1152
- function pause(ms: number): Promise<void> {
1153
- return new Promise(resolve => setTimeout(resolve, ms));
917
+ });
1154
918
  }
1155
919
 
1156
920
  function receiptLine(receipt: any): string {
@@ -1164,90 +928,90 @@ function receiptLine(receipt: any): string {
1164
928
  }
1165
929
 
1166
930
  async function placementSmokeCommand(rawArgs: string[] = []): Promise<void> {
1167
- const { daemonCall } = await getDaemonClient();
1168
931
  const pauseMs = Number(parseFlagValue(rawArgs, "pause") || 1200);
1169
932
  const positional = nonFlagArgs(rawArgs);
1170
933
 
1171
- let sessions = positional.slice(0, 2);
1172
- if (sessions.length < 2) {
1173
- const tmuxSessions = await daemonCall("tmux.sessions") as any[];
1174
- sessions = tmuxSessions
1175
- .map(s => s?.name)
1176
- .filter((name: unknown): name is string => typeof name === "string" && name.startsWith("lattices-place-"))
1177
- .slice(0, 2);
1178
- }
1179
-
1180
- if (sessions.length < 2) {
1181
- console.log("Need two named sessions. Usage: lattices dev placement-smoke <session-a> <session-b>");
1182
- console.log("Tip: launch two small lattices fixture projects first, then rerun this command.");
1183
- return;
1184
- }
1185
-
1186
- const [a, b] = sessions;
1187
- console.log(`Placement smoke: ${a} + ${b}`);
934
+ await withDaemon(async ({ daemonCall }) => {
935
+ let sessions = positional.slice(0, 2);
936
+ if (sessions.length < 2) {
937
+ const tmuxSessions = await daemonCall("tmux.sessions") as any[];
938
+ sessions = tmuxSessions
939
+ .map(s => s?.name)
940
+ .filter((name: unknown): name is string => typeof name === "string" && name.startsWith("lattices-place-"))
941
+ .slice(0, 2);
942
+ }
1188
943
 
1189
- for (const session of sessions) {
1190
- const resolved = await daemonCall("window.resolve", {
1191
- target: { kind: "session", session },
1192
- placement: "left",
1193
- }) as any;
1194
- console.log(` resolve ${session}: wid=${resolved.wid ?? "?"} app=${resolved.app ?? "?"} resolution=${resolved.targetResolution ?? "?"}`);
1195
- }
944
+ if (sessions.length < 2) {
945
+ console.log("Need two named sessions. Usage: lattices dev placement-smoke <session-a> <session-b>");
946
+ console.log("Tip: launch two small lattices fixture projects first, then rerun this command.");
947
+ return;
948
+ }
1196
949
 
1197
- const beats = [
1198
- {
1199
- label: "beat 1: halves",
1200
- actions: [
1201
- { id: "a-left-half", type: "window.place", target: { kind: "session", session: a }, args: { placement: "left" } },
1202
- { id: "b-right-half", type: "window.place", target: { kind: "session", session: b }, args: { placement: "right" } },
1203
- ],
1204
- },
1205
- {
1206
- label: "beat 2: 4x4 corners",
1207
- actions: [
1208
- { id: "a-top-left-4x4", type: "window.place", target: { kind: "session", session: a }, args: { placement: "grid:4x4:0,0" } },
1209
- { id: "b-bottom-right-4x4", type: "window.place", target: { kind: "session", session: b }, args: { placement: "grid:4x4:3,3" } },
1210
- ],
1211
- },
1212
- {
1213
- label: "beat 3: workbench",
1214
- actions: [
1215
- {
1216
- id: "a-workbench-left",
1217
- type: "window.place",
1218
- target: { kind: "session", session: a },
1219
- args: { placement: { kind: "fractions", x: 0.02, y: 0.05, w: 0.62, h: 0.9 } },
1220
- },
1221
- {
1222
- id: "b-console-right",
1223
- type: "window.place",
1224
- target: { kind: "session", session: b },
1225
- args: { placement: { kind: "fractions", x: 0.67, y: 0.12, w: 0.3, h: 0.76 } },
1226
- },
1227
- ],
1228
- },
1229
- ];
950
+ const [a, b] = sessions;
951
+ console.log(`Placement smoke: ${a} + ${b}`);
952
+
953
+ for (const session of sessions) {
954
+ const resolved = await daemonCall("window.resolve", {
955
+ target: { kind: "session", session },
956
+ placement: "left",
957
+ }) as any;
958
+ console.log(` resolve ${session}: wid=${resolved.wid ?? "?"} app=${resolved.app ?? "?"} resolution=${resolved.targetResolution ?? "?"}`);
959
+ }
960
+
961
+ const beats = [
962
+ {
963
+ label: "beat 1: halves",
964
+ actions: [
965
+ { id: "a-left-half", type: "window.place", target: { kind: "session", session: a }, args: { placement: "left" } },
966
+ { id: "b-right-half", type: "window.place", target: { kind: "session", session: b }, args: { placement: "right" } },
967
+ ],
968
+ },
969
+ {
970
+ label: "beat 2: 4x4 corners",
971
+ actions: [
972
+ { id: "a-top-left-4x4", type: "window.place", target: { kind: "session", session: a }, args: { placement: "grid:4x4:0,0" } },
973
+ { id: "b-bottom-right-4x4", type: "window.place", target: { kind: "session", session: b }, args: { placement: "grid:4x4:3,3" } },
974
+ ],
975
+ },
976
+ {
977
+ label: "beat 3: workbench",
978
+ actions: [
979
+ {
980
+ id: "a-workbench-left",
981
+ type: "window.place",
982
+ target: { kind: "session", session: a },
983
+ args: { placement: { kind: "fractions", x: 0.02, y: 0.05, w: 0.62, h: 0.9 } },
984
+ },
985
+ {
986
+ id: "b-console-right",
987
+ type: "window.place",
988
+ target: { kind: "session", session: b },
989
+ args: { placement: { kind: "fractions", x: 0.67, y: 0.12, w: 0.3, h: 0.76 } },
990
+ },
991
+ ],
992
+ },
993
+ ];
1230
994
 
1231
- for (const beat of beats) {
1232
- console.log(`\n${beat.label}`);
1233
- const result = await daemonCall("actions.execute", {
1234
- source: "placement-smoke",
1235
- actions: beat.actions,
1236
- }, 15000) as any;
1237
- console.log(` batch=${result.status || "?"} request=${result.requestId || "?"}`);
1238
- for (const receipt of result.receipts || []) {
1239
- console.log(receiptLine(receipt));
995
+ for (const beat of beats) {
996
+ console.log(`\n${beat.label}`);
997
+ const result = await daemonCall("actions.execute", {
998
+ source: "placement-smoke",
999
+ actions: beat.actions,
1000
+ }, 15000) as any;
1001
+ console.log(` batch=${result.status || "?"} request=${result.requestId || "?"}`);
1002
+ for (const receipt of result.receipts || []) {
1003
+ console.log(receiptLine(receipt));
1004
+ }
1005
+ await pause(pauseMs);
1240
1006
  }
1241
- await pause(pauseMs);
1242
- }
1243
1007
 
1244
- const focused = await daemonCall("window.focus", { session: a }, 5000) as any;
1245
- console.log(`\nfocus ${a}: ok=${focused.ok === true} wid=${focused.wid ?? "?"} raised=${focused.raised === true}`);
1008
+ const focused = await daemonCall("window.focus", { session: a }, 5000) as any;
1009
+ console.log(`\nfocus ${a}: ok=${focused.ok === true} wid=${focused.wid ?? "?"} raised=${focused.raised === true}`);
1010
+ });
1246
1011
  }
1247
1012
 
1248
1013
  async function sessionsCommand(jsonFlag: boolean): Promise<void> {
1249
- try {
1250
- const { daemonCall } = await getDaemonClient();
1014
+ await withDaemon(async ({ daemonCall }) => {
1251
1015
  const sessions = await daemonCall("tmux.sessions") as any[];
1252
1016
  if (jsonFlag) {
1253
1017
  console.log(JSON.stringify(sessions, null, 2));
@@ -1262,71 +1026,388 @@ async function sessionsCommand(jsonFlag: boolean): Promise<void> {
1262
1026
  const windows = s.windowCount || s.windows || "?";
1263
1027
  console.log(` \x1b[1m${s.name}\x1b[0m (${windows} windows)`);
1264
1028
  }
1265
- } catch {
1266
- console.log("Daemon not running. Start with: lattices app");
1029
+ });
1030
+ }
1031
+
1032
+ async function terminalsCommand(rawArgs: string[] = []): Promise<void> {
1033
+ await withDaemon(async ({ daemonCall }) => {
1034
+ const jsonFlag = hasFlag(rawArgs, "json");
1035
+ const refresh = hasFlag(rawArgs, "refresh");
1036
+ const terminals = await daemonCall("terminals.list", { refresh }, refresh ? 15000 : undefined) as any[];
1037
+
1038
+ if (jsonFlag) {
1039
+ console.log(JSON.stringify(terminals, null, 2));
1040
+ return;
1041
+ }
1042
+ if (!terminals.length) {
1043
+ console.log("No terminal instances found.");
1044
+ return;
1045
+ }
1046
+
1047
+ console.log(`Terminals (${terminals.length}):\n`);
1048
+ for (const terminal of terminals) {
1049
+ const app = terminal.app || "terminal";
1050
+ const wid = terminal.windowId ? ` wid=${terminal.windowId}` : "";
1051
+ const cwd = terminal.cwd ? ` cwd=${terminal.cwd}` : "";
1052
+ const session = terminal.tmuxSession ? ` session=${terminal.tmuxSession}` : "";
1053
+ const claude = terminal.hasClaude ? " claude" : "";
1054
+ console.log(` ${app} ${terminal.tty}${wid}${session}${claude}`);
1055
+ if (terminal.displayName) console.log(` ${terminal.displayName}`);
1056
+ if (cwd) console.log(` ${cwd.trim()}`);
1057
+ }
1058
+ });
1059
+ }
1060
+
1061
+ async function computerCommand(subcommand?: string, ...rawArgs: string[]): Promise<void> {
1062
+ const sub = subcommand || "demo-terminal";
1063
+ const jsonFlag = hasFlag(rawArgs, "json");
1064
+ const aliases: Record<string, string> = {
1065
+ "demo-terminal": "computer.demoTerminal",
1066
+ "terminal-demo": "computer.demoTerminal",
1067
+ "term-demo": "computer.demoTerminal",
1068
+ "demo-scout": "computer.demoScout",
1069
+ "scout-demo": "computer.demoScout",
1070
+ "scout": "computer.demoScout",
1071
+ "prepare": "computer.prepare",
1072
+ "observe": "computer.prepare",
1073
+ "stage": "computer.prepare",
1074
+ "launch": "computer.launchApp",
1075
+ "launch-app": "computer.launchApp",
1076
+ "app": "computer.launchApp",
1077
+ "focus": "computer.focusWindow",
1078
+ "focus-window": "computer.focusWindow",
1079
+ "click": "computer.click",
1080
+ "mouse-click": "computer.click",
1081
+ "cursor": "computer.showCursor",
1082
+ "show-cursor": "computer.showCursor",
1083
+ "mouse-cursor": "computer.showCursor",
1084
+ "magic-cursor": "computer.magicCursor",
1085
+ "ghost-cursor": "computer.magicCursor",
1086
+ "move-cursor": "computer.magicCursor",
1087
+ "magic-scout": "computer.magicCursor",
1088
+ "scout-magic": "computer.magicCursor",
1089
+ "type": "computer.typeText",
1090
+ "type-text": "computer.typeText",
1091
+ "typetext": "computer.typeText",
1092
+ "type-window": "computer.typeWindowText",
1093
+ "type-app": "computer.typeWindowText",
1094
+ "app-type": "computer.typeWindowText",
1095
+ };
1096
+ const method = aliases[sub];
1097
+
1098
+ if (!method) {
1099
+ console.log(`lattices computer — run bounded computer-use actions
1100
+
1101
+ Usage:
1102
+ lattices computer prepare [--json] [--text "hello"]
1103
+ lattices computer focus-window [--json] [--wid id] [--app name]
1104
+ lattices computer launch-app Scout [--json]
1105
+ lattices computer type-window --app Scout --text "hello" [--x-ratio .5 --y-ratio .86] [--execute]
1106
+ lattices computer click --app Scout --x-ratio .5 --y-ratio .86 --treatment execute
1107
+ lattices computer click --app Scout --x-ratio .74 --y-ratio .95 --transport ax --ax-label Send --execute
1108
+ lattices cua click --app Scout --x-ratio .74 --y-ratio .95 --transport ax --ax-label Send --execute
1109
+ lattices computer magic-scout "draft text" --execute
1110
+ lattices computer scout [message] [--treatment present|execute] [--send]
1111
+ lattices computer cursor [--json] [--style marker] [--shape arrow] [--size tiny] [--trail thread]
1112
+ lattices computer type-text --text "hello" [--json] [--enter]
1113
+ lattices computer demo-terminal [--json] [--dry-run]
1114
+ lattices computer demo-terminal --text "hello" [--wid id] [--tty tty] [--iterm-session-id id] [--app iTerm2]
1115
+
1116
+ Common flags:
1117
+ --treatment observe|stage|present|execute
1118
+ --style spotlight|pulse|marker
1119
+ --shape arrow|needle|petal|shard|chevron|facet|wedge|prism|notch|kite
1120
+ --angle-deg -16..16
1121
+ --size tiny|small|regular|large
1122
+ --trail thread|ribbon|spark|comet|route|none
1123
+ --motion glide|snap|float|rush|crawl|accelerate|teleport|spring|magnet|slingshot
1124
+ --trajectory straight|soft|arc|swoop|overshoot
1125
+ --glow none|soft|halo|comet
1126
+ --idle still|breathe|wiggle|orbit|hover|nod|drift|shimmer|blink|tremble
1127
+ --edge none|pulse|ripple|tick|reticle|blink|spark|underline|echo|scan|pin
1128
+ --caption auto
1129
+ --caption-title "Spring reticle" --caption-body "AX text follows the cursor"
1130
+ --caption-tags "shape arrow,motion spring,edge reticle"
1131
+ --caption-placement top-left|top-right|bottom-left|bottom-right|top-center|center|near-cursor
1132
+ --caption-x-ratio 0.04 --caption-y-ratio 0.08
1133
+ --caption-lead-ms 650 --caption-sound engage
1134
+ --typewriter --type-interval-ms 18
1135
+ --transport auto|tmux|iterm|pasteboard
1136
+ --transport ax|pointer for app clicks
1137
+ --ax-label Send --no-focus
1138
+ --x-ratio 0..1 --y-ratio 0..1
1139
+ --from-x-ratio 0..1 --from-y-ratio 0..1
1140
+ --send
1141
+ --no-capture
1142
+ `);
1143
+ return;
1267
1144
  }
1145
+
1146
+ const params: Record<string, unknown> = { source: "cli" };
1147
+ const magicScout = sub === "magic-scout" || sub === "scout-magic";
1148
+ const positional = nonFlagArgs(rawArgs);
1149
+ let text = parseFlagValue(rawArgs, "text");
1150
+ const tty = parseFlagValue(rawArgs, "tty");
1151
+ const app = parseFlagValue(rawArgs, "app");
1152
+ const name = parseFlagValue(rawArgs, "name");
1153
+ const bundleId = parseFlagValue(rawArgs, "bundleId") || parseFlagValue(rawArgs, "bundle-id") || parseFlagValue(rawArgs, "bundleIdentifier");
1154
+ const path = parseFlagValue(rawArgs, "path") || parseFlagValue(rawArgs, "appPath") || parseFlagValue(rawArgs, "app-path");
1155
+ const wid = parseFlagValue(rawArgs, "wid");
1156
+ const terminalSessionId = parseFlagValue(rawArgs, "terminalSessionId")
1157
+ || parseFlagValue(rawArgs, "terminal-session-id")
1158
+ || parseFlagValue(rawArgs, "itermSessionId")
1159
+ || parseFlagValue(rawArgs, "iterm-session-id");
1160
+ const session = parseFlagValue(rawArgs, "session");
1161
+ const title = parseFlagValue(rawArgs, "title");
1162
+ const treatment = parseFlagValue(rawArgs, "treatment") || parseFlagValue(rawArgs, "mode") || parseFlagValue(rawArgs, "phase");
1163
+ const transport = parseFlagValue(rawArgs, "transport");
1164
+ const capture = parseFlagValue(rawArgs, "capture");
1165
+ const x = parseFlagValue(rawArgs, "x");
1166
+ const y = parseFlagValue(rawArgs, "y");
1167
+ const fromX = parseFlagValue(rawArgs, "fromX") || parseFlagValue(rawArgs, "from-x") || parseFlagValue(rawArgs, "startX") || parseFlagValue(rawArgs, "start-x");
1168
+ const fromY = parseFlagValue(rawArgs, "fromY") || parseFlagValue(rawArgs, "from-y") || parseFlagValue(rawArgs, "startY") || parseFlagValue(rawArgs, "start-y");
1169
+ const xRatio = parseFlagValue(rawArgs, "xRatio") || parseFlagValue(rawArgs, "x-ratio") || parseFlagValue(rawArgs, "relativeX") || parseFlagValue(rawArgs, "relative-x") || parseFlagValue(rawArgs, "windowX") || parseFlagValue(rawArgs, "window-x");
1170
+ const yRatio = parseFlagValue(rawArgs, "yRatio") || parseFlagValue(rawArgs, "y-ratio") || parseFlagValue(rawArgs, "relativeY") || parseFlagValue(rawArgs, "relative-y") || parseFlagValue(rawArgs, "windowY") || parseFlagValue(rawArgs, "window-y");
1171
+ const fromXRatio = parseFlagValue(rawArgs, "fromXRatio") || parseFlagValue(rawArgs, "from-x-ratio") || parseFlagValue(rawArgs, "startXRatio") || parseFlagValue(rawArgs, "start-x-ratio");
1172
+ const fromYRatio = parseFlagValue(rawArgs, "fromYRatio") || parseFlagValue(rawArgs, "from-y-ratio") || parseFlagValue(rawArgs, "startYRatio") || parseFlagValue(rawArgs, "start-y-ratio");
1173
+ const button = parseFlagValue(rawArgs, "button");
1174
+ const axLabel = parseFlagValue(rawArgs, "axLabel") || parseFlagValue(rawArgs, "ax-label") || parseFlagValue(rawArgs, "targetText") || parseFlagValue(rawArgs, "target-text");
1175
+ const appearance = parseFlagValue(rawArgs, "appearance") || parseFlagValue(rawArgs, "style") || parseFlagValue(rawArgs, "cursor-style") || parseFlagValue(rawArgs, "cursorStyle");
1176
+ const shape = parseFlagValue(rawArgs, "shape") || parseFlagValue(rawArgs, "marker-shape") || parseFlagValue(rawArgs, "markerShape") || parseFlagValue(rawArgs, "cursor-shape") || parseFlagValue(rawArgs, "cursorShape");
1177
+ const angleDeg = parseFlagValue(rawArgs, "angleDeg") || parseFlagValue(rawArgs, "angle-deg") || parseFlagValue(rawArgs, "rotationDeg") || parseFlagValue(rawArgs, "rotation-deg") || parseFlagValue(rawArgs, "rotation") || parseFlagValue(rawArgs, "angle");
1178
+ const size = parseFlagValue(rawArgs, "size") || parseFlagValue(rawArgs, "marker-size") || parseFlagValue(rawArgs, "markerSize") || parseFlagValue(rawArgs, "cursor-size") || parseFlagValue(rawArgs, "cursorSize");
1179
+ const color = parseFlagValue(rawArgs, "color");
1180
+ const durationMs = parseFlagValue(rawArgs, "durationMs") || parseFlagValue(rawArgs, "duration-ms");
1181
+ const typeIntervalMs = parseFlagValue(rawArgs, "typeIntervalMs")
1182
+ || parseFlagValue(rawArgs, "type-interval-ms")
1183
+ || parseFlagValue(rawArgs, "typingIntervalMs")
1184
+ || parseFlagValue(rawArgs, "typing-interval-ms");
1185
+ const label = parseFlagValue(rawArgs, "label");
1186
+ const caption = parseFlagValue(rawArgs, "caption")
1187
+ || parseFlagValue(rawArgs, "treatmentLabel")
1188
+ || parseFlagValue(rawArgs, "treatment-label")
1189
+ || parseFlagValue(rawArgs, "variant");
1190
+ const captionTitle = parseFlagValue(rawArgs, "captionTitle") || parseFlagValue(rawArgs, "caption-title");
1191
+ const captionBody = parseFlagValue(rawArgs, "captionBody")
1192
+ || parseFlagValue(rawArgs, "caption-body")
1193
+ || parseFlagValue(rawArgs, "captionDetail")
1194
+ || parseFlagValue(rawArgs, "caption-detail");
1195
+ const captionTags = parseFlagValue(rawArgs, "captionTags") || parseFlagValue(rawArgs, "caption-tags");
1196
+ const captionMode = parseFlagValue(rawArgs, "captionMode") || parseFlagValue(rawArgs, "caption-mode");
1197
+ const captionEyebrow = parseFlagValue(rawArgs, "captionEyebrow") || parseFlagValue(rawArgs, "caption-eyebrow");
1198
+ const captionLeadMs = parseFlagValue(rawArgs, "captionLeadMs") || parseFlagValue(rawArgs, "caption-lead-ms");
1199
+ const captionSound = parseFlagValue(rawArgs, "captionSound") || parseFlagValue(rawArgs, "caption-sound");
1200
+ const captionPlacement = parseFlagValue(rawArgs, "captionPlacement") || parseFlagValue(rawArgs, "caption-placement");
1201
+ const captionMargin = parseFlagValue(rawArgs, "captionMargin") || parseFlagValue(rawArgs, "caption-margin");
1202
+ const captionX = parseFlagValue(rawArgs, "captionX") || parseFlagValue(rawArgs, "caption-x");
1203
+ const captionY = parseFlagValue(rawArgs, "captionY") || parseFlagValue(rawArgs, "caption-y");
1204
+ const captionXRatio = parseFlagValue(rawArgs, "captionXRatio") || parseFlagValue(rawArgs, "caption-x-ratio") || parseFlagValue(rawArgs, "captionLeftRatio") || parseFlagValue(rawArgs, "caption-left-ratio");
1205
+ const captionYRatio = parseFlagValue(rawArgs, "captionYRatio") || parseFlagValue(rawArgs, "caption-y-ratio") || parseFlagValue(rawArgs, "captionTopRatio") || parseFlagValue(rawArgs, "caption-top-ratio");
1206
+ const sound = parseFlagValue(rawArgs, "sound") || parseFlagValue(rawArgs, "sfx");
1207
+ const trail = parseFlagValue(rawArgs, "trail") || parseFlagValue(rawArgs, "effect");
1208
+ const pathStyle = parseFlagValue(rawArgs, "pathStyle") || parseFlagValue(rawArgs, "path-style");
1209
+ const motion = parseFlagValue(rawArgs, "motion") || parseFlagValue(rawArgs, "easing") || parseFlagValue(rawArgs, "velocity");
1210
+ const trajectory = parseFlagValue(rawArgs, "trajectory") || parseFlagValue(rawArgs, "curve") || parseFlagValue(rawArgs, "arc");
1211
+ const glow = parseFlagValue(rawArgs, "glow") || parseFlagValue(rawArgs, "bloom");
1212
+ const idle = parseFlagValue(rawArgs, "idle") || parseFlagValue(rawArgs, "settle") || parseFlagValue(rawArgs, "presence");
1213
+ const edge = parseFlagValue(rawArgs, "edge") || parseFlagValue(rawArgs, "edgeEffect") || parseFlagValue(rawArgs, "edge-effect") || parseFlagValue(rawArgs, "arrival");
1214
+
1215
+ if (!app && !name && method === "computer.launchApp" && positional[0]) {
1216
+ params.app = positional[0];
1217
+ }
1218
+ if (magicScout && !app && !name) {
1219
+ params.app = "Scout";
1220
+ }
1221
+ if (!text && (method === "computer.typeWindowText" || method === "computer.demoScout" || method === "computer.magicCursor")) {
1222
+ const targetApp = String(params.app || app || name || "");
1223
+ const messageOffset = targetApp && positional[0] === targetApp ? 1 : 0;
1224
+ const positionalText = positional.slice(messageOffset).join(" ").trim();
1225
+ if (positionalText) text = positionalText;
1226
+ }
1227
+ if (method === "computer.click" && !x && !y && positional.length >= 2) {
1228
+ const px = Number(positional[0]);
1229
+ const py = Number(positional[1]);
1230
+ if (Number.isFinite(px) && Number.isFinite(py)) {
1231
+ params.x = px;
1232
+ params.y = py;
1233
+ }
1234
+ }
1235
+
1236
+ if (text) params.text = text;
1237
+ if (tty) params.tty = tty;
1238
+ if (app) params.app = app;
1239
+ if (name) params.name = name;
1240
+ if (bundleId) params.bundleId = bundleId;
1241
+ if (path) params.path = path;
1242
+ if (wid && Number.isFinite(Number(wid))) params.wid = Number(wid);
1243
+ if (terminalSessionId) params.terminalSessionId = terminalSessionId;
1244
+ if (session) params.session = session;
1245
+ if (title) params.title = title;
1246
+ if (treatment) params.treatment = treatment;
1247
+ if (transport) params.transport = transport;
1248
+ if (x && Number.isFinite(Number(x))) params.x = Number(x);
1249
+ if (y && Number.isFinite(Number(y))) params.y = Number(y);
1250
+ if (fromX && Number.isFinite(Number(fromX))) params.fromX = Number(fromX);
1251
+ if (fromY && Number.isFinite(Number(fromY))) params.fromY = Number(fromY);
1252
+ if (xRatio && Number.isFinite(Number(xRatio))) params.xRatio = Number(xRatio);
1253
+ if (yRatio && Number.isFinite(Number(yRatio))) params.yRatio = Number(yRatio);
1254
+ if (fromXRatio && Number.isFinite(Number(fromXRatio))) params.fromXRatio = Number(fromXRatio);
1255
+ if (fromYRatio && Number.isFinite(Number(fromYRatio))) params.fromYRatio = Number(fromYRatio);
1256
+ if (magicScout && params.xRatio === undefined) params.xRatio = 0.5;
1257
+ if (magicScout && params.yRatio === undefined) params.yRatio = 0.86;
1258
+ if (button) params.button = button;
1259
+ if (axLabel) params.axLabel = axLabel;
1260
+ if (appearance) params.appearance = appearance;
1261
+ if (shape) params.shape = shape;
1262
+ if (angleDeg && Number.isFinite(Number(angleDeg))) params.angleDeg = Number(angleDeg);
1263
+ if (size) params.size = size;
1264
+ if (color) params.color = color;
1265
+ if (durationMs && Number.isFinite(Number(durationMs))) params.durationMs = Number(durationMs);
1266
+ if (typeIntervalMs && Number.isFinite(Number(typeIntervalMs))) params.typeIntervalMs = Number(typeIntervalMs);
1267
+ if (label) params.label = label;
1268
+ if (caption) params.caption = caption;
1269
+ if (captionTitle) params.captionTitle = captionTitle;
1270
+ if (captionBody) params.captionBody = captionBody;
1271
+ if (captionTags) params.captionTags = captionTags;
1272
+ if (captionMode) params.captionMode = captionMode;
1273
+ if (captionEyebrow) params.captionEyebrow = captionEyebrow;
1274
+ if (captionLeadMs && Number.isFinite(Number(captionLeadMs))) params.captionLeadMs = Number(captionLeadMs);
1275
+ if (captionSound) params.captionSound = captionSound;
1276
+ if (captionPlacement) params.captionPlacement = captionPlacement;
1277
+ if (captionMargin && Number.isFinite(Number(captionMargin))) params.captionMargin = Number(captionMargin);
1278
+ if (captionX && Number.isFinite(Number(captionX))) params.captionX = Number(captionX);
1279
+ if (captionY && Number.isFinite(Number(captionY))) params.captionY = Number(captionY);
1280
+ if (captionXRatio && Number.isFinite(Number(captionXRatio))) params.captionXRatio = Number(captionXRatio);
1281
+ if (captionYRatio && Number.isFinite(Number(captionYRatio))) params.captionYRatio = Number(captionYRatio);
1282
+ if (sound) params.sound = sound;
1283
+ if (trail) params.trail = trail;
1284
+ if (pathStyle) params.pathStyle = pathStyle;
1285
+ if (motion) params.motion = motion;
1286
+ if (trajectory) params.trajectory = trajectory;
1287
+ if (glow) params.glow = glow;
1288
+ if (idle) params.idle = idle;
1289
+ if (edge) params.edge = edge;
1290
+ if (capture === "false" || capture === "0") params.capture = false;
1291
+ if (hasFlag(rawArgs, "no-capture") || hasFlag(rawArgs, "noCapture")) params.capture = false;
1292
+ if (hasFlag(rawArgs, "no-focus") || hasFlag(rawArgs, "noFocus") || hasFlag(rawArgs, "nofocus")) params.noFocus = true;
1293
+ if (hasFlag(rawArgs, "dry-run") || hasFlag(rawArgs, "dryRun")) params.dryRun = true;
1294
+ if (hasFlag(rawArgs, "enter")) params.enter = true;
1295
+ if (hasFlag(rawArgs, "send")) params.send = true;
1296
+ if (hasFlag(rawArgs, "append")) params.append = true;
1297
+ if (hasFlag(rawArgs, "show-caption") || hasFlag(rawArgs, "showCaption")) params.showCaption = true;
1298
+ if (hasFlag(rawArgs, "no-caption-selections") || hasFlag(rawArgs, "noCaptionSelections")) params.captionSelections = false;
1299
+ if (hasFlag(rawArgs, "typewriter") || hasFlag(rawArgs, "typing")) params.typewriter = true;
1300
+ if (hasFlag(rawArgs, "execute")) params.treatment = "execute";
1301
+ if (hasFlag(rawArgs, "present")) params.treatment = "present";
1302
+ if (hasFlag(rawArgs, "stage")) params.treatment = "stage";
1303
+ if (hasFlag(rawArgs, "observe")) params.treatment = "observe";
1304
+ if (hasFlag(rawArgs, "click")) params.click = true;
1305
+
1306
+ await withDaemon(async ({ daemonCall }) => {
1307
+ let result: any;
1308
+ if (method === "computer.click" || method === "computer.magicCursor") {
1309
+ const cua = await import("./cua.ts");
1310
+ result = method === "computer.click"
1311
+ ? await cua.click(params as any)
1312
+ : await cua.magicCursor(params as any);
1313
+ } else {
1314
+ result = await daemonCall(method, params, 30000) as any;
1315
+ }
1316
+ if (jsonFlag) {
1317
+ console.log(JSON.stringify(result, null, 2));
1318
+ return;
1319
+ }
1320
+
1321
+ const selected = result.selected || {};
1322
+ const terminal = selected.terminal || {};
1323
+ const target = result.target || terminal;
1324
+ const run = result.run || {};
1325
+ console.log(`${result.action || sub} ${result.treatment ? `(${result.treatment})` : ""}`);
1326
+ if (result.cursor) {
1327
+ console.log(" target: cursor");
1328
+ } else {
1329
+ console.log(` target: ${target.app || result.app || "terminal"} ${terminal.tty || ""}${target.windowId || target.wid ? ` wid:${target.windowId || target.wid}` : ""}`);
1330
+ }
1331
+ if (result.cursor) console.log(` cursor: (${Math.round(result.cursor.x)}, ${Math.round(result.cursor.y)})`);
1332
+ if (result.from) console.log(` from: (${Math.round(result.from.x)}, ${Math.round(result.from.y)})`);
1333
+ console.log(` run: ${run.id || "?"}`);
1334
+ if (typeof result.launched === "boolean") console.log(` launched: ${result.launched}`);
1335
+ if (typeof result.focused === "boolean") console.log(` focused: ${result.focused}`);
1336
+ if (typeof result.clicked === "boolean") console.log(` clicked: ${result.clicked}`);
1337
+ if (typeof result.shown === "boolean") console.log(` shown: ${result.shown}`);
1338
+ if (result.button) console.log(` button: ${result.button}`);
1339
+ if (result.appearance?.style) console.log(` appearance: ${result.appearance.style}${result.appearance.color ? ` ${result.appearance.color}` : ""}${result.appearance.shape ? ` shape:${result.appearance.shape}` : ""}${result.appearance.angleDeg !== undefined ? ` angle:${result.appearance.angleDeg}` : ""}${result.appearance.size ? ` size:${result.appearance.size}` : ""}`);
1340
+ if (result.typedText !== undefined) console.log(` typed: ${result.dryRun ? "dry run" : JSON.stringify(result.typedText || "")}`);
1341
+ if (result.transport) console.log(` transport: ${result.transport}`);
1342
+ if (result.beforeArtifact?.path) console.log(` before: ${result.beforeArtifact.path}`);
1343
+ if (result.afterArtifact?.path) console.log(` after: ${result.afterArtifact.path}`);
1344
+ });
1268
1345
  }
1269
1346
 
1270
1347
  async function voiceCommand(subcommand?: string, ...rest: string[]): Promise<void> {
1271
- const { daemonCall } = await getDaemonClient();
1272
- try {
1348
+ if (subcommand !== "status" && subcommand !== "simulate" && subcommand !== "sim" && subcommand !== "intents") {
1349
+ console.log("Usage: lattices voice <subcommand>\n");
1350
+ console.log(" status Show voice provider status");
1351
+ console.log(" simulate Parse and execute a voice command");
1352
+ console.log(" intents List all available intents");
1353
+ console.log("\nExamples:");
1354
+ console.log(' lattices voice simulate "tile this left"');
1355
+ console.log(' lattices voice simulate "focus chrome" --dry-run');
1356
+ return;
1357
+ }
1358
+
1359
+ if (subcommand === "simulate" || subcommand === "sim") {
1360
+ const text = rest.join(" ");
1361
+ if (!text) {
1362
+ console.log("Usage: lattices voice simulate <text>");
1363
+ return;
1364
+ }
1365
+ }
1366
+
1367
+ await withDaemon(async ({ daemonCall }) => {
1273
1368
  switch (subcommand) {
1274
- case "status": {
1275
- const status = await daemonCall("voice.status") as any;
1276
- console.log(`Provider: ${status.provider}`);
1277
- console.log(`Available: ${status.available}`);
1278
- console.log(`Listening: ${status.listening}`);
1279
- if (status.lastTranscript) console.log(`Last: "${status.lastTranscript}"`);
1280
- break;
1369
+ case "status": {
1370
+ const status = await daemonCall("voice.status") as any;
1371
+ console.log(`Provider: ${status.provider}`);
1372
+ console.log(`Available: ${status.available}`);
1373
+ console.log(`Listening: ${status.listening}`);
1374
+ if (status.lastTranscript) console.log(`Last: "${status.lastTranscript}"`);
1375
+ break;
1376
+ }
1377
+ case "simulate":
1378
+ case "sim": {
1379
+ const text = rest.join(" ");
1380
+ const execute = !rest.includes("--dry-run");
1381
+ const dryFlag = rest.includes("--dry-run");
1382
+ const cleanText = dryFlag ? rest.filter(r => r !== "--dry-run").join(" ") : text;
1383
+ const result = await daemonCall("voice.simulate", { text: cleanText, execute }, 15000) as any;
1384
+ if (!result.parsed) {
1385
+ console.log(`\x1b[33mNo match:\x1b[0m "${cleanText}"`);
1386
+ return;
1281
1387
  }
1282
- case "simulate":
1283
- case "sim": {
1284
- const text = rest.join(" ");
1285
- if (!text) {
1286
- console.log("Usage: lattices voice simulate <text>");
1287
- return;
1288
- }
1289
- const execute = !rest.includes("--dry-run");
1290
- const dryFlag = rest.includes("--dry-run");
1291
- const cleanText = dryFlag ? rest.filter(r => r !== "--dry-run").join(" ") : text;
1292
- const result = await daemonCall("voice.simulate", { text: cleanText, execute }, 15000) as any;
1293
- if (!result.parsed) {
1294
- console.log(`\x1b[33mNo match:\x1b[0m "${cleanText}"`);
1295
- return;
1296
- }
1297
- const slots = Object.entries(result.slots || {}).map(([k,v]) => `${k}: ${v}`).join(", ");
1298
- const conf = result.confidence ? ` (${(result.confidence * 100).toFixed(0)}%)` : "";
1299
- console.log(`\x1b[36m${result.intent}\x1b[0m${slots ? ` ${slots}` : ""}${conf}`);
1300
- if (result.executed) {
1301
- console.log(`\x1b[32mExecuted\x1b[0m`);
1302
- } else if (result.error) {
1303
- console.log(`\x1b[31mError:\x1b[0m ${result.error}`);
1304
- }
1305
- break;
1388
+ const slots = Object.entries(result.slots || {}).map(([k,v]) => `${k}: ${v}`).join(", ");
1389
+ const conf = result.confidence ? ` (${(result.confidence * 100).toFixed(0)}%)` : "";
1390
+ console.log(`\x1b[36m${result.intent}\x1b[0m${slots ? ` ${slots}` : ""}${conf}`);
1391
+ if (result.executed) {
1392
+ console.log(`\x1b[32mExecuted\x1b[0m`);
1393
+ } else if (result.error) {
1394
+ console.log(`\x1b[31mError:\x1b[0m ${result.error}`);
1306
1395
  }
1307
- case "intents": {
1308
- const intents = await daemonCall("intents.list") as any[];
1309
- for (const intent of intents) {
1310
- const slots = intent.slots.map((s: any) => `${s.name}:${s.type}${s.required ? "*" : ""}`).join(", ");
1311
- console.log(` \x1b[1m${intent.intent}\x1b[0m ${intent.description}`);
1312
- if (slots) console.log(` slots: ${slots}`);
1313
- console.log(` e.g. "${intent.examples[0]}"`);
1314
- console.log();
1315
- }
1316
- break;
1396
+ break;
1397
+ }
1398
+ case "intents": {
1399
+ const intents = await daemonCall("intents.list") as any[];
1400
+ for (const intent of intents) {
1401
+ const slots = intent.slots.map((s: any) => `${s.name}:${s.type}${s.required ? "*" : ""}`).join(", ");
1402
+ console.log(` \x1b[1m${intent.intent}\x1b[0m ${intent.description}`);
1403
+ if (slots) console.log(` slots: ${slots}`);
1404
+ console.log(` e.g. "${intent.examples[0]}"`);
1405
+ console.log();
1317
1406
  }
1318
- default:
1319
- console.log("Usage: lattices voice <subcommand>\n");
1320
- console.log(" status Show voice provider status");
1321
- console.log(" simulate Parse and execute a voice command");
1322
- console.log(" intents List all available intents");
1323
- console.log("\nExamples:");
1324
- console.log(' lattices voice simulate "tile this left"');
1325
- console.log(' lattices voice simulate "focus chrome" --dry-run');
1407
+ break;
1326
1408
  }
1327
- } catch (e: unknown) {
1328
- console.log(`Error: ${(e as Error).message}`);
1329
- }
1409
+ }
1410
+ });
1330
1411
  }
1331
1412
 
1332
1413
  async function assistantCommand(subcommand?: string, ...rest: string[]): Promise<void> {
@@ -1366,14 +1447,11 @@ async function callCommand(method?: string, ...rest: string[]): Promise<void> {
1366
1447
  console.log(' lattices call window.place \'{"session":"vox","placement":"left"}\'');
1367
1448
  return;
1368
1449
  }
1369
- try {
1370
- const { daemonCall } = await getDaemonClient();
1450
+ await withDaemon(async ({ daemonCall }) => {
1371
1451
  const params = rest[0] ? JSON.parse(rest[0]) : null;
1372
1452
  const result = await daemonCall(method, params, 15000);
1373
1453
  console.log(JSON.stringify(result, null, 2));
1374
- } catch (e: unknown) {
1375
- console.log(`Error: ${(e as Error).message}`);
1376
- }
1454
+ });
1377
1455
  }
1378
1456
 
1379
1457
  interface AppActorAsset {
@@ -1566,40 +1644,42 @@ async function actorHUDCommand(rest: string[]): Promise<void> {
1566
1644
  return;
1567
1645
  }
1568
1646
 
1569
- const { daemonCall } = await getDaemonClient();
1570
1647
  const url = positional[1];
1571
1648
  const clear = hasFlag(rest, "clear");
1572
- const result = await daemonCall("overlay.actor.hud", {
1573
- id,
1574
- clear,
1575
- ...(url && !clear ? { hudUrl: url } : {}),
1576
- ...actorHUDOptions(rest),
1577
- }, 15000) as any;
1649
+ await withDaemon(async ({ daemonCall }) => {
1650
+ const result = await daemonCall("overlay.actor.hud", {
1651
+ id,
1652
+ clear,
1653
+ ...(url && !clear ? { hudUrl: url } : {}),
1654
+ ...actorHUDOptions(rest),
1655
+ }, 15000) as any;
1578
1656
 
1579
- if (hasFlag(rest, "json")) {
1580
- console.log(JSON.stringify(result, null, 2));
1581
- } else if (clear) {
1582
- console.log(`Cleared HUD for ${id}.`);
1583
- } else {
1584
- console.log(`Attached hover HUD to ${id}.`);
1585
- }
1657
+ if (hasFlag(rest, "json")) {
1658
+ console.log(JSON.stringify(result, null, 2));
1659
+ } else if (clear) {
1660
+ console.log(`Cleared HUD for ${id}.`);
1661
+ } else {
1662
+ console.log(`Attached hover HUD to ${id}.`);
1663
+ }
1664
+ });
1586
1665
  }
1587
1666
 
1588
1667
  async function actorVisibilityCommand(action: string, rest: string[]): Promise<void> {
1589
- const { daemonCall } = await getDaemonClient();
1590
- const result = await daemonCall("overlay.actor.visibility", {
1591
- action,
1592
- feedback: !hasFlag(rest, "quiet") && action !== "status",
1593
- }, 15000) as any;
1668
+ await withDaemon(async ({ daemonCall }) => {
1669
+ const result = await daemonCall("overlay.actor.visibility", {
1670
+ action,
1671
+ feedback: !hasFlag(rest, "quiet") && action !== "status",
1672
+ }, 15000) as any;
1594
1673
 
1595
- if (hasFlag(rest, "json")) {
1596
- console.log(JSON.stringify(result, null, 2));
1597
- return;
1598
- }
1674
+ if (hasFlag(rest, "json")) {
1675
+ console.log(JSON.stringify(result, null, 2));
1676
+ return;
1677
+ }
1599
1678
 
1600
- const state = result.visible ? "shown" : "hidden";
1601
- const count = Number(result.actorCount ?? 0);
1602
- console.log(`Actor layer ${state} (${count} actor${count === 1 ? "" : "s"}).`);
1679
+ const state = result.visible ? "shown" : "hidden";
1680
+ const count = Number(result.actorCount ?? 0);
1681
+ console.log(`Actor layer ${state} (${count} actor${count === 1 ? "" : "s"}).`);
1682
+ });
1603
1683
  }
1604
1684
 
1605
1685
  async function actorAppCommand(rest: string[]): Promise<void> {
@@ -1610,86 +1690,28 @@ async function actorAppCommand(rest: string[]): Promise<void> {
1610
1690
  return;
1611
1691
  }
1612
1692
  const message = positional.slice(1).join(" ") || `Tap to switch to ${appQuery}.`;
1613
- const asset = ensureAppActorAsset(appQuery);
1614
- const { daemonCall } = await getDaemonClient();
1615
- const id = parseFlagValue(rest, "id") || `app-${slugify(asset.appName)}`;
1616
- const state = parseFlagValue(rest, "state") || "idle";
1617
- const ttlMs = Number(parseFlagValue(rest, "ttl") || parseFlagValue(rest, "ttlMs") || 0);
1618
- const x = Number(parseFlagValue(rest, "x") || 520);
1619
- const y = Number(parseFlagValue(rest, "y") || 340);
1620
- const placement = parseFlagValue(rest, "placement") || "point";
1621
- const style = parseFlagValue(rest, "style") || "playful";
1622
- const dismissible = hasFlag(rest, "dismissible");
1623
- const labelHidden = shouldHideActorLabel(rest);
1624
- const closeOnActivate = hasFlag(rest, "close-on-activate") || hasFlag(rest, "closeOnActivate");
1625
- const scale = Number(parseFlagValue(rest, "scale") || 1);
1626
-
1627
- const result = await daemonCall("overlay.actor.publish", {
1628
- id,
1629
- renderer: "sprite",
1630
- asset: asset.id,
1631
- state,
1632
- name: parseFlagValue(rest, "name") || asset.appName,
1633
- message,
1634
- placement,
1635
- x,
1636
- y,
1637
- style,
1638
- ttlMs,
1639
- dismissible,
1640
- labelHidden,
1641
- closeOnActivate,
1642
- scale,
1643
- ...actorHUDOptions(rest),
1644
- targetApp: asset.appName,
1645
- targetBundleId: asset.bundleIdentifier,
1646
- targetAppPath: asset.appPath,
1647
- }, 15000) as any;
1648
-
1649
- if (!hasFlag(rest, "no-move")) {
1650
- await daemonCall("overlay.actor.moveTo", {
1651
- id,
1652
- x: x + 40,
1653
- y: y + 50,
1654
- durationMs: 700,
1655
- easing: "spring",
1656
- }, 15000);
1657
- }
1693
+ await withDaemon(async ({ daemonCall }) => {
1694
+ const asset = ensureAppActorAsset(appQuery);
1695
+ const id = parseFlagValue(rest, "id") || `app-${slugify(asset.appName)}`;
1696
+ const state = parseFlagValue(rest, "state") || "idle";
1697
+ const ttlMs = Number(parseFlagValue(rest, "ttl") || parseFlagValue(rest, "ttlMs") || 0);
1698
+ const x = Number(parseFlagValue(rest, "x") || 520);
1699
+ const y = Number(parseFlagValue(rest, "y") || 340);
1700
+ const placement = parseFlagValue(rest, "placement") || "point";
1701
+ const style = parseFlagValue(rest, "style") || "playful";
1702
+ const dismissible = hasFlag(rest, "dismissible");
1703
+ const labelHidden = shouldHideActorLabel(rest);
1704
+ const closeOnActivate = hasFlag(rest, "close-on-activate") || hasFlag(rest, "closeOnActivate");
1705
+ const scale = Number(parseFlagValue(rest, "scale") || 1);
1658
1706
 
1659
- if (hasFlag(rest, "json")) {
1660
- console.log(JSON.stringify({ ...result, asset: asset.id, appPath: asset.appPath }, null, 2));
1661
- } else {
1662
- console.log(`Published ${asset.appName} actor (${id}). Click it to switch to ${asset.appName}.`);
1663
- }
1664
- }
1665
-
1666
- async function actorSwitcherCommand(rest: string[]): Promise<void> {
1667
- const appNames = nonFlagArgs(rest);
1668
- const apps = appNames.length ? appNames : ["Codex", "Talkie"];
1669
- const { daemonCall } = await getDaemonClient();
1670
- const startX = Number(parseFlagValue(rest, "x") || 420);
1671
- const y = Number(parseFlagValue(rest, "y") || 220);
1672
- const gap = Number(parseFlagValue(rest, "gap") || 270);
1673
- const ttlMs = Number(parseFlagValue(rest, "ttl") || parseFlagValue(rest, "ttlMs") || 0);
1674
- const style = parseFlagValue(rest, "style") || "info";
1675
- const dismissible = hasFlag(rest, "dismissible");
1676
- const labelHidden = shouldHideActorLabel(rest);
1677
- const closeOnActivate = hasFlag(rest, "close-on-activate") || hasFlag(rest, "closeOnActivate");
1678
- const scale = Number(parseFlagValue(rest, "scale") || 1);
1679
- const results: any[] = [];
1680
-
1681
- for (let i = 0; i < apps.length; i++) {
1682
- const asset = ensureAppActorAsset(apps[i]);
1683
- const id = `switch-${slugify(asset.appName)}`;
1684
- const x = startX + i * gap;
1685
1707
  const result = await daemonCall("overlay.actor.publish", {
1686
1708
  id,
1687
1709
  renderer: "sprite",
1688
1710
  asset: asset.id,
1689
- state: "ready",
1690
- name: asset.appName,
1691
- message: `Tap to switch to ${asset.appName}.`,
1692
- placement: "point",
1711
+ state,
1712
+ name: parseFlagValue(rest, "name") || asset.appName,
1713
+ message,
1714
+ placement,
1693
1715
  x,
1694
1716
  y,
1695
1717
  style,
@@ -1703,21 +1725,81 @@ async function actorSwitcherCommand(rest: string[]): Promise<void> {
1703
1725
  targetBundleId: asset.bundleIdentifier,
1704
1726
  targetAppPath: asset.appPath,
1705
1727
  }, 15000) as any;
1706
- results.push({ ...result, asset: asset.id, appPath: asset.appPath });
1707
- await daemonCall("overlay.actor.moveTo", {
1708
- id,
1709
- x: x + 28,
1710
- y: y + 36,
1711
- durationMs: 650,
1712
- easing: "spring",
1713
- }, 15000);
1714
- }
1715
1728
 
1716
- if (hasFlag(rest, "json")) {
1717
- console.log(JSON.stringify(results, null, 2));
1718
- } else {
1719
- console.log(`Published app switcher for ${apps.join(", ")}.`);
1720
- }
1729
+ if (!hasFlag(rest, "no-move")) {
1730
+ await daemonCall("overlay.actor.moveTo", {
1731
+ id,
1732
+ x: x + 40,
1733
+ y: y + 50,
1734
+ durationMs: 700,
1735
+ easing: "spring",
1736
+ }, 15000);
1737
+ }
1738
+
1739
+ if (hasFlag(rest, "json")) {
1740
+ console.log(JSON.stringify({ ...result, asset: asset.id, appPath: asset.appPath }, null, 2));
1741
+ } else {
1742
+ console.log(`Published ${asset.appName} actor (${id}). Click it to switch to ${asset.appName}.`);
1743
+ }
1744
+ });
1745
+ }
1746
+
1747
+ async function actorSwitcherCommand(rest: string[]): Promise<void> {
1748
+ const appNames = nonFlagArgs(rest);
1749
+ const apps = appNames.length ? appNames : ["Codex", "Talkie"];
1750
+ await withDaemon(async ({ daemonCall }) => {
1751
+ const startX = Number(parseFlagValue(rest, "x") || 420);
1752
+ const y = Number(parseFlagValue(rest, "y") || 220);
1753
+ const gap = Number(parseFlagValue(rest, "gap") || 270);
1754
+ const ttlMs = Number(parseFlagValue(rest, "ttl") || parseFlagValue(rest, "ttlMs") || 0);
1755
+ const style = parseFlagValue(rest, "style") || "info";
1756
+ const dismissible = hasFlag(rest, "dismissible");
1757
+ const labelHidden = shouldHideActorLabel(rest);
1758
+ const closeOnActivate = hasFlag(rest, "close-on-activate") || hasFlag(rest, "closeOnActivate");
1759
+ const scale = Number(parseFlagValue(rest, "scale") || 1);
1760
+ const results: any[] = [];
1761
+
1762
+ for (let i = 0; i < apps.length; i++) {
1763
+ const asset = ensureAppActorAsset(apps[i]);
1764
+ const id = `switch-${slugify(asset.appName)}`;
1765
+ const x = startX + i * gap;
1766
+ const result = await daemonCall("overlay.actor.publish", {
1767
+ id,
1768
+ renderer: "sprite",
1769
+ asset: asset.id,
1770
+ state: "ready",
1771
+ name: asset.appName,
1772
+ message: `Tap to switch to ${asset.appName}.`,
1773
+ placement: "point",
1774
+ x,
1775
+ y,
1776
+ style,
1777
+ ttlMs,
1778
+ dismissible,
1779
+ labelHidden,
1780
+ closeOnActivate,
1781
+ scale,
1782
+ ...actorHUDOptions(rest),
1783
+ targetApp: asset.appName,
1784
+ targetBundleId: asset.bundleIdentifier,
1785
+ targetAppPath: asset.appPath,
1786
+ }, 15000) as any;
1787
+ results.push({ ...result, asset: asset.id, appPath: asset.appPath });
1788
+ await daemonCall("overlay.actor.moveTo", {
1789
+ id,
1790
+ x: x + 28,
1791
+ y: y + 36,
1792
+ durationMs: 650,
1793
+ easing: "spring",
1794
+ }, 15000);
1795
+ }
1796
+
1797
+ if (hasFlag(rest, "json")) {
1798
+ console.log(JSON.stringify(results, null, 2));
1799
+ } else {
1800
+ console.log(`Published app switcher for ${apps.join(", ")}.`);
1801
+ }
1802
+ });
1721
1803
  }
1722
1804
 
1723
1805
  type HUDPathField = string | {
@@ -2022,20 +2104,21 @@ function upsertHUDRegistryEntry(resolved: ResolvedHUDManifest, published = false
2022
2104
  }
2023
2105
 
2024
2106
  async function publishHUDManifest(resolved: ResolvedHUDManifest, rest: string[], index = 0): Promise<Record<string, unknown>> {
2025
- const { daemonCall } = await getDaemonClient();
2026
- const payload = hudPublishPayload(resolved, rest, index);
2027
- const result = await daemonCall("overlay.actor.publish", payload, 15000) as Record<string, unknown>;
2028
- if (!hasFlag(rest, "no-move")) {
2029
- await daemonCall("overlay.actor.moveTo", {
2030
- id: resolved.id,
2031
- x: Number(payload.x) + 24,
2032
- y: Number(payload.y) + 30,
2033
- durationMs: 600,
2034
- easing: "spring",
2035
- }, 15000);
2036
- }
2037
- upsertHUDRegistryEntry(resolved, true);
2038
- return result;
2107
+ return withDaemon(async ({ daemonCall }) => {
2108
+ const payload = hudPublishPayload(resolved, rest, index);
2109
+ const result = await daemonCall("overlay.actor.publish", payload, 15000) as Record<string, unknown>;
2110
+ if (!hasFlag(rest, "no-move")) {
2111
+ await daemonCall("overlay.actor.moveTo", {
2112
+ id: resolved.id,
2113
+ x: Number(payload.x) + 24,
2114
+ y: Number(payload.y) + 30,
2115
+ durationMs: 600,
2116
+ easing: "spring",
2117
+ }, 15000);
2118
+ }
2119
+ upsertHUDRegistryEntry(resolved, true);
2120
+ return result;
2121
+ });
2039
2122
  }
2040
2123
 
2041
2124
  async function hudRegisterCommand(rest: string[]): Promise<void> {
@@ -2157,190 +2240,8 @@ async function hudCommand(sub?: string, ...rest: string[]): Promise<void> {
2157
2240
  }
2158
2241
  }
2159
2242
 
2160
- async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
2161
- try {
2162
- const { daemonCall } = await getDaemonClient();
2163
-
2164
- // ── Subcommands ──
2165
- if (sub === "create") {
2166
- await layerCreateCommand(rest);
2167
- return;
2168
- }
2169
- if (sub === "snap") {
2170
- await layerSnapCommand(rest[0]);
2171
- return;
2172
- }
2173
- if (sub === "session" || sub === "sessions") {
2174
- await layerSessionCommand(rest[0]);
2175
- return;
2176
- }
2177
- if (sub === "clear") {
2178
- await daemonCall("session.layers.clear");
2179
- console.log("Cleared all session layers.");
2180
- return;
2181
- }
2182
- if (sub === "delete" || sub === "rm") {
2183
- if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
2184
- await daemonCall("session.layers.delete", { name: rest[0] });
2185
- console.log(`Deleted session layer "${rest[0]}".`);
2186
- return;
2187
- }
2188
-
2189
- // ── List or switch (original behavior) ──
2190
- if (sub === undefined || sub === null || sub === "") {
2191
- const result = await daemonCall("layers.list") as any;
2192
- if (!result.layers.length) {
2193
- console.log("No layers configured.");
2194
- return;
2195
- }
2196
- console.log("Layers:\n");
2197
- for (const layer of result.layers) {
2198
- const active = layer.index === result.active ? " \x1b[32m● active\x1b[0m" : "";
2199
- console.log(` [${layer.index}] ${layer.label} (${layer.projectCount} projects)${active}`);
2200
- }
2201
- return;
2202
- }
2203
- const idx = parseInt(sub, 10);
2204
- if (!isNaN(idx)) {
2205
- await daemonCall("layer.activate", { index: idx, mode: "launch" });
2206
- console.log(`Activated layer ${idx}`);
2207
- } else {
2208
- await daemonCall("layer.activate", { name: sub, mode: "launch" });
2209
- console.log(`Activated layer "${sub}"`);
2210
- }
2211
- } catch (e: unknown) {
2212
- console.log(`Error: ${(e as Error).message}`);
2213
- }
2214
- }
2215
-
2216
- // ── Layer create: build a session layer from window specs ────────────
2217
- // Usage: lattices layer create <name> [wid:123 wid:456 ...]
2218
- // lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
2219
- async function layerCreateCommand(args: string[]): Promise<void> {
2220
- const { daemonCall } = await getDaemonClient();
2221
- const name = args[0];
2222
- if (!name) {
2223
- console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
2224
- return;
2225
- }
2226
-
2227
- const jsonIdx = args.indexOf("--json");
2228
- if (jsonIdx !== -1 && args[jsonIdx + 1]) {
2229
- // JSON mode: parse window specs with tile positions
2230
- const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
2231
- wid?: number; app?: string; title?: string; tile?: string;
2232
- }>;
2233
-
2234
- // Collect wids, resolve app-based specs
2235
- const windowIds: number[] = [];
2236
- const windows: Array<{ app: string; contentHint?: string }> = [];
2237
- const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
2238
-
2239
- for (const spec of specs) {
2240
- if (spec.wid) {
2241
- windowIds.push(spec.wid);
2242
- if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
2243
- } else if (spec.app) {
2244
- windows.push({ app: spec.app, contentHint: spec.title });
2245
- if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
2246
- }
2247
- }
2248
-
2249
- const result = await daemonCall("session.layers.create", {
2250
- name,
2251
- ...(windowIds.length ? { windowIds } : {}),
2252
- ...(windows.length ? { windows } : {}),
2253
- }) as any;
2254
-
2255
- console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
2256
-
2257
- // Apply tile positions
2258
- for (const t of tiles) {
2259
- try {
2260
- await daemonCall("window.place", {
2261
- ...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
2262
- placement: t.tile,
2263
- });
2264
- } catch { /* window may not be resolved yet */ }
2265
- }
2266
-
2267
- if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
2268
- return;
2269
- }
2270
-
2271
- // Simple wid mode: lattices layer create <name> wid:123 wid:456
2272
- const wids = args.slice(1)
2273
- .filter(a => a.startsWith("wid:"))
2274
- .map(a => parseInt(a.slice(4), 10))
2275
- .filter(n => !isNaN(n));
2276
-
2277
- const result = await daemonCall("session.layers.create", {
2278
- name,
2279
- ...(wids.length ? { windowIds: wids } : {}),
2280
- }) as any;
2281
-
2282
- console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
2283
- }
2284
-
2285
- // ── Layer snap: snapshot current visible windows into a session layer ─
2286
- async function layerSnapCommand(name?: string): Promise<void> {
2287
- const { daemonCall } = await getDaemonClient();
2288
- const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
2289
-
2290
- // Get all current windows
2291
- const windows = await daemonCall("windows.list") as any[];
2292
- const visibleWids = windows
2293
- .filter((w: any) => !w.isMinimized && w.app !== "lattices")
2294
- .map((w: any) => w.wid);
2295
-
2296
- if (!visibleWids.length) {
2297
- console.log("No visible windows to snapshot.");
2298
- return;
2299
- }
2300
-
2301
- await daemonCall("session.layers.create", {
2302
- name: layerName,
2303
- windowIds: visibleWids,
2304
- });
2305
-
2306
- console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
2307
- }
2308
-
2309
- // ── Layer session: list or switch session layers ─────────────────────
2310
- async function layerSessionCommand(nameOrIndex?: string): Promise<void> {
2311
- const { daemonCall } = await getDaemonClient();
2312
- const result = await daemonCall("session.layers.list") as any;
2313
-
2314
- if (!nameOrIndex) {
2315
- // List session layers
2316
- if (!result.layers.length) {
2317
- console.log("No session layers. Create one with: lattices layer create <name>");
2318
- return;
2319
- }
2320
- console.log("Session layers:\n");
2321
- for (let i = 0; i < result.layers.length; i++) {
2322
- const l = result.layers[i];
2323
- const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
2324
- const winCount = l.windows?.length || 0;
2325
- console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
2326
- }
2327
- return;
2328
- }
2329
-
2330
- // Switch by index or name
2331
- const idx = parseInt(nameOrIndex, 10);
2332
- if (!isNaN(idx)) {
2333
- await daemonCall("session.layers.switch", { index: idx });
2334
- console.log(`Switched to session layer ${idx}.`);
2335
- } else {
2336
- await daemonCall("session.layers.switch", { name: nameOrIndex });
2337
- console.log(`Switched to session layer "${nameOrIndex}".`);
2338
- }
2339
- }
2340
-
2341
2243
  async function diagCommand(limit?: string): Promise<void> {
2342
- try {
2343
- const { daemonCall } = await getDaemonClient();
2244
+ await withDaemon(async ({ daemonCall }) => {
2344
2245
  const result = await daemonCall("diagnostics.list", { limit: parseInt(limit || "", 10) || 40 }) as any;
2345
2246
  if (!result.entries || !result.entries.length) {
2346
2247
  console.log("No diagnostic entries.");
@@ -2352,9 +2253,7 @@ async function diagCommand(limit?: string): Promise<void> {
2352
2253
  entry.level === "error" ? "\x1b[31m✗\x1b[0m" : "›";
2353
2254
  console.log(` \x1b[90m${entry.time}\x1b[0m ${icon} ${entry.message}`);
2354
2255
  }
2355
- } catch (e: unknown) {
2356
- console.log(`Error: ${(e as Error).message}`);
2357
- }
2256
+ });
2358
2257
  }
2359
2258
 
2360
2259
  async function distributeCommand(rawArgs: string[] = []): Promise<void> {
@@ -2373,7 +2272,7 @@ async function daemonLsCommand(): Promise<boolean> {
2373
2272
  if (!(await isDaemonRunning())) return false;
2374
2273
  const sessions = await daemonCall("tmux.sessions") as any[];
2375
2274
  if (!sessions.length) {
2376
- console.log("No active tmux sessions.");
2275
+ console.log("No active sessions.");
2377
2276
  return true;
2378
2277
  }
2379
2278
 
@@ -2474,12 +2373,10 @@ async function daemonStatusInventory(): Promise<boolean> {
2474
2373
  // ── OCR commands ──────────────────────────────────────────────────────
2475
2374
 
2476
2375
  async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2477
- const { daemonCall } = await getDaemonClient();
2478
-
2479
2376
  if (!sub || sub === "snapshot" || sub === "ls" || sub === "--full" || sub === "-f" || sub === "--json") {
2480
2377
  const full = sub === "--full" || sub === "-f" || rest.includes("--full") || rest.includes("-f");
2481
2378
  const json = sub === "--json" || rest.includes("--json");
2482
- try {
2379
+ await withDaemon(async ({ daemonCall }) => {
2483
2380
  const results = await daemonCall("ocr.snapshot", null, 5000) as any[];
2484
2381
  if (!results.length) {
2485
2382
  console.log("No scan results yet. The first scan runs ~60s after launch.");
@@ -2517,9 +2414,7 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2517
2414
  }
2518
2415
  console.log();
2519
2416
  }
2520
- } catch {
2521
- console.log("Daemon not running. Start with: lattices app");
2522
- }
2417
+ });
2523
2418
  return;
2524
2419
  }
2525
2420
 
@@ -2529,7 +2424,7 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2529
2424
  console.log("Usage: lattices scan search <query>");
2530
2425
  return;
2531
2426
  }
2532
- try {
2427
+ await withDaemon(async ({ daemonCall }) => {
2533
2428
  const results = await daemonCall("ocr.search", { query }, 5000) as any[];
2534
2429
  if (!results.length) {
2535
2430
  console.log(`No matches for "${query}".`);
@@ -2544,9 +2439,7 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2544
2439
  console.log(` ${snippet}`);
2545
2440
  console.log();
2546
2441
  }
2547
- } catch (e: unknown) {
2548
- console.log(`Error: ${(e as Error).message}`);
2549
- }
2442
+ });
2550
2443
  return;
2551
2444
  }
2552
2445
 
@@ -2554,7 +2447,7 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2554
2447
  const full = rest.includes("--full") || rest.includes("-f");
2555
2448
  const numArg = rest.find(a => !a.startsWith("-"));
2556
2449
  const limit = parseInt(numArg || "", 10) || 20;
2557
- try {
2450
+ await withDaemon(async ({ daemonCall }) => {
2558
2451
  const results = await daemonCall("ocr.recent", { limit }, 5000) as any[];
2559
2452
  if (!results.length) {
2560
2453
  console.log("No history yet. The first scan runs ~60s after launch.");
@@ -2583,20 +2476,16 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2583
2476
  }
2584
2477
  console.log();
2585
2478
  }
2586
- } catch {
2587
- console.log("Daemon not running. Start with: lattices app");
2588
- }
2479
+ });
2589
2480
  return;
2590
2481
  }
2591
2482
 
2592
2483
  if (sub === "deep" || sub === "now" || sub === "scan") {
2593
- try {
2484
+ await withDaemon(async ({ daemonCall }) => {
2594
2485
  console.log("Triggering deep scan (Vision OCR)...");
2595
2486
  await daemonCall("ocr.scan", null, 30000);
2596
2487
  console.log("Done.");
2597
- } catch (e: unknown) {
2598
- console.log(`Error: ${(e as Error).message}`);
2599
- }
2488
+ });
2600
2489
  return;
2601
2490
  }
2602
2491
 
@@ -2606,7 +2495,7 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2606
2495
  console.log("Usage: lattices scan history <wid>");
2607
2496
  return;
2608
2497
  }
2609
- try {
2498
+ await withDaemon(async ({ daemonCall }) => {
2610
2499
  const results = await daemonCall("ocr.history", { wid }, 5000) as any[];
2611
2500
  if (!results.length) {
2612
2501
  console.log(`No history for wid:${wid}.`);
@@ -2624,9 +2513,7 @@ async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
2624
2513
  }
2625
2514
  console.log();
2626
2515
  }
2627
- } catch (e: unknown) {
2628
- console.log(`Error: ${(e as Error).message}`);
2629
- }
2516
+ });
2630
2517
  return;
2631
2518
  }
2632
2519
 
@@ -2645,14 +2532,13 @@ Usage:
2645
2532
  }
2646
2533
 
2647
2534
  function printUsage(): void {
2648
- console.log(`lattices — workspace launcher for tmux, windows, layers, and the menu bar app
2535
+ console.log(`lattices — workspace launcher for sessions, windows, layers, and the menu bar app
2649
2536
 
2650
2537
  Usage:
2651
2538
  lattices Show workspace status and common commands
2652
- lattices start Start or reattach the current directory's tmux workspace
2653
- lattices tmux Alias for lattices start
2539
+ lattices start Start or reattach the current directory's workspace
2654
2540
  lattices init Generate .lattices.json config for this project
2655
- lattices ls List active tmux sessions
2541
+ lattices ls List active sessions
2656
2542
  lattices status Show managed vs unmanaged session inventory
2657
2543
  lattices kill [name] Kill a session (defaults to current project)
2658
2544
  lattices sync Reconcile session to match declared config
@@ -2667,7 +2553,25 @@ Usage:
2667
2553
  lattices place <query> [pos] Deep search + focus + tile (default: bottom-right)
2668
2554
  lattices focus <session> Raise a session's window
2669
2555
  lattices windows [--json] List all desktop windows (daemon required)
2670
- lattices sessions [--json] List active tmux sessions via daemon
2556
+ lattices sessions [--json] List active sessions via daemon
2557
+ lattices terminals [--json] [--refresh]
2558
+ List synthesized terminal instances
2559
+ lattices capture window [wid] Save a screenshot run artifact
2560
+ lattices capture record window [wid] Record a window/visible region as a .mov artifact
2561
+ lattices capture record-command --app Scout -- <cmd>
2562
+ Record a target while running an action command
2563
+ lattices capture stop <run-id> Stop a running capture recording
2564
+ lattices runs [id] [--json] List recent runs or inspect one run
2565
+ lattices computer prepare Resolve/stage a safe terminal action
2566
+ lattices computer focus-window Focus and verify a target window
2567
+ lattices computer launch-app Launch/focus a normal macOS app
2568
+ lattices computer type-window Type into a normal app window
2569
+ lattices computer click Stage or post a window-relative click
2570
+ lattices cua click CLI alias for the CUA SDK click action
2571
+ lattices computer scout Scout warm-up run for memo/demo recording
2572
+ lattices computer cursor Show a recorded cursor appearance
2573
+ lattices computer type-text Type text into a safe terminal target
2574
+ lattices computer demo-terminal Record/focus/type a safe terminal demo
2671
2575
  lattices tile <position> Tile the frontmost window (left, right, top, etc.)
2672
2576
  lattices tile family [app] [region] Smart-grid the frontmost app family, or a named app
2673
2577
  lattices distribute [app] [region] Smart-grid visible windows or just one app (daemon required)
@@ -2770,11 +2674,11 @@ Workspace:
2770
2674
  session ${sessionName}
2771
2675
  config ${config ? ".lattices.json" : "none yet"}
2772
2676
  panes ${panes.map((p) => p.name || "pane").join(", ")}
2773
- tmux ${tmuxReady ? (sessionRunning ? "running" : "ready") : "missing"}
2677
+ sessions ${tmuxReady ? (sessionRunning ? "running" : "ready") : "missing"}
2774
2678
  app ${appRunning ? "running" : "not running"}
2775
2679
 
2776
2680
  Common commands:
2777
- lattices start Start or reattach this directory's tmux workspace
2681
+ lattices start Start or reattach this directory's workspace
2778
2682
  lattices init Create a .lattices.json for this project
2779
2683
  lattices app Launch the menu bar app
2780
2684
  lattices ls List active sessions
@@ -2811,7 +2715,7 @@ function listSessions(): void {
2811
2715
  "tmux list-sessions -F '#{session_name} (#{session_windows} windows, created #{session_created_string})'"
2812
2716
  );
2813
2717
  if (!out) {
2814
- console.log("No active tmux sessions.");
2718
+ console.log("No active sessions.");
2815
2719
  return;
2816
2720
  }
2817
2721
 
@@ -2914,7 +2818,7 @@ interface SpaceOptimizeRequest {
2914
2818
  function isPlacementToken(value?: string): boolean {
2915
2819
  if (!value) return false;
2916
2820
  const normalized = value.toLowerCase();
2917
- return normalized in tilePresets || /^grid:\d+x\d+:\d+,\d+$/i.test(normalized);
2821
+ return normalized in tilePresets || /^(?:grid:)?\d+x\d+:\d+,\d+(?:-\d+,\d+)?$/i.test(normalized);
2918
2822
  }
2919
2823
 
2920
2824
  function parseSpaceOptimizeArgs(rawArgs: string[], defaultScope: SpaceOptimizeScope): SpaceOptimizeRequest {
@@ -2939,8 +2843,7 @@ async function optimizeWindowsCommand(
2939
2843
  request: SpaceOptimizeRequest,
2940
2844
  successVerb: string
2941
2845
  ): Promise<void> {
2942
- try {
2943
- const { daemonCall } = await getDaemonClient();
2846
+ await withDaemon(async ({ daemonCall }) => {
2944
2847
  const params: Record<string, unknown> = {
2945
2848
  scope: request.scope,
2946
2849
  strategy: "balanced",
@@ -2961,20 +2864,59 @@ async function optimizeWindowsCommand(
2961
2864
  console.log(
2962
2865
  `${successVerb} ${count} window${count === 1 ? "" : "s"} for ${target}${regionSuffix}.`
2963
2866
  );
2964
- } catch {
2965
- console.log("Daemon not running. Start with: lattices app");
2867
+ });
2868
+ }
2869
+
2870
+ function gridTileBounds(position: string, screen: ScreenBounds): number[] | null {
2871
+ const match = position.toLowerCase().match(/^(grid:)?(\d+)x(\d+):(\d+),(\d+)(?:-(\d+),(\d+))?$/);
2872
+ if (!match) return null;
2873
+
2874
+ const oneBased = !match[1];
2875
+ const columns = Number(match[2]);
2876
+ const rows = Number(match[3]);
2877
+ let c0 = Number(match[4]);
2878
+ let r0 = Number(match[5]);
2879
+ let c1 = match[6] === undefined ? c0 : Number(match[6]);
2880
+ let r1 = match[7] === undefined ? r0 : Number(match[7]);
2881
+ if (oneBased) {
2882
+ c0 -= 1;
2883
+ r0 -= 1;
2884
+ c1 -= 1;
2885
+ r1 -= 1;
2886
+ }
2887
+ const leftCell = Math.min(c0, c1);
2888
+ const rightCell = Math.max(c0, c1);
2889
+ const topCell = Math.min(r0, r1);
2890
+ const bottomCell = Math.max(r0, r1);
2891
+
2892
+ if (
2893
+ columns <= 0 || rows <= 0 ||
2894
+ leftCell < 0 || topCell < 0 ||
2895
+ rightCell >= columns || bottomCell >= rows
2896
+ ) {
2897
+ return null;
2966
2898
  }
2899
+
2900
+ const cellW = screen.w / columns;
2901
+ const cellH = screen.h / rows;
2902
+ return [
2903
+ screen.x + leftCell * cellW,
2904
+ screen.y + topCell * cellH,
2905
+ screen.x + (rightCell + 1) * cellW,
2906
+ screen.y + (bottomCell + 1) * cellH,
2907
+ ];
2967
2908
  }
2968
2909
 
2969
2910
  function tileWindow(position: string): void {
2970
- const preset = tilePresets[position];
2971
- if (!preset) {
2911
+ const normalized = position.toLowerCase();
2912
+ const screen = getScreenBounds();
2913
+ const bounds = tilePresets[normalized]?.(screen) ?? gridTileBounds(normalized, screen);
2914
+ if (!bounds) {
2972
2915
  console.log(`Unknown position: ${position}`);
2973
- console.log(`Available: ${Object.keys(tilePresets).filter(k => !k.includes("-half") && k !== "max").join(", ")}`);
2916
+ console.log(`Available: ${Object.keys(tilePresets).filter(k => !k.includes("-half") && k !== "max").join(", ")}, grid:CxR:c,r (0-based), CxR:c,r (1-based)`);
2974
2917
  return;
2975
2918
  }
2976
- const screen = getScreenBounds();
2977
- const [x1, y1, x2, y2] = preset(screen).map(Math.round);
2919
+ const [x1, y1, x2, y2] = bounds.map(Math.round);
2978
2920
  const script = `
2979
2921
  tell application "System Events"
2980
2922
  set frontApp to name of first application process whose frontmost is true
@@ -2983,7 +2925,7 @@ function tileWindow(position: string): void {
2983
2925
  set bounds of front window to {${x1}, ${y1}, ${x2}, ${y2}}
2984
2926
  end tell`;
2985
2927
  runQuiet(`osascript -e '${esc(script)}'`);
2986
- console.log(`Tiled → ${position}`);
2928
+ console.log(`Tiled → ${normalized}`);
2987
2929
  }
2988
2930
 
2989
2931
  function createOrAttach(): void {
@@ -3023,7 +2965,7 @@ function statusInventory(): void {
3023
2965
  'tmux list-sessions -F "#{session_name}\t#{session_windows}\t#{session_attached}"'
3024
2966
  );
3025
2967
  if (!sessionsRaw) {
3026
- console.log("No active tmux sessions.");
2968
+ console.log("No active sessions.");
3027
2969
  return;
3028
2970
  }
3029
2971
 
@@ -3212,7 +3154,7 @@ switch (command) {
3212
3154
  break;
3213
3155
  case "search":
3214
3156
  case "s":
3215
- await searchCommand(args[1], new Set(args.slice(2)));
3157
+ await searchCommand(args[1], new Set(args.slice(2)), args.slice(2));
3216
3158
  break;
3217
3159
  case "focus":
3218
3160
  await focusCommand(args[1]);
@@ -3223,6 +3165,24 @@ switch (command) {
3223
3165
  case "sessions":
3224
3166
  await sessionsCommand(args[1] === "--json");
3225
3167
  break;
3168
+ case "terminals":
3169
+ await terminalsCommand(args.slice(1));
3170
+ break;
3171
+ case "capture":
3172
+ await captureCommand(args[1], ...args.slice(2));
3173
+ break;
3174
+ case "runs":
3175
+ await runsCommand(args.slice(1));
3176
+ break;
3177
+ case "run":
3178
+ await runsCommand(args.slice(1));
3179
+ break;
3180
+ case "computer":
3181
+ await computerCommand(args[1], ...args.slice(2));
3182
+ break;
3183
+ case "cua":
3184
+ await computerCommand(args[1], ...args.slice(2));
3185
+ break;
3226
3186
  case "voice":
3227
3187
  await voiceCommand(args[1], ...args.slice(2));
3228
3188
  break;