@lattices/cli 0.3.0 → 0.4.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 +85 -9
- package/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Package.swift +8 -1
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +45 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +189 -6
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +802 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +49 -9
- package/app/Sources/IntentEngine.swift +962 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1275 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +58 -45
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +740 -0
- package/bin/lattices-app.ts +338 -0
- package/bin/lattices-dev +208 -0
- package/bin/{lattices.js → lattices.ts} +777 -140
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +29 -11
- package/bin/client.js +0 -4
- package/bin/lattices-app.js +0 -221
|
@@ -1,32 +1,37 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { basename, resolve
|
|
6
|
+
import { basename, resolve } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
8
|
|
|
10
9
|
// Daemon client (lazy-loaded to avoid blocking startup for TTY commands)
|
|
11
|
-
let _daemonClient;
|
|
12
|
-
async function getDaemonClient() {
|
|
10
|
+
let _daemonClient: typeof import("./daemon-client.ts") | undefined;
|
|
11
|
+
async function getDaemonClient(): Promise<typeof import("./daemon-client.ts")> {
|
|
13
12
|
if (!_daemonClient) {
|
|
14
|
-
|
|
15
|
-
_daemonClient = await import(resolve(__dirname, "daemon-client.js"));
|
|
13
|
+
_daemonClient = await import("./daemon-client.ts");
|
|
16
14
|
}
|
|
17
15
|
return _daemonClient;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
const command = args[0];
|
|
18
|
+
const args: string[] = process.argv.slice(2);
|
|
19
|
+
const command: string | undefined = args[0];
|
|
22
20
|
|
|
23
21
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
interface ExecOpts {
|
|
24
|
+
encoding?: string;
|
|
25
|
+
stdio?: string | string[];
|
|
26
|
+
cwd?: string;
|
|
27
|
+
[key: string]: any;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function
|
|
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 {
|
|
30
35
|
try {
|
|
31
36
|
return run(cmd, { stdio: "pipe" });
|
|
32
37
|
} catch {
|
|
@@ -34,77 +39,117 @@ function runQuiet(cmd) {
|
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
function hasTmux() {
|
|
42
|
+
function hasTmux(): boolean {
|
|
38
43
|
return runQuiet("which tmux") !== null;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
/** Commands that require tmux to be installed */
|
|
47
|
+
const tmuxRequiredCommands = new Set([
|
|
48
|
+
"init", "ls", "list", "kill", "rm", "sync", "reconcile",
|
|
49
|
+
"restart", "respawn", "group", "groups", "tab", "status",
|
|
50
|
+
"inventory", "distribute", "sessions",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function requireTmux(command: string | undefined): void {
|
|
54
|
+
if (hasTmux()) return;
|
|
55
|
+
|
|
56
|
+
const isImplicitCreate = command && !tmuxRequiredCommands.has(command)
|
|
57
|
+
&& !["search", "s", "focus", "place", "tile", "t", "windows", "window",
|
|
58
|
+
"voice", "call", "layer", "layers", "diag", "diagnostics", "scan",
|
|
59
|
+
"ocr", "daemon", "dev", "app", "mouse", "help", "-h", "--help"].includes(command);
|
|
60
|
+
|
|
61
|
+
if (command && !tmuxRequiredCommands.has(command) && !isImplicitCreate) return;
|
|
62
|
+
|
|
63
|
+
console.error(`
|
|
64
|
+
\x1b[1;31m✘ tmux not found\x1b[0m
|
|
65
|
+
|
|
66
|
+
Lattices uses tmux for terminal session management.
|
|
67
|
+
Install it with Homebrew:
|
|
68
|
+
|
|
69
|
+
\x1b[1mbrew install tmux\x1b[0m
|
|
70
|
+
|
|
71
|
+
If tmux is installed somewhere else, make sure it's on your PATH:
|
|
72
|
+
|
|
73
|
+
\x1b[90mexport PATH="/path/to/tmux/bin:$PATH"\x1b[0m
|
|
74
|
+
|
|
75
|
+
Then run this command again.
|
|
76
|
+
`.trim());
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isInsideTmux(): boolean {
|
|
42
81
|
return !!process.env.TMUX;
|
|
43
82
|
}
|
|
44
83
|
|
|
45
|
-
function sessionExists(name) {
|
|
84
|
+
function sessionExists(name: string): boolean {
|
|
46
85
|
return runQuiet(`tmux has-session -t "${name}" 2>&1`) !== null;
|
|
47
86
|
}
|
|
48
87
|
|
|
49
|
-
function pathHash(dir) {
|
|
88
|
+
function pathHash(dir: string): string {
|
|
50
89
|
return createHash("sha256").update(resolve(dir)).digest("hex").slice(0, 6);
|
|
51
90
|
}
|
|
52
91
|
|
|
53
|
-
function toSessionName(dir) {
|
|
92
|
+
function toSessionName(dir: string): string {
|
|
54
93
|
const base = basename(dir).replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
55
94
|
return `${base}-${pathHash(dir)}`;
|
|
56
95
|
}
|
|
57
96
|
|
|
58
|
-
function esc(str) {
|
|
97
|
+
function esc(str: string): string {
|
|
59
98
|
return str.replace(/'/g, "'\\''");
|
|
60
99
|
}
|
|
61
100
|
|
|
62
101
|
// ── Config ───────────────────────────────────────────────────────────
|
|
63
102
|
|
|
64
|
-
function readConfig(dir) {
|
|
103
|
+
function readConfig(dir: string): any | null {
|
|
65
104
|
const configPath = resolve(dir, ".lattices.json");
|
|
66
105
|
if (!existsSync(configPath)) return null;
|
|
67
106
|
try {
|
|
68
107
|
const raw = readFileSync(configPath, "utf8");
|
|
69
108
|
return JSON.parse(raw);
|
|
70
|
-
} catch (e) {
|
|
71
|
-
console.warn(`Warning: invalid .lattices.json — ${e.message}`);
|
|
109
|
+
} catch (e: unknown) {
|
|
110
|
+
console.warn(`Warning: invalid .lattices.json — ${(e as Error).message}`);
|
|
72
111
|
return null;
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
114
|
|
|
76
115
|
// ── Workspace config (tab groups) ───────────────────────────────────
|
|
77
116
|
|
|
78
|
-
function readWorkspaceConfig() {
|
|
117
|
+
function readWorkspaceConfig(): any | null {
|
|
79
118
|
const configPath = resolve(homedir(), ".lattices", "workspace.json");
|
|
80
119
|
if (!existsSync(configPath)) return null;
|
|
81
120
|
try {
|
|
82
121
|
const raw = readFileSync(configPath, "utf8");
|
|
83
122
|
return JSON.parse(raw);
|
|
84
|
-
} catch (e) {
|
|
85
|
-
console.warn(`Warning: invalid workspace.json — ${e.message}`);
|
|
123
|
+
} catch (e: unknown) {
|
|
124
|
+
console.warn(`Warning: invalid workspace.json — ${(e as Error).message}`);
|
|
86
125
|
return null;
|
|
87
126
|
}
|
|
88
127
|
}
|
|
89
128
|
|
|
90
|
-
function toGroupSessionName(groupId) {
|
|
129
|
+
function toGroupSessionName(groupId: string): string {
|
|
91
130
|
return `lattices-group-${groupId}`;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
133
|
/** Get ordered pane IDs for a specific window within a session */
|
|
95
|
-
function getPaneIdsForWindow(sessionName, windowIndex) {
|
|
134
|
+
function getPaneIdsForWindow(sessionName: string, windowIndex: number): string[] {
|
|
96
135
|
const out = runQuiet(
|
|
97
136
|
`tmux list-panes -t "${sessionName}:${windowIndex}" -F "#{pane_id}"`
|
|
98
137
|
);
|
|
99
138
|
return out ? out.split("\n").filter(Boolean) : [];
|
|
100
139
|
}
|
|
101
140
|
|
|
141
|
+
interface PaneConfig {
|
|
142
|
+
name?: string;
|
|
143
|
+
cmd?: string;
|
|
144
|
+
size?: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
102
147
|
/** Create a tmux window with pane layout for a project dir */
|
|
103
|
-
function createWindowForProject(sessionName, windowIndex, dir, label) {
|
|
148
|
+
function createWindowForProject(sessionName: string, windowIndex: number, dir: string, label?: string): void {
|
|
104
149
|
const config = readConfig(dir);
|
|
105
150
|
const d = esc(dir);
|
|
106
151
|
|
|
107
|
-
let panes;
|
|
152
|
+
let panes: PaneConfig[];
|
|
108
153
|
if (config?.panes?.length) {
|
|
109
154
|
panes = resolvePane(config.panes, dir);
|
|
110
155
|
} else {
|
|
@@ -141,7 +186,7 @@ function createWindowForProject(sessionName, windowIndex, dir, label) {
|
|
|
141
186
|
const paneIds = getPaneIdsForWindow(sessionName, windowIndex);
|
|
142
187
|
for (let i = 0; i < panes.length && i < paneIds.length; i++) {
|
|
143
188
|
if (panes[i].cmd) {
|
|
144
|
-
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}' Enter`);
|
|
189
|
+
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
|
|
145
190
|
}
|
|
146
191
|
if (panes[i].name) {
|
|
147
192
|
runQuiet(`tmux select-pane -t "${paneIds[i]}" -T "${panes[i].name}"`);
|
|
@@ -154,8 +199,19 @@ function createWindowForProject(sessionName, windowIndex, dir, label) {
|
|
|
154
199
|
}
|
|
155
200
|
}
|
|
156
201
|
|
|
202
|
+
interface TabConfig {
|
|
203
|
+
path: string;
|
|
204
|
+
label?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface GroupConfig {
|
|
208
|
+
id: string;
|
|
209
|
+
label?: string;
|
|
210
|
+
tabs?: TabConfig[];
|
|
211
|
+
}
|
|
212
|
+
|
|
157
213
|
/** Create a group session with one tmux window per tab */
|
|
158
|
-
function createGroupSession(group) {
|
|
214
|
+
function createGroupSession(group: GroupConfig): string | null {
|
|
159
215
|
const name = toGroupSessionName(group.id);
|
|
160
216
|
const tabs = group.tabs || [];
|
|
161
217
|
|
|
@@ -194,7 +250,7 @@ function createGroupSession(group) {
|
|
|
194
250
|
return name;
|
|
195
251
|
}
|
|
196
252
|
|
|
197
|
-
function listGroups() {
|
|
253
|
+
function listGroups(): void {
|
|
198
254
|
const ws = readWorkspaceConfig();
|
|
199
255
|
if (!ws?.groups?.length) {
|
|
200
256
|
console.log("No tab groups configured in ~/.lattices/workspace.json");
|
|
@@ -204,12 +260,12 @@ function listGroups() {
|
|
|
204
260
|
console.log("Tab Groups:\n");
|
|
205
261
|
for (const group of ws.groups) {
|
|
206
262
|
const tabs = group.tabs || [];
|
|
207
|
-
const runningCount = tabs.filter((t) => sessionExists(toSessionName(resolve(t.path)))).length;
|
|
263
|
+
const runningCount = tabs.filter((t: TabConfig) => sessionExists(toSessionName(resolve(t.path)))).length;
|
|
208
264
|
const running = runningCount > 0;
|
|
209
265
|
const status = running
|
|
210
266
|
? `\x1b[32m● ${runningCount}/${tabs.length} running\x1b[0m`
|
|
211
267
|
: "\x1b[90m○ stopped\x1b[0m";
|
|
212
|
-
const tabLabels = tabs.map((t) => t.label || basename(t.path)).join(", ");
|
|
268
|
+
const tabLabels = tabs.map((t: TabConfig) => t.label || basename(t.path)).join(", ");
|
|
213
269
|
console.log(` ${group.label || group.id} ${status}`);
|
|
214
270
|
console.log(` id: ${group.id}`);
|
|
215
271
|
console.log(` tabs: ${tabLabels}`);
|
|
@@ -217,7 +273,7 @@ function listGroups() {
|
|
|
217
273
|
}
|
|
218
274
|
}
|
|
219
275
|
|
|
220
|
-
function groupCommand(id) {
|
|
276
|
+
function groupCommand(id?: string): void {
|
|
221
277
|
const ws = readWorkspaceConfig();
|
|
222
278
|
if (!ws?.groups?.length) {
|
|
223
279
|
console.log("No tab groups configured in ~/.lattices/workspace.json");
|
|
@@ -229,9 +285,9 @@ function groupCommand(id) {
|
|
|
229
285
|
return;
|
|
230
286
|
}
|
|
231
287
|
|
|
232
|
-
const group = ws.groups.find((g) => g.id === id);
|
|
288
|
+
const group = ws.groups.find((g: GroupConfig) => g.id === id);
|
|
233
289
|
if (!group) {
|
|
234
|
-
console.log(`No group "${id}". Available: ${ws.groups.map((g) => g.id).join(", ")}`);
|
|
290
|
+
console.log(`No group "${id}". Available: ${ws.groups.map((g: GroupConfig) => g.id).join(", ")}`);
|
|
235
291
|
return;
|
|
236
292
|
}
|
|
237
293
|
|
|
@@ -267,7 +323,7 @@ function groupCommand(id) {
|
|
|
267
323
|
attach(firstName);
|
|
268
324
|
}
|
|
269
325
|
|
|
270
|
-
function tabCommand(groupId, tabName) {
|
|
326
|
+
function tabCommand(groupId?: string, tabName?: string): void {
|
|
271
327
|
if (!groupId) {
|
|
272
328
|
console.log("Usage: lattices tab <group-id> <tab-name|index>");
|
|
273
329
|
return;
|
|
@@ -279,13 +335,13 @@ function tabCommand(groupId, tabName) {
|
|
|
279
335
|
return;
|
|
280
336
|
}
|
|
281
337
|
|
|
282
|
-
const group = ws.groups.find((g) => g.id === groupId);
|
|
338
|
+
const group = ws.groups.find((g: GroupConfig) => g.id === groupId);
|
|
283
339
|
if (!group) {
|
|
284
340
|
console.log(`No group "${groupId}".`);
|
|
285
341
|
return;
|
|
286
342
|
}
|
|
287
343
|
|
|
288
|
-
const tabs = group.tabs || [];
|
|
344
|
+
const tabs: TabConfig[] = group.tabs || [];
|
|
289
345
|
|
|
290
346
|
if (!tabName) {
|
|
291
347
|
// List tabs with their session status
|
|
@@ -301,7 +357,7 @@ function tabCommand(groupId, tabName) {
|
|
|
301
357
|
}
|
|
302
358
|
|
|
303
359
|
// Resolve tab target to an index
|
|
304
|
-
let tabIdx;
|
|
360
|
+
let tabIdx: number;
|
|
305
361
|
if (/^\d+$/.test(tabName)) {
|
|
306
362
|
tabIdx = parseInt(tabName, 10);
|
|
307
363
|
} else {
|
|
@@ -337,7 +393,7 @@ function tabCommand(groupId, tabName) {
|
|
|
337
393
|
|
|
338
394
|
// ── Detect dev command ───────────────────────────────────────────────
|
|
339
395
|
|
|
340
|
-
function detectPackageManager(dir) {
|
|
396
|
+
function detectPackageManager(dir: string): string {
|
|
341
397
|
if (existsSync(resolve(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
342
398
|
if (existsSync(resolve(dir, "bun.lockb")) || existsSync(resolve(dir, "bun.lock")))
|
|
343
399
|
return "bun";
|
|
@@ -345,11 +401,11 @@ function detectPackageManager(dir) {
|
|
|
345
401
|
return "npm";
|
|
346
402
|
}
|
|
347
403
|
|
|
348
|
-
function detectDevCommand(dir) {
|
|
404
|
+
function detectDevCommand(dir: string): string | null {
|
|
349
405
|
const pkgPath = resolve(dir, "package.json");
|
|
350
406
|
if (!existsSync(pkgPath)) return null;
|
|
351
407
|
|
|
352
|
-
let pkg;
|
|
408
|
+
let pkg: any;
|
|
353
409
|
try {
|
|
354
410
|
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
355
411
|
} catch {
|
|
@@ -358,19 +414,19 @@ function detectDevCommand(dir) {
|
|
|
358
414
|
|
|
359
415
|
const scripts = pkg.scripts || {};
|
|
360
416
|
const pm = detectPackageManager(dir);
|
|
361
|
-
const
|
|
417
|
+
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
362
418
|
|
|
363
|
-
if (scripts.dev) return `${
|
|
364
|
-
if (scripts.start) return `${
|
|
365
|
-
if (scripts.serve) return `${
|
|
366
|
-
if (scripts.watch) return `${
|
|
419
|
+
if (scripts.dev) return `${runCmd} dev`;
|
|
420
|
+
if (scripts.start) return `${runCmd} start`;
|
|
421
|
+
if (scripts.serve) return `${runCmd} serve`;
|
|
422
|
+
if (scripts.watch) return `${runCmd} watch`;
|
|
367
423
|
return null;
|
|
368
424
|
}
|
|
369
425
|
|
|
370
426
|
// ── Session creation ─────────────────────────────────────────────────
|
|
371
427
|
|
|
372
|
-
function resolvePane(panes, dir) {
|
|
373
|
-
return panes.map((p) => ({
|
|
428
|
+
function resolvePane(panes: any[], dir: string): PaneConfig[] {
|
|
429
|
+
return panes.map((p: any) => ({
|
|
374
430
|
name: p.name || "",
|
|
375
431
|
cmd: p.cmd || undefined,
|
|
376
432
|
size: p.size || undefined,
|
|
@@ -378,19 +434,19 @@ function resolvePane(panes, dir) {
|
|
|
378
434
|
}
|
|
379
435
|
|
|
380
436
|
/** Get ordered pane IDs (e.g. ["%0", "%1"]) for a session */
|
|
381
|
-
function getPaneIds(name) {
|
|
437
|
+
function getPaneIds(name: string): string[] {
|
|
382
438
|
const out = runQuiet(
|
|
383
439
|
`tmux list-panes -t "${name}" -F "#{pane_id}"`
|
|
384
440
|
);
|
|
385
441
|
return out ? out.split("\n").filter(Boolean) : [];
|
|
386
442
|
}
|
|
387
443
|
|
|
388
|
-
function createSession(dir) {
|
|
444
|
+
function createSession(dir: string): string {
|
|
389
445
|
const name = toSessionName(dir);
|
|
390
446
|
const config = readConfig(dir);
|
|
391
447
|
const d = esc(dir);
|
|
392
448
|
|
|
393
|
-
let panes;
|
|
449
|
+
let panes: PaneConfig[];
|
|
394
450
|
if (config?.panes?.length) {
|
|
395
451
|
panes = resolvePane(config.panes, dir);
|
|
396
452
|
console.log(`Using .lattices.json (${panes.length} panes)`);
|
|
@@ -425,7 +481,7 @@ function createSession(dir) {
|
|
|
425
481
|
// Send commands and name each pane
|
|
426
482
|
for (let i = 0; i < panes.length && i < paneIds.length; i++) {
|
|
427
483
|
if (panes[i].cmd) {
|
|
428
|
-
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}' Enter`);
|
|
484
|
+
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
|
|
429
485
|
}
|
|
430
486
|
if (panes[i].name) {
|
|
431
487
|
runQuiet(`tmux select-pane -t "${paneIds[i]}" -T "${panes[i].name}"`);
|
|
@@ -449,9 +505,9 @@ function createSession(dir) {
|
|
|
449
505
|
/** Check each pane and prefill or restart commands that have exited.
|
|
450
506
|
* mode: "prefill" types the command without pressing Enter
|
|
451
507
|
* mode: "ensure" types the command and presses Enter */
|
|
452
|
-
function restoreCommands(name, dir, mode) {
|
|
508
|
+
function restoreCommands(name: string, dir: string, mode: "prefill" | "ensure"): void {
|
|
453
509
|
const config = readConfig(dir);
|
|
454
|
-
let panes;
|
|
510
|
+
let panes: PaneConfig[];
|
|
455
511
|
if (config?.panes?.length) {
|
|
456
512
|
panes = resolvePane(config.panes, dir);
|
|
457
513
|
} else {
|
|
@@ -469,9 +525,9 @@ function restoreCommands(name, dir, mode) {
|
|
|
469
525
|
);
|
|
470
526
|
if (cur && shells.has(cur)) {
|
|
471
527
|
if (mode === "ensure") {
|
|
472
|
-
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}' Enter`);
|
|
528
|
+
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
|
|
473
529
|
} else {
|
|
474
|
-
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd)}'`);
|
|
530
|
+
run(`tmux send-keys -t "${paneIds[i]}" '${esc(panes[i].cmd!)}'`);
|
|
475
531
|
}
|
|
476
532
|
count++;
|
|
477
533
|
}
|
|
@@ -484,7 +540,7 @@ function restoreCommands(name, dir, mode) {
|
|
|
484
540
|
|
|
485
541
|
// ── Sync / reconcile ────────────────────────────────────────────────
|
|
486
542
|
|
|
487
|
-
function resolvePanes(dir) {
|
|
543
|
+
function resolvePanes(dir: string): PaneConfig[] {
|
|
488
544
|
const config = readConfig(dir);
|
|
489
545
|
if (config?.panes?.length) {
|
|
490
546
|
return resolvePane(config.panes, dir);
|
|
@@ -492,7 +548,117 @@ function resolvePanes(dir) {
|
|
|
492
548
|
return defaultPanes(dir);
|
|
493
549
|
}
|
|
494
550
|
|
|
495
|
-
|
|
551
|
+
// ── Dev command ──────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
function detectProjectType(dir: string): string | null {
|
|
554
|
+
// Check for lattices-style hybrid project (Swift app + Node CLI)
|
|
555
|
+
if (existsSync(resolve(dir, "app/Package.swift")) && existsSync(resolve(dir, "bin/lattices-app.ts")))
|
|
556
|
+
return "lattices-app";
|
|
557
|
+
if (existsSync(resolve(dir, "Package.swift"))) return "swift";
|
|
558
|
+
if (existsSync(resolve(dir, "Cargo.toml"))) return "rust";
|
|
559
|
+
if (existsSync(resolve(dir, "go.mod"))) return "go";
|
|
560
|
+
if (existsSync(resolve(dir, "package.json"))) return "node";
|
|
561
|
+
if (existsSync(resolve(dir, "Makefile"))) return "make";
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function devCommand(sub?: string, ...flags: string[]): Promise<void> {
|
|
566
|
+
const dir = process.cwd();
|
|
567
|
+
const type = detectProjectType(dir);
|
|
568
|
+
|
|
569
|
+
// Helper to forward to lattices-app.ts
|
|
570
|
+
async function forwardToAppScript(cmd: string, extraFlags: string[] = []): Promise<void> {
|
|
571
|
+
const appScript = resolve(import.meta.dir, "lattices-app.ts");
|
|
572
|
+
const { execFileSync } = await import("node:child_process");
|
|
573
|
+
try {
|
|
574
|
+
execFileSync("bun", [appScript, cmd, ...extraFlags], { stdio: "inherit" });
|
|
575
|
+
} catch { /* exit code forwarded */ }
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!sub) {
|
|
579
|
+
// bare `lattices dev` — run dev server
|
|
580
|
+
if (!type) {
|
|
581
|
+
console.log("No recognized project in current directory.");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
console.log(`Detected: ${type} project`);
|
|
585
|
+
if (type === "lattices-app") {
|
|
586
|
+
await forwardToAppScript("restart", flags);
|
|
587
|
+
} else if (type === "node") {
|
|
588
|
+
const cmd = detectDevCommand(dir);
|
|
589
|
+
if (cmd) {
|
|
590
|
+
console.log(`Running: ${cmd}`);
|
|
591
|
+
execSync(cmd, { cwd: dir, stdio: "inherit" });
|
|
592
|
+
} else {
|
|
593
|
+
console.log("No dev script found in package.json.");
|
|
594
|
+
}
|
|
595
|
+
} else if (type === "swift") {
|
|
596
|
+
console.log("Running: swift run");
|
|
597
|
+
execSync("swift run", { cwd: dir, stdio: "inherit" });
|
|
598
|
+
} else if (type === "rust") {
|
|
599
|
+
console.log("Running: cargo run");
|
|
600
|
+
execSync("cargo run", { cwd: dir, stdio: "inherit" });
|
|
601
|
+
} else if (type === "go") {
|
|
602
|
+
console.log("Running: go run .");
|
|
603
|
+
execSync("go run .", { cwd: dir, stdio: "inherit" });
|
|
604
|
+
} else if (type === "make") {
|
|
605
|
+
execSync("make", { cwd: dir, stdio: "inherit" });
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (sub === "build") {
|
|
611
|
+
if (!type) {
|
|
612
|
+
console.log("No recognized project in current directory.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (type === "lattices-app") {
|
|
616
|
+
await forwardToAppScript("build");
|
|
617
|
+
} else if (type === "swift") {
|
|
618
|
+
console.log("Building: swift build -c release");
|
|
619
|
+
execSync("swift build -c release", { cwd: dir, stdio: "inherit" });
|
|
620
|
+
} else if (type === "node") {
|
|
621
|
+
const pm = detectPackageManager(dir);
|
|
622
|
+
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
623
|
+
const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8"));
|
|
624
|
+
if (pkg.scripts?.build) {
|
|
625
|
+
console.log(`Running: ${runCmd} build`);
|
|
626
|
+
execSync(`${runCmd} build`, { cwd: dir, stdio: "inherit" });
|
|
627
|
+
} else {
|
|
628
|
+
console.log("No build script found in package.json.");
|
|
629
|
+
}
|
|
630
|
+
} else if (type === "rust") {
|
|
631
|
+
console.log("Building: cargo build --release");
|
|
632
|
+
execSync("cargo build --release", { cwd: dir, stdio: "inherit" });
|
|
633
|
+
} else if (type === "go") {
|
|
634
|
+
console.log("Building: go build .");
|
|
635
|
+
execSync("go build .", { cwd: dir, stdio: "inherit" });
|
|
636
|
+
} else if (type === "make") {
|
|
637
|
+
execSync("make", { cwd: dir, stdio: "inherit" });
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (sub === "restart") {
|
|
643
|
+
if (type === "lattices-app") {
|
|
644
|
+
await forwardToAppScript("restart", flags);
|
|
645
|
+
} else {
|
|
646
|
+
// For other project types, just rebuild
|
|
647
|
+
await devCommand("build");
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (sub === "type") {
|
|
653
|
+
console.log(type || "unknown");
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
console.log(`Unknown dev subcommand: ${sub}`);
|
|
658
|
+
console.log("Usage: lattices dev [build|restart|type]");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function defaultPanes(dir: string): PaneConfig[] {
|
|
496
662
|
const devCmd = detectDevCommand(dir);
|
|
497
663
|
if (devCmd) {
|
|
498
664
|
return [
|
|
@@ -504,7 +670,7 @@ function defaultPanes(dir) {
|
|
|
504
670
|
return [{ name: "claude", cmd: "claude" }];
|
|
505
671
|
}
|
|
506
672
|
|
|
507
|
-
function syncSession() {
|
|
673
|
+
function syncSession(): void {
|
|
508
674
|
const dir = process.cwd();
|
|
509
675
|
const name = toSessionName(dir);
|
|
510
676
|
|
|
@@ -564,7 +730,7 @@ function syncSession() {
|
|
|
564
730
|
`tmux display-message -t "${freshIds[i]}" -p "#{pane_current_command}"`
|
|
565
731
|
);
|
|
566
732
|
if (cur && shells.has(cur)) {
|
|
567
|
-
run(`tmux send-keys -t "${freshIds[i]}" '${esc(panes[i].cmd)}' Enter`);
|
|
733
|
+
run(`tmux send-keys -t "${freshIds[i]}" '${esc(panes[i].cmd!)}' Enter`);
|
|
568
734
|
restored++;
|
|
569
735
|
}
|
|
570
736
|
}
|
|
@@ -583,7 +749,7 @@ function syncSession() {
|
|
|
583
749
|
|
|
584
750
|
// ── Restart pane ────────────────────────────────────────────────────
|
|
585
751
|
|
|
586
|
-
function restartPane(target) {
|
|
752
|
+
function restartPane(target?: string): void {
|
|
587
753
|
const dir = process.cwd();
|
|
588
754
|
const name = toSessionName(dir);
|
|
589
755
|
|
|
@@ -596,7 +762,7 @@ function restartPane(target) {
|
|
|
596
762
|
const paneIds = getPaneIds(name);
|
|
597
763
|
|
|
598
764
|
// Resolve target to an index
|
|
599
|
-
let idx;
|
|
765
|
+
let idx: number;
|
|
600
766
|
if (target === undefined || target === null || target === "") {
|
|
601
767
|
// Default: first pane (claude)
|
|
602
768
|
idx = 0;
|
|
@@ -665,10 +831,22 @@ function restartPane(target) {
|
|
|
665
831
|
|
|
666
832
|
// ── Daemon-aware commands ────────────────────────────────────────────
|
|
667
833
|
|
|
668
|
-
async function
|
|
834
|
+
async function mouseCommand(sub?: string): Promise<void> {
|
|
835
|
+
const { daemonCall } = await getDaemonClient();
|
|
836
|
+
if (sub === "summon") {
|
|
837
|
+
const result = await daemonCall("mouse.summon") as any;
|
|
838
|
+
console.log(`🎯 Mouse summoned to (${result.x}, ${result.y})`);
|
|
839
|
+
} else {
|
|
840
|
+
// Default: find
|
|
841
|
+
const result = await daemonCall("mouse.find") as any;
|
|
842
|
+
console.log(`🔍 Mouse at (${result.x}, ${result.y})`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function daemonStatusCommand(): Promise<void> {
|
|
669
847
|
try {
|
|
670
848
|
const { daemonCall } = await getDaemonClient();
|
|
671
|
-
const status = await daemonCall("daemon.status");
|
|
849
|
+
const status = await daemonCall("daemon.status") as any;
|
|
672
850
|
const uptime = Math.round(status.uptime);
|
|
673
851
|
const h = Math.floor(uptime / 3600);
|
|
674
852
|
const m = Math.floor((uptime % 3600) / 60);
|
|
@@ -685,10 +863,10 @@ async function daemonStatusCommand() {
|
|
|
685
863
|
}
|
|
686
864
|
}
|
|
687
865
|
|
|
688
|
-
async function windowsCommand(jsonFlag) {
|
|
866
|
+
async function windowsCommand(jsonFlag: boolean): Promise<void> {
|
|
689
867
|
try {
|
|
690
868
|
const { daemonCall } = await getDaemonClient();
|
|
691
|
-
const windows = await daemonCall("windows.list");
|
|
869
|
+
const windows = await daemonCall("windows.list") as any[];
|
|
692
870
|
if (jsonFlag) {
|
|
693
871
|
console.log(JSON.stringify(windows, null, 2));
|
|
694
872
|
return;
|
|
@@ -712,7 +890,7 @@ async function windowsCommand(jsonFlag) {
|
|
|
712
890
|
}
|
|
713
891
|
}
|
|
714
892
|
|
|
715
|
-
async function windowAssignCommand(wid, layerId) {
|
|
893
|
+
async function windowAssignCommand(wid?: string, layerId?: string): Promise<void> {
|
|
716
894
|
if (!wid || !layerId) {
|
|
717
895
|
console.log("Usage: lattices window assign <wid> <layer-id>");
|
|
718
896
|
return;
|
|
@@ -721,15 +899,15 @@ async function windowAssignCommand(wid, layerId) {
|
|
|
721
899
|
const { daemonCall } = await getDaemonClient();
|
|
722
900
|
await daemonCall("window.assignLayer", { wid: parseInt(wid), layer: layerId });
|
|
723
901
|
console.log(`Tagged wid:${wid} → layer:${layerId}`);
|
|
724
|
-
} catch (e) {
|
|
725
|
-
console.log(`Error: ${e.message}`);
|
|
902
|
+
} catch (e: unknown) {
|
|
903
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
726
904
|
}
|
|
727
905
|
}
|
|
728
906
|
|
|
729
|
-
async function windowLayerMapCommand(jsonFlag) {
|
|
907
|
+
async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
|
|
730
908
|
try {
|
|
731
909
|
const { daemonCall } = await getDaemonClient();
|
|
732
|
-
const map = await daemonCall("window.layerMap");
|
|
910
|
+
const map = await daemonCall("window.layerMap") as any;
|
|
733
911
|
if (jsonFlag) {
|
|
734
912
|
console.log(JSON.stringify(map, null, 2));
|
|
735
913
|
return;
|
|
@@ -748,7 +926,7 @@ async function windowLayerMapCommand(jsonFlag) {
|
|
|
748
926
|
}
|
|
749
927
|
}
|
|
750
928
|
|
|
751
|
-
async function focusCommand(session) {
|
|
929
|
+
async function focusCommand(session?: string): Promise<void> {
|
|
752
930
|
if (!session) {
|
|
753
931
|
console.log("Usage: lattices focus <session-name>");
|
|
754
932
|
return;
|
|
@@ -757,16 +935,304 @@ async function focusCommand(session) {
|
|
|
757
935
|
const { daemonCall } = await getDaemonClient();
|
|
758
936
|
await daemonCall("window.focus", { session });
|
|
759
937
|
console.log(`Focused: ${session}`);
|
|
760
|
-
} catch (e) {
|
|
761
|
-
console.log(`Error: ${e.message}`);
|
|
938
|
+
} catch (e: unknown) {
|
|
939
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ── Search ───────────────────────────────────────────────────────────
|
|
944
|
+
|
|
945
|
+
interface SearchResult {
|
|
946
|
+
score: number;
|
|
947
|
+
window: any;
|
|
948
|
+
tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
|
|
949
|
+
reasons: string[];
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function relativeTime(iso: string): string {
|
|
953
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
954
|
+
const s = Math.floor(ms / 1000);
|
|
955
|
+
if (s < 60) return "just now";
|
|
956
|
+
const m = Math.floor(s / 60);
|
|
957
|
+
if (m < 60) return `${m}m ago`;
|
|
958
|
+
const h = Math.floor(m / 60);
|
|
959
|
+
if (h < 24) return `${h}h ago`;
|
|
960
|
+
const d = Math.floor(h / 24);
|
|
961
|
+
return `${d}d ago`;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Unified search via lattices.search daemon API.
|
|
965
|
+
// All search surfaces should go through this one function.
|
|
966
|
+
interface SearchOptions {
|
|
967
|
+
sources?: string[]; // e.g. ["titles", "apps", "cwd", "ocr"] — omit for smart default
|
|
968
|
+
after?: string; // ISO8601 — only windows interacted after this time
|
|
969
|
+
before?: string; // ISO8601 — only windows interacted before this time
|
|
970
|
+
recency?: boolean; // boost recently-focused windows (default true)
|
|
971
|
+
mode?: string; // legacy compat: "quick", "complete", "terminal"
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
|
|
975
|
+
const { daemonCall } = await getDaemonClient();
|
|
976
|
+
const params: Record<string, any> = { query };
|
|
977
|
+
if (opts.sources) params.sources = opts.sources;
|
|
978
|
+
if (opts.after) params.after = opts.after;
|
|
979
|
+
if (opts.before) params.before = opts.before;
|
|
980
|
+
if (opts.recency !== undefined) params.recency = opts.recency;
|
|
981
|
+
if (opts.mode) params.mode = opts.mode; // legacy fallback
|
|
982
|
+
const hits = await daemonCall("lattices.search", params, 10000) as any[];
|
|
983
|
+
return hits.map((w: any) => ({
|
|
984
|
+
score: w.score || 0,
|
|
985
|
+
window: w,
|
|
986
|
+
tabs: (w.terminalTabs || []).map((t: any) => ({
|
|
987
|
+
tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
|
|
988
|
+
})),
|
|
989
|
+
reasons: w.matchSources || [],
|
|
990
|
+
}));
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Convenience aliases
|
|
994
|
+
async function deepSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["all"] }); }
|
|
995
|
+
async function terminalSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["terminals"] }); }
|
|
996
|
+
|
|
997
|
+
// Format and print search results
|
|
998
|
+
function printResults(ranked: SearchResult[]): void {
|
|
999
|
+
if (!ranked.length) return;
|
|
1000
|
+
for (const r of ranked) {
|
|
1001
|
+
const w = r.window;
|
|
1002
|
+
const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
|
|
1003
|
+
console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
|
|
1004
|
+
for (const t of r.tabs) {
|
|
1005
|
+
const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
|
|
1006
|
+
const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
|
|
1007
|
+
console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
|
|
1010
|
+
}
|
|
1011
|
+
console.log();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ── search command ───────────────────────────────────────────────────
|
|
1015
|
+
|
|
1016
|
+
async function searchCommand(query: string | undefined, flags: Set<string>, rawArgs: string[] = []): Promise<void> {
|
|
1017
|
+
if (!query) {
|
|
1018
|
+
console.log("Usage: lattices search <query> [--quick | --terminal | --all | --sources=... | --after=... | --before=... | --json | --wid]");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Build search options from flags
|
|
1023
|
+
const opts: SearchOptions = {};
|
|
1024
|
+
|
|
1025
|
+
// Source selection: explicit --sources, or legacy --quick/--terminal, or default
|
|
1026
|
+
const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
|
|
1027
|
+
if (sourcesFlag) {
|
|
1028
|
+
opts.sources = sourcesFlag.slice("--sources=".length).split(",");
|
|
1029
|
+
} else if (flags.has("--all")) {
|
|
1030
|
+
opts.sources = ["all"];
|
|
1031
|
+
} else if (flags.has("--quick")) {
|
|
1032
|
+
opts.sources = ["titles", "apps", "sessions"];
|
|
1033
|
+
} else if (flags.has("--terminal")) {
|
|
1034
|
+
opts.sources = ["terminals"];
|
|
1035
|
+
}
|
|
1036
|
+
// else: omit → smart default on daemon side
|
|
1037
|
+
|
|
1038
|
+
// Time filters
|
|
1039
|
+
const afterFlag = rawArgs.find(a => a.startsWith("--after="));
|
|
1040
|
+
if (afterFlag) opts.after = afterFlag.slice("--after=".length);
|
|
1041
|
+
const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
|
|
1042
|
+
if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
|
|
1043
|
+
|
|
1044
|
+
// No-recency flag
|
|
1045
|
+
if (flags.has("--no-recency")) opts.recency = false;
|
|
1046
|
+
|
|
1047
|
+
const ranked = await search(query, opts);
|
|
1048
|
+
const jsonOut = flags.has("--json");
|
|
1049
|
+
const widOnly = flags.has("--wid");
|
|
1050
|
+
|
|
1051
|
+
if (jsonOut) {
|
|
1052
|
+
console.log(JSON.stringify(ranked.map(r => ({
|
|
1053
|
+
wid: r.window.wid, app: r.window.app, title: r.window.title,
|
|
1054
|
+
score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
|
|
1055
|
+
})), null, 2));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (widOnly) {
|
|
1060
|
+
for (const r of ranked) console.log(r.window.wid);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (!ranked.length) {
|
|
1065
|
+
console.log(`No results for "${query}"`);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
printResults(ranked);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ── place command ────────────────────────────────────────────────────
|
|
1073
|
+
|
|
1074
|
+
async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
|
|
1075
|
+
if (!query) {
|
|
1076
|
+
console.log("Usage: lattices place <query> [position]");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
const { daemonCall } = await getDaemonClient();
|
|
1081
|
+
const ranked = await deepSearch(query);
|
|
1082
|
+
|
|
1083
|
+
if (!ranked.length) {
|
|
1084
|
+
console.log(`No window matching "${query}"`);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const pos = tilePosition || "bottom-right";
|
|
1089
|
+
const win = ranked[0].window;
|
|
1090
|
+
await daemonCall("window.focus", { wid: win.wid });
|
|
1091
|
+
await daemonCall("intents.execute", {
|
|
1092
|
+
intent: "tile_window",
|
|
1093
|
+
slots: { position: pos, wid: win.wid }
|
|
1094
|
+
}, 3000);
|
|
1095
|
+
console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
|
|
1096
|
+
} catch (e: unknown) {
|
|
1097
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function sessionsCommand(jsonFlag: boolean): Promise<void> {
|
|
1102
|
+
try {
|
|
1103
|
+
const { daemonCall } = await getDaemonClient();
|
|
1104
|
+
const sessions = await daemonCall("tmux.sessions") as any[];
|
|
1105
|
+
if (jsonFlag) {
|
|
1106
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (!sessions.length) {
|
|
1110
|
+
console.log("No active sessions.");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
console.log(`Sessions (${sessions.length}):\n`);
|
|
1114
|
+
for (const s of sessions) {
|
|
1115
|
+
const windows = s.windowCount || s.windows || "?";
|
|
1116
|
+
console.log(` \x1b[1m${s.name}\x1b[0m (${windows} windows)`);
|
|
1117
|
+
}
|
|
1118
|
+
} catch {
|
|
1119
|
+
console.log("Daemon not running. Start with: lattices app");
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function voiceCommand(subcommand?: string, ...rest: string[]): Promise<void> {
|
|
1124
|
+
const { daemonCall } = await getDaemonClient();
|
|
1125
|
+
try {
|
|
1126
|
+
switch (subcommand) {
|
|
1127
|
+
case "status": {
|
|
1128
|
+
const status = await daemonCall("voice.status") as any;
|
|
1129
|
+
console.log(`Provider: ${status.provider}`);
|
|
1130
|
+
console.log(`Available: ${status.available}`);
|
|
1131
|
+
console.log(`Listening: ${status.listening}`);
|
|
1132
|
+
if (status.lastTranscript) console.log(`Last: "${status.lastTranscript}"`);
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
case "simulate":
|
|
1136
|
+
case "sim": {
|
|
1137
|
+
const text = rest.join(" ");
|
|
1138
|
+
if (!text) {
|
|
1139
|
+
console.log("Usage: lattices voice simulate <text>");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const execute = !rest.includes("--dry-run");
|
|
1143
|
+
const dryFlag = rest.includes("--dry-run");
|
|
1144
|
+
const cleanText = dryFlag ? rest.filter(r => r !== "--dry-run").join(" ") : text;
|
|
1145
|
+
const result = await daemonCall("voice.simulate", { text: cleanText, execute }, 15000) as any;
|
|
1146
|
+
if (!result.parsed) {
|
|
1147
|
+
console.log(`\x1b[33mNo match:\x1b[0m "${cleanText}"`);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const slots = Object.entries(result.slots || {}).map(([k,v]) => `${k}: ${v}`).join(", ");
|
|
1151
|
+
const conf = result.confidence ? ` (${(result.confidence * 100).toFixed(0)}%)` : "";
|
|
1152
|
+
console.log(`\x1b[36m${result.intent}\x1b[0m${slots ? ` ${slots}` : ""}${conf}`);
|
|
1153
|
+
if (result.executed) {
|
|
1154
|
+
console.log(`\x1b[32mExecuted\x1b[0m`);
|
|
1155
|
+
} else if (result.error) {
|
|
1156
|
+
console.log(`\x1b[31mError:\x1b[0m ${result.error}`);
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
case "intents": {
|
|
1161
|
+
const intents = await daemonCall("intents.list") as any[];
|
|
1162
|
+
for (const intent of intents) {
|
|
1163
|
+
const slots = intent.slots.map((s: any) => `${s.name}:${s.type}${s.required ? "*" : ""}`).join(", ");
|
|
1164
|
+
console.log(` \x1b[1m${intent.intent}\x1b[0m ${intent.description}`);
|
|
1165
|
+
if (slots) console.log(` slots: ${slots}`);
|
|
1166
|
+
console.log(` e.g. "${intent.examples[0]}"`);
|
|
1167
|
+
console.log();
|
|
1168
|
+
}
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
default:
|
|
1172
|
+
console.log("Usage: lattices voice <subcommand>\n");
|
|
1173
|
+
console.log(" status Show voice provider status");
|
|
1174
|
+
console.log(" simulate Parse and execute a voice command");
|
|
1175
|
+
console.log(" intents List all available intents");
|
|
1176
|
+
console.log("\nExamples:");
|
|
1177
|
+
console.log(' lattices voice simulate "tile this left"');
|
|
1178
|
+
console.log(' lattices voice simulate "focus chrome" --dry-run');
|
|
1179
|
+
}
|
|
1180
|
+
} catch (e: unknown) {
|
|
1181
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async function callCommand(method?: string, ...rest: string[]): Promise<void> {
|
|
1186
|
+
if (!method) {
|
|
1187
|
+
console.log("Usage: lattices call <method> [params-json]");
|
|
1188
|
+
console.log("\nExamples:");
|
|
1189
|
+
console.log(" lattices call daemon.status");
|
|
1190
|
+
console.log(" lattices call api.schema");
|
|
1191
|
+
console.log(' lattices call window.place \'{"session":"vox","placement":"left"}\'');
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
const { daemonCall } = await getDaemonClient();
|
|
1196
|
+
const params = rest[0] ? JSON.parse(rest[0]) : null;
|
|
1197
|
+
const result = await daemonCall(method, params, 15000);
|
|
1198
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1199
|
+
} catch (e: unknown) {
|
|
1200
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
762
1201
|
}
|
|
763
1202
|
}
|
|
764
1203
|
|
|
765
|
-
async function layerCommand(
|
|
1204
|
+
async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
|
|
766
1205
|
try {
|
|
767
1206
|
const { daemonCall } = await getDaemonClient();
|
|
768
|
-
|
|
769
|
-
|
|
1207
|
+
|
|
1208
|
+
// ── Subcommands ──
|
|
1209
|
+
if (sub === "create") {
|
|
1210
|
+
await layerCreateCommand(rest);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (sub === "snap") {
|
|
1214
|
+
await layerSnapCommand(rest[0]);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (sub === "session" || sub === "sessions") {
|
|
1218
|
+
await layerSessionCommand(rest[0]);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (sub === "clear") {
|
|
1222
|
+
await daemonCall("session.layers.clear");
|
|
1223
|
+
console.log("Cleared all session layers.");
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (sub === "delete" || sub === "rm") {
|
|
1227
|
+
if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
|
|
1228
|
+
await daemonCall("session.layers.delete", { name: rest[0] });
|
|
1229
|
+
console.log(`Deleted session layer "${rest[0]}".`);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ── List or switch (original behavior) ──
|
|
1234
|
+
if (sub === undefined || sub === null || sub === "") {
|
|
1235
|
+
const result = await daemonCall("layers.list") as any;
|
|
770
1236
|
if (!result.layers.length) {
|
|
771
1237
|
console.log("No layers configured.");
|
|
772
1238
|
return;
|
|
@@ -778,23 +1244,148 @@ async function layerCommand(index) {
|
|
|
778
1244
|
}
|
|
779
1245
|
return;
|
|
780
1246
|
}
|
|
781
|
-
const idx = parseInt(
|
|
1247
|
+
const idx = parseInt(sub, 10);
|
|
782
1248
|
if (!isNaN(idx)) {
|
|
783
|
-
await daemonCall("layer.
|
|
784
|
-
console.log(`
|
|
1249
|
+
await daemonCall("layer.activate", { index: idx, mode: "launch" });
|
|
1250
|
+
console.log(`Activated layer ${idx}`);
|
|
785
1251
|
} else {
|
|
786
|
-
await daemonCall("layer.
|
|
787
|
-
console.log(`
|
|
1252
|
+
await daemonCall("layer.activate", { name: sub, mode: "launch" });
|
|
1253
|
+
console.log(`Activated layer "${sub}"`);
|
|
788
1254
|
}
|
|
789
|
-
} catch (e) {
|
|
790
|
-
console.log(`Error: ${e.message}`);
|
|
1255
|
+
} catch (e: unknown) {
|
|
1256
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
791
1257
|
}
|
|
792
1258
|
}
|
|
793
1259
|
|
|
794
|
-
|
|
1260
|
+
// ── Layer create: build a session layer from window specs ────────────
|
|
1261
|
+
// Usage: lattices layer create <name> [wid:123 wid:456 ...]
|
|
1262
|
+
// lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
|
|
1263
|
+
async function layerCreateCommand(args: string[]): Promise<void> {
|
|
1264
|
+
const { daemonCall } = await getDaemonClient();
|
|
1265
|
+
const name = args[0];
|
|
1266
|
+
if (!name) {
|
|
1267
|
+
console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const jsonIdx = args.indexOf("--json");
|
|
1272
|
+
if (jsonIdx !== -1 && args[jsonIdx + 1]) {
|
|
1273
|
+
// JSON mode: parse window specs with tile positions
|
|
1274
|
+
const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
|
|
1275
|
+
wid?: number; app?: string; title?: string; tile?: string;
|
|
1276
|
+
}>;
|
|
1277
|
+
|
|
1278
|
+
// Collect wids, resolve app-based specs
|
|
1279
|
+
const windowIds: number[] = [];
|
|
1280
|
+
const windows: Array<{ app: string; contentHint?: string }> = [];
|
|
1281
|
+
const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
|
|
1282
|
+
|
|
1283
|
+
for (const spec of specs) {
|
|
1284
|
+
if (spec.wid) {
|
|
1285
|
+
windowIds.push(spec.wid);
|
|
1286
|
+
if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
|
|
1287
|
+
} else if (spec.app) {
|
|
1288
|
+
windows.push({ app: spec.app, contentHint: spec.title });
|
|
1289
|
+
if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const result = await daemonCall("session.layers.create", {
|
|
1294
|
+
name,
|
|
1295
|
+
...(windowIds.length ? { windowIds } : {}),
|
|
1296
|
+
...(windows.length ? { windows } : {}),
|
|
1297
|
+
}) as any;
|
|
1298
|
+
|
|
1299
|
+
console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
|
|
1300
|
+
|
|
1301
|
+
// Apply tile positions
|
|
1302
|
+
for (const t of tiles) {
|
|
1303
|
+
try {
|
|
1304
|
+
await daemonCall("window.place", {
|
|
1305
|
+
...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
|
|
1306
|
+
placement: t.tile,
|
|
1307
|
+
});
|
|
1308
|
+
} catch { /* window may not be resolved yet */ }
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Simple wid mode: lattices layer create <name> wid:123 wid:456
|
|
1316
|
+
const wids = args.slice(1)
|
|
1317
|
+
.filter(a => a.startsWith("wid:"))
|
|
1318
|
+
.map(a => parseInt(a.slice(4), 10))
|
|
1319
|
+
.filter(n => !isNaN(n));
|
|
1320
|
+
|
|
1321
|
+
const result = await daemonCall("session.layers.create", {
|
|
1322
|
+
name,
|
|
1323
|
+
...(wids.length ? { windowIds: wids } : {}),
|
|
1324
|
+
}) as any;
|
|
1325
|
+
|
|
1326
|
+
console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ── Layer snap: snapshot current visible windows into a session layer ─
|
|
1330
|
+
async function layerSnapCommand(name?: string): Promise<void> {
|
|
1331
|
+
const { daemonCall } = await getDaemonClient();
|
|
1332
|
+
const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
|
|
1333
|
+
|
|
1334
|
+
// Get all current windows
|
|
1335
|
+
const windows = await daemonCall("windows.list") as any[];
|
|
1336
|
+
const visibleWids = windows
|
|
1337
|
+
.filter((w: any) => !w.isMinimized && w.app !== "lattices")
|
|
1338
|
+
.map((w: any) => w.wid);
|
|
1339
|
+
|
|
1340
|
+
if (!visibleWids.length) {
|
|
1341
|
+
console.log("No visible windows to snapshot.");
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
await daemonCall("session.layers.create", {
|
|
1346
|
+
name: layerName,
|
|
1347
|
+
windowIds: visibleWids,
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// ── Layer session: list or switch session layers ─────────────────────
|
|
1354
|
+
async function layerSessionCommand(nameOrIndex?: string): Promise<void> {
|
|
1355
|
+
const { daemonCall } = await getDaemonClient();
|
|
1356
|
+
const result = await daemonCall("session.layers.list") as any;
|
|
1357
|
+
|
|
1358
|
+
if (!nameOrIndex) {
|
|
1359
|
+
// List session layers
|
|
1360
|
+
if (!result.layers.length) {
|
|
1361
|
+
console.log("No session layers. Create one with: lattices layer create <name>");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
console.log("Session layers:\n");
|
|
1365
|
+
for (let i = 0; i < result.layers.length; i++) {
|
|
1366
|
+
const l = result.layers[i];
|
|
1367
|
+
const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
|
|
1368
|
+
const winCount = l.windows?.length || 0;
|
|
1369
|
+
console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
|
|
1370
|
+
}
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Switch by index or name
|
|
1375
|
+
const idx = parseInt(nameOrIndex, 10);
|
|
1376
|
+
if (!isNaN(idx)) {
|
|
1377
|
+
await daemonCall("session.layers.switch", { index: idx });
|
|
1378
|
+
console.log(`Switched to session layer ${idx}.`);
|
|
1379
|
+
} else {
|
|
1380
|
+
await daemonCall("session.layers.switch", { name: nameOrIndex });
|
|
1381
|
+
console.log(`Switched to session layer "${nameOrIndex}".`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async function diagCommand(limit?: string): Promise<void> {
|
|
795
1386
|
try {
|
|
796
1387
|
const { daemonCall } = await getDaemonClient();
|
|
797
|
-
const result = await daemonCall("diagnostics.list", { limit: parseInt(limit, 10) || 40 });
|
|
1388
|
+
const result = await daemonCall("diagnostics.list", { limit: parseInt(limit || "", 10) || 40 }) as any;
|
|
798
1389
|
if (!result.entries || !result.entries.length) {
|
|
799
1390
|
console.log("No diagnostic entries.");
|
|
800
1391
|
return;
|
|
@@ -805,26 +1396,26 @@ async function diagCommand(limit) {
|
|
|
805
1396
|
entry.level === "error" ? "\x1b[31m✗\x1b[0m" : "›";
|
|
806
1397
|
console.log(` \x1b[90m${entry.time}\x1b[0m ${icon} ${entry.message}`);
|
|
807
1398
|
}
|
|
808
|
-
} catch (e) {
|
|
809
|
-
console.log(`Error: ${e.message}`);
|
|
1399
|
+
} catch (e: unknown) {
|
|
1400
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
810
1401
|
}
|
|
811
1402
|
}
|
|
812
1403
|
|
|
813
|
-
async function distributeCommand() {
|
|
1404
|
+
async function distributeCommand(): Promise<void> {
|
|
814
1405
|
try {
|
|
815
1406
|
const { daemonCall } = await getDaemonClient();
|
|
816
|
-
await daemonCall("
|
|
1407
|
+
await daemonCall("space.optimize", { scope: "visible", strategy: "balanced" });
|
|
817
1408
|
console.log("Distributed visible windows into grid");
|
|
818
1409
|
} catch {
|
|
819
1410
|
console.log("Daemon not running. Start with: lattices app");
|
|
820
1411
|
}
|
|
821
1412
|
}
|
|
822
1413
|
|
|
823
|
-
async function daemonLsCommand() {
|
|
1414
|
+
async function daemonLsCommand(): Promise<boolean> {
|
|
824
1415
|
try {
|
|
825
1416
|
const { daemonCall, isDaemonRunning } = await getDaemonClient();
|
|
826
1417
|
if (!(await isDaemonRunning())) return false;
|
|
827
|
-
const sessions = await daemonCall("tmux.sessions");
|
|
1418
|
+
const sessions = await daemonCall("tmux.sessions") as any[];
|
|
828
1419
|
if (!sessions.length) {
|
|
829
1420
|
console.log("No active tmux sessions.");
|
|
830
1421
|
return true;
|
|
@@ -832,7 +1423,7 @@ async function daemonLsCommand() {
|
|
|
832
1423
|
|
|
833
1424
|
// Annotate sessions with workspace group info
|
|
834
1425
|
const ws = readWorkspaceConfig();
|
|
835
|
-
const sessionGroupMap = new Map();
|
|
1426
|
+
const sessionGroupMap = new Map<string, { group: string; tab: string }>();
|
|
836
1427
|
if (ws?.groups) {
|
|
837
1428
|
for (const g of ws.groups) {
|
|
838
1429
|
for (const tab of g.tabs || []) {
|
|
@@ -858,14 +1449,14 @@ async function daemonLsCommand() {
|
|
|
858
1449
|
}
|
|
859
1450
|
}
|
|
860
1451
|
|
|
861
|
-
async function daemonStatusInventory() {
|
|
1452
|
+
async function daemonStatusInventory(): Promise<boolean> {
|
|
862
1453
|
try {
|
|
863
1454
|
const { daemonCall, isDaemonRunning } = await getDaemonClient();
|
|
864
1455
|
if (!(await isDaemonRunning())) return false;
|
|
865
|
-
const inv = await daemonCall("tmux.inventory");
|
|
1456
|
+
const inv = await daemonCall("tmux.inventory") as any;
|
|
866
1457
|
|
|
867
1458
|
// Build managed session name set
|
|
868
|
-
const managed = new Map();
|
|
1459
|
+
const managed = new Map<string, string>();
|
|
869
1460
|
const ws = readWorkspaceConfig();
|
|
870
1461
|
if (ws?.groups) {
|
|
871
1462
|
for (const g of ws.groups) {
|
|
@@ -879,7 +1470,7 @@ async function daemonStatusInventory() {
|
|
|
879
1470
|
for (const s of inv.all) {
|
|
880
1471
|
if (!managed.has(s.name)) {
|
|
881
1472
|
// Check if it matches a scanned project (via daemon)
|
|
882
|
-
const projects = await daemonCall("projects.list");
|
|
1473
|
+
const projects = await daemonCall("projects.list") as any[];
|
|
883
1474
|
for (const p of projects) {
|
|
884
1475
|
managed.set(p.sessionName, p.name);
|
|
885
1476
|
}
|
|
@@ -887,7 +1478,7 @@ async function daemonStatusInventory() {
|
|
|
887
1478
|
}
|
|
888
1479
|
}
|
|
889
1480
|
|
|
890
|
-
const managedSessions = inv.all.filter((s) => managed.has(s.name));
|
|
1481
|
+
const managedSessions = inv.all.filter((s: any) => managed.has(s.name));
|
|
891
1482
|
const orphanSessions = inv.orphans;
|
|
892
1483
|
|
|
893
1484
|
if (managedSessions.length > 0) {
|
|
@@ -926,14 +1517,14 @@ async function daemonStatusInventory() {
|
|
|
926
1517
|
|
|
927
1518
|
// ── OCR commands ──────────────────────────────────────────────────────
|
|
928
1519
|
|
|
929
|
-
async function scanCommand(sub, ...rest) {
|
|
1520
|
+
async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
|
|
930
1521
|
const { daemonCall } = await getDaemonClient();
|
|
931
1522
|
|
|
932
1523
|
if (!sub || sub === "snapshot" || sub === "ls" || sub === "--full" || sub === "-f" || sub === "--json") {
|
|
933
1524
|
const full = sub === "--full" || sub === "-f" || rest.includes("--full") || rest.includes("-f");
|
|
934
1525
|
const json = sub === "--json" || rest.includes("--json");
|
|
935
1526
|
try {
|
|
936
|
-
const results = await daemonCall("ocr.snapshot", null, 5000);
|
|
1527
|
+
const results = await daemonCall("ocr.snapshot", null, 5000) as any[];
|
|
937
1528
|
if (!results.length) {
|
|
938
1529
|
console.log("No scan results yet. The first scan runs ~60s after launch.");
|
|
939
1530
|
return;
|
|
@@ -957,7 +1548,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
957
1548
|
}
|
|
958
1549
|
} else {
|
|
959
1550
|
const maxPreview = 5;
|
|
960
|
-
const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
1551
|
+
const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
961
1552
|
for (const line of preview) {
|
|
962
1553
|
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
963
1554
|
}
|
|
@@ -983,7 +1574,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
983
1574
|
return;
|
|
984
1575
|
}
|
|
985
1576
|
try {
|
|
986
|
-
const results = await daemonCall("ocr.search", { query }, 5000);
|
|
1577
|
+
const results = await daemonCall("ocr.search", { query }, 5000) as any[];
|
|
987
1578
|
if (!results.length) {
|
|
988
1579
|
console.log(`No matches for "${query}".`);
|
|
989
1580
|
return;
|
|
@@ -997,8 +1588,8 @@ async function scanCommand(sub, ...rest) {
|
|
|
997
1588
|
console.log(` ${snippet}`);
|
|
998
1589
|
console.log();
|
|
999
1590
|
}
|
|
1000
|
-
} catch (e) {
|
|
1001
|
-
console.log(`Error: ${e.message}`);
|
|
1591
|
+
} catch (e: unknown) {
|
|
1592
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1002
1593
|
}
|
|
1003
1594
|
return;
|
|
1004
1595
|
}
|
|
@@ -1006,9 +1597,9 @@ async function scanCommand(sub, ...rest) {
|
|
|
1006
1597
|
if (sub === "recent" || sub === "log") {
|
|
1007
1598
|
const full = rest.includes("--full") || rest.includes("-f");
|
|
1008
1599
|
const numArg = rest.find(a => !a.startsWith("-"));
|
|
1009
|
-
const limit = parseInt(numArg, 10) || 20;
|
|
1600
|
+
const limit = parseInt(numArg || "", 10) || 20;
|
|
1010
1601
|
try {
|
|
1011
|
-
const results = await daemonCall("ocr.recent", { limit }, 5000);
|
|
1602
|
+
const results = await daemonCall("ocr.recent", { limit }, 5000) as any[];
|
|
1012
1603
|
if (!results.length) {
|
|
1013
1604
|
console.log("No history yet. The first scan runs ~60s after launch.");
|
|
1014
1605
|
return;
|
|
@@ -1026,7 +1617,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
1026
1617
|
}
|
|
1027
1618
|
} else {
|
|
1028
1619
|
const maxPreview = 5;
|
|
1029
|
-
const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
1620
|
+
const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
1030
1621
|
for (const line of preview) {
|
|
1031
1622
|
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
1032
1623
|
}
|
|
@@ -1047,8 +1638,8 @@ async function scanCommand(sub, ...rest) {
|
|
|
1047
1638
|
console.log("Triggering deep scan (Vision OCR)...");
|
|
1048
1639
|
await daemonCall("ocr.scan", null, 30000);
|
|
1049
1640
|
console.log("Done.");
|
|
1050
|
-
} catch (e) {
|
|
1051
|
-
console.log(`Error: ${e.message}`);
|
|
1641
|
+
} catch (e: unknown) {
|
|
1642
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1052
1643
|
}
|
|
1053
1644
|
return;
|
|
1054
1645
|
}
|
|
@@ -1060,7 +1651,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
1060
1651
|
return;
|
|
1061
1652
|
}
|
|
1062
1653
|
try {
|
|
1063
|
-
const results = await daemonCall("ocr.history", { wid }, 5000);
|
|
1654
|
+
const results = await daemonCall("ocr.history", { wid }, 5000) as any[];
|
|
1064
1655
|
if (!results.length) {
|
|
1065
1656
|
console.log(`No history for wid:${wid}.`);
|
|
1066
1657
|
return;
|
|
@@ -1070,15 +1661,15 @@ async function scanCommand(sub, ...rest) {
|
|
|
1070
1661
|
const ts = new Date(r.timestamp * 1000).toLocaleTimeString();
|
|
1071
1662
|
const src = r.source === "accessibility" ? "\x1b[33mAX\x1b[0m" : "\x1b[35mOCR\x1b[0m";
|
|
1072
1663
|
const lines = (r.fullText || "").split("\n").filter(Boolean);
|
|
1073
|
-
const preview = lines.slice(0, 2).map(l => l.length > 80 ? l.slice(0, 77) + "..." : l);
|
|
1664
|
+
const preview = lines.slice(0, 2).map((l: string) => l.length > 80 ? l.slice(0, 77) + "..." : l);
|
|
1074
1665
|
console.log(` \x1b[90m${ts}\x1b[0m ${src} \x1b[1m${r.app}\x1b[0m — "${r.title}"`);
|
|
1075
1666
|
for (const line of preview) {
|
|
1076
1667
|
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
1077
1668
|
}
|
|
1078
1669
|
console.log();
|
|
1079
1670
|
}
|
|
1080
|
-
} catch (e) {
|
|
1081
|
-
console.log(`Error: ${e.message}`);
|
|
1671
|
+
} catch (e: unknown) {
|
|
1672
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1082
1673
|
}
|
|
1083
1674
|
return;
|
|
1084
1675
|
}
|
|
@@ -1097,7 +1688,7 @@ Usage:
|
|
|
1097
1688
|
`);
|
|
1098
1689
|
}
|
|
1099
1690
|
|
|
1100
|
-
function printUsage() {
|
|
1691
|
+
function printUsage(): void {
|
|
1101
1692
|
console.log(`lattices — Claude Code + dev server in tmux
|
|
1102
1693
|
|
|
1103
1694
|
Usage:
|
|
@@ -1111,17 +1702,38 @@ Usage:
|
|
|
1111
1702
|
lattices group [id] List tab groups or launch/attach a group
|
|
1112
1703
|
lattices groups List all tab groups with status
|
|
1113
1704
|
lattices tab <group> [tab] Switch tab within a group (by label or index)
|
|
1705
|
+
lattices search <query> Search windows by title, app, session, OCR
|
|
1706
|
+
lattices search <q> --deep Deep search: index + live terminal inspection
|
|
1707
|
+
lattices search <q> --wid Print matching window IDs only (pipeable)
|
|
1708
|
+
lattices search <q> --json JSON output
|
|
1709
|
+
lattices place <query> [pos] Deep search + focus + tile (default: bottom-right)
|
|
1710
|
+
lattices focus <session> Raise a session's window
|
|
1114
1711
|
lattices windows [--json] List all desktop windows (daemon required)
|
|
1115
|
-
lattices
|
|
1712
|
+
lattices sessions [--json] List active tmux sessions via daemon
|
|
1116
1713
|
lattices tile <position> Tile the frontmost window (left, right, top, etc.)
|
|
1117
1714
|
lattices distribute Smart-grid all visible windows (daemon required)
|
|
1118
1715
|
lattices layer [name|index] List layers or switch by name/index (daemon required)
|
|
1716
|
+
lattices layer create <name> [wid:N ...] [--json '<specs>'] Create a session layer
|
|
1717
|
+
lattices layer snap [name] Snapshot visible windows into a session layer
|
|
1718
|
+
lattices layer session [n] List or switch session layers (runtime, no restart)
|
|
1719
|
+
lattices layer delete <name> Delete a session layer
|
|
1720
|
+
lattices layer clear Clear all session layers
|
|
1721
|
+
lattices voice status Voice provider status
|
|
1722
|
+
lattices voice simulate <t> Parse and execute a voice command
|
|
1723
|
+
lattices voice intents List all available intents
|
|
1724
|
+
lattices call <method> [p] Raw daemon API call (params as JSON)
|
|
1119
1725
|
lattices scan Show text from all visible windows
|
|
1120
1726
|
lattices scan --full Full text dump
|
|
1121
1727
|
lattices scan search <q> Full-text search across scanned windows
|
|
1122
1728
|
lattices scan recent [n] Show recent scans chronologically
|
|
1123
1729
|
lattices scan deep Trigger a deep Vision OCR scan
|
|
1124
1730
|
lattices scan history <wid> Scan timeline for a specific window
|
|
1731
|
+
lattices dev Run dev server (auto-detected)
|
|
1732
|
+
lattices dev build Build the project (swift/node/rust/go/make)
|
|
1733
|
+
lattices dev restart Build + restart (swift app) or just build
|
|
1734
|
+
lattices dev type Print detected project type
|
|
1735
|
+
lattices mouse Find mouse — sonar pulse at cursor position
|
|
1736
|
+
lattices mouse summon Summon mouse to screen center
|
|
1125
1737
|
lattices daemon status Show daemon status
|
|
1126
1738
|
lattices diag [limit] Show diagnostic log entries
|
|
1127
1739
|
lattices app Launch the menu bar companion app
|
|
@@ -1172,7 +1784,7 @@ Layouts:
|
|
|
1172
1784
|
`);
|
|
1173
1785
|
}
|
|
1174
1786
|
|
|
1175
|
-
function initConfig() {
|
|
1787
|
+
function initConfig(): void {
|
|
1176
1788
|
const dir = process.cwd();
|
|
1177
1789
|
const configPath = resolve(dir, ".lattices.json");
|
|
1178
1790
|
|
|
@@ -1192,7 +1804,7 @@ function initConfig() {
|
|
|
1192
1804
|
console.log(JSON.stringify(config, null, 2));
|
|
1193
1805
|
}
|
|
1194
1806
|
|
|
1195
|
-
function listSessions() {
|
|
1807
|
+
function listSessions(): void {
|
|
1196
1808
|
const out = runQuiet(
|
|
1197
1809
|
"tmux list-sessions -F '#{session_name} (#{session_windows} windows, created #{session_created_string})'"
|
|
1198
1810
|
);
|
|
@@ -1203,7 +1815,7 @@ function listSessions() {
|
|
|
1203
1815
|
|
|
1204
1816
|
// Annotate sessions that belong to tab groups
|
|
1205
1817
|
const ws = readWorkspaceConfig();
|
|
1206
|
-
const sessionGroupMap = new Map();
|
|
1818
|
+
const sessionGroupMap = new Map<string, { group: string; tab: string }>();
|
|
1207
1819
|
if (ws?.groups) {
|
|
1208
1820
|
for (const g of ws.groups) {
|
|
1209
1821
|
for (const tab of g.tabs || []) {
|
|
@@ -1216,7 +1828,7 @@ function listSessions() {
|
|
|
1216
1828
|
}
|
|
1217
1829
|
}
|
|
1218
1830
|
|
|
1219
|
-
const lines = out.split("\n").map((line) => {
|
|
1831
|
+
const lines = out.split("\n").map((line: string) => {
|
|
1220
1832
|
const sessionName = line.split(" ")[0];
|
|
1221
1833
|
const info = sessionGroupMap.get(sessionName);
|
|
1222
1834
|
return info
|
|
@@ -1228,7 +1840,7 @@ function listSessions() {
|
|
|
1228
1840
|
console.log(lines.join("\n"));
|
|
1229
1841
|
}
|
|
1230
1842
|
|
|
1231
|
-
function killSession(name) {
|
|
1843
|
+
function killSession(name?: string): void {
|
|
1232
1844
|
if (!name) name = toSessionName(process.cwd());
|
|
1233
1845
|
if (!sessionExists(name)) {
|
|
1234
1846
|
console.log(`No session "${name}".`);
|
|
@@ -1240,7 +1852,14 @@ function killSession(name) {
|
|
|
1240
1852
|
|
|
1241
1853
|
// ── Window tiling ────────────────────────────────────────────────────
|
|
1242
1854
|
|
|
1243
|
-
|
|
1855
|
+
interface ScreenBounds {
|
|
1856
|
+
x: number;
|
|
1857
|
+
y: number;
|
|
1858
|
+
w: number;
|
|
1859
|
+
h: number;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function getScreenBounds(): ScreenBounds {
|
|
1244
1863
|
// Get the visible area (excludes menu bar and dock) in AppleScript coordinates (top-left origin)
|
|
1245
1864
|
const script = `
|
|
1246
1865
|
tell application "Finder"
|
|
@@ -1255,7 +1874,7 @@ function getScreenBounds() {
|
|
|
1255
1874
|
}
|
|
1256
1875
|
|
|
1257
1876
|
// Presets return AppleScript bounds: [left, top, right, bottom] within the visible area
|
|
1258
|
-
const tilePresets = {
|
|
1877
|
+
const tilePresets: Record<string, (s: ScreenBounds) => number[]> = {
|
|
1259
1878
|
"left": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
|
|
1260
1879
|
"left-half": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
|
|
1261
1880
|
"right": (s) => [s.x + s.w / 2, s.y, s.x + s.w, s.y + s.h],
|
|
@@ -1282,7 +1901,7 @@ const tilePresets = {
|
|
|
1282
1901
|
"right-third": (s) => [s.x + Math.round(s.w * 0.667), s.y, s.x + s.w, s.y + s.h],
|
|
1283
1902
|
};
|
|
1284
1903
|
|
|
1285
|
-
function tileWindow(position) {
|
|
1904
|
+
function tileWindow(position: string): void {
|
|
1286
1905
|
const preset = tilePresets[position];
|
|
1287
1906
|
if (!preset) {
|
|
1288
1907
|
console.log(`Unknown position: ${position}`);
|
|
@@ -1302,7 +1921,7 @@ function tileWindow(position) {
|
|
|
1302
1921
|
console.log(`Tiled → ${position}`);
|
|
1303
1922
|
}
|
|
1304
1923
|
|
|
1305
|
-
function createOrAttach() {
|
|
1924
|
+
function createOrAttach(): void {
|
|
1306
1925
|
const dir = process.cwd();
|
|
1307
1926
|
const name = toSessionName(dir);
|
|
1308
1927
|
|
|
@@ -1323,7 +1942,7 @@ function createOrAttach() {
|
|
|
1323
1942
|
attach(name);
|
|
1324
1943
|
}
|
|
1325
1944
|
|
|
1326
|
-
function attach(name) {
|
|
1945
|
+
function attach(name: string): void {
|
|
1327
1946
|
if (isInsideTmux()) {
|
|
1328
1947
|
execSync(`tmux switch-client -t "${name}"`, { stdio: "inherit" });
|
|
1329
1948
|
} else {
|
|
@@ -1333,7 +1952,7 @@ function attach(name) {
|
|
|
1333
1952
|
|
|
1334
1953
|
// ── Status / Inventory ───────────────────────────────────────────────
|
|
1335
1954
|
|
|
1336
|
-
function statusInventory() {
|
|
1955
|
+
function statusInventory(): void {
|
|
1337
1956
|
// Query all tmux sessions
|
|
1338
1957
|
const sessionsRaw = runQuiet(
|
|
1339
1958
|
'tmux list-sessions -F "#{session_name}\t#{session_windows}\t#{session_attached}"'
|
|
@@ -1349,17 +1968,17 @@ function statusInventory() {
|
|
|
1349
1968
|
);
|
|
1350
1969
|
|
|
1351
1970
|
// Parse panes grouped by session
|
|
1352
|
-
const panesBySession = new Map();
|
|
1971
|
+
const panesBySession = new Map<string, { title: string; cmd: string }[]>();
|
|
1353
1972
|
if (panesRaw) {
|
|
1354
1973
|
for (const line of panesRaw.split("\n").filter(Boolean)) {
|
|
1355
1974
|
const [sess, title, cmd] = line.split("\t");
|
|
1356
1975
|
if (!panesBySession.has(sess)) panesBySession.set(sess, []);
|
|
1357
|
-
panesBySession.get(sess)
|
|
1976
|
+
panesBySession.get(sess)!.push({ title, cmd });
|
|
1358
1977
|
}
|
|
1359
1978
|
}
|
|
1360
1979
|
|
|
1361
1980
|
// Build managed session name set
|
|
1362
|
-
const managed = new Map(); // name -> label
|
|
1981
|
+
const managed = new Map<string, string>(); // name -> label
|
|
1363
1982
|
|
|
1364
1983
|
// From workspace groups
|
|
1365
1984
|
const ws = readWorkspaceConfig();
|
|
@@ -1391,7 +2010,7 @@ function statusInventory() {
|
|
|
1391
2010
|
}
|
|
1392
2011
|
|
|
1393
2012
|
// Parse sessions and classify
|
|
1394
|
-
const sessions = sessionsRaw.split("\n").filter(Boolean).map((line) => {
|
|
2013
|
+
const sessions = sessionsRaw.split("\n").filter(Boolean).map((line: string) => {
|
|
1395
2014
|
const [name, windows, attached] = line.split("\t");
|
|
1396
2015
|
return { name, windows: parseInt(windows) || 1, attached: attached !== "0" };
|
|
1397
2016
|
});
|
|
@@ -1437,10 +2056,7 @@ function statusInventory() {
|
|
|
1437
2056
|
|
|
1438
2057
|
// ── Main ─────────────────────────────────────────────────────────────
|
|
1439
2058
|
|
|
1440
|
-
|
|
1441
|
-
console.error("tmux is not installed. Install with: brew install tmux");
|
|
1442
|
-
process.exit(1);
|
|
1443
|
-
}
|
|
2059
|
+
requireTmux(command);
|
|
1444
2060
|
|
|
1445
2061
|
switch (command) {
|
|
1446
2062
|
case "init":
|
|
@@ -1509,12 +2125,28 @@ switch (command) {
|
|
|
1509
2125
|
console.log(" lattices window map [--json] Show all layer tags");
|
|
1510
2126
|
}
|
|
1511
2127
|
break;
|
|
2128
|
+
case "search":
|
|
2129
|
+
case "s":
|
|
2130
|
+
await searchCommand(args[1], new Set(args.slice(2)));
|
|
2131
|
+
break;
|
|
1512
2132
|
case "focus":
|
|
1513
2133
|
await focusCommand(args[1]);
|
|
1514
2134
|
break;
|
|
2135
|
+
case "place":
|
|
2136
|
+
await placeCommand(args[1], args[2]);
|
|
2137
|
+
break;
|
|
2138
|
+
case "sessions":
|
|
2139
|
+
await sessionsCommand(args[1] === "--json");
|
|
2140
|
+
break;
|
|
2141
|
+
case "voice":
|
|
2142
|
+
await voiceCommand(args[1], ...args.slice(2));
|
|
2143
|
+
break;
|
|
2144
|
+
case "call":
|
|
2145
|
+
await callCommand(args[1], ...args.slice(2));
|
|
2146
|
+
break;
|
|
1515
2147
|
case "layer":
|
|
1516
2148
|
case "layers":
|
|
1517
|
-
await layerCommand(args[1]);
|
|
2149
|
+
await layerCommand(args[1], ...args.slice(2));
|
|
1518
2150
|
break;
|
|
1519
2151
|
case "diag":
|
|
1520
2152
|
case "diagnostics":
|
|
@@ -1524,6 +2156,9 @@ switch (command) {
|
|
|
1524
2156
|
case "ocr":
|
|
1525
2157
|
await scanCommand(args[1], ...args.slice(2));
|
|
1526
2158
|
break;
|
|
2159
|
+
case "mouse":
|
|
2160
|
+
await mouseCommand(args[1]);
|
|
2161
|
+
break;
|
|
1527
2162
|
case "daemon":
|
|
1528
2163
|
if (args[1] === "status") {
|
|
1529
2164
|
await daemonStatusCommand();
|
|
@@ -1531,13 +2166,15 @@ switch (command) {
|
|
|
1531
2166
|
console.log("Usage: lattices daemon status");
|
|
1532
2167
|
}
|
|
1533
2168
|
break;
|
|
2169
|
+
case "dev":
|
|
2170
|
+
await devCommand(args[1], ...args.slice(2));
|
|
2171
|
+
break;
|
|
1534
2172
|
case "app": {
|
|
1535
2173
|
// Forward to lattices-app script
|
|
1536
2174
|
const { execFileSync } = await import("node:child_process");
|
|
1537
|
-
const
|
|
1538
|
-
const appScript = resolve(__dirname2, "lattices-app.js");
|
|
2175
|
+
const appScript = resolve(import.meta.dir, "lattices-app.ts");
|
|
1539
2176
|
try {
|
|
1540
|
-
execFileSync("
|
|
2177
|
+
execFileSync("bun", [appScript, ...args.slice(1)], { stdio: "inherit" });
|
|
1541
2178
|
} catch { /* exit code forwarded */ }
|
|
1542
2179
|
break;
|
|
1543
2180
|
}
|