@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.
- package/README.md +13 -4
- package/apps/mac/Info.plist +4 -2
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -2
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +11 -0
- package/apps/mac/Lattices.entitlements +6 -0
- package/bin/assistant-intelligence.ts +41 -3
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +2 -1
- package/bin/cua.ts +26 -0
- package/bin/infer.ts +22 -4
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +111 -12
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +29 -2
- package/bin/lattices.ts +729 -769
- package/docs/api.md +496 -3
- package/docs/app.md +5 -4
- package/docs/assistant-knowledge.md +130 -0
- package/docs/config.md +5 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +53 -0
- package/docs/mouse-gestures.md +40 -3
- package/docs/ocr.md +3 -0
- package/docs/prompts/hands-off-system.md +9 -1
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/{LAT-006-mira-in-lattices.md → LAT-006-runs-and-capture-in-lattices.md} +83 -70
- package/docs/quickstart.md +3 -1
- package/docs/reference/dewey.config.ts +1 -1
- package/docs/release.md +4 -3
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +5 -3
- package/docs/voice.md +3 -3
- package/package.json +27 -5
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
package/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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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(`
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
954
|
-
console.log(`Error: ${(e as Error).message}`);
|
|
955
|
-
}
|
|
887
|
+
});
|
|
956
888
|
}
|
|
957
889
|
|
|
958
890
|
async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
|
|
959
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
986
|
-
const { daemonCall } = await getDaemonClient();
|
|
914
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
987
915
|
await daemonCall("window.focus", { session });
|
|
988
916
|
console.log(`Focused: ${session}`);
|
|
989
|
-
}
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
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
|
-
|
|
1198
|
-
{
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1266
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1674
|
+
if (hasFlag(rest, "json")) {
|
|
1675
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1599
1678
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
|
1690
|
-
name: asset.appName,
|
|
1691
|
-
message
|
|
1692
|
-
placement
|
|
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
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
2965
|
-
|
|
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
|
|
2971
|
-
|
|
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
|
|
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 → ${
|
|
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
|
|
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;
|