@lattices/cli 0.3.0 → 0.4.0
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/Package.swift +8 -1
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +44 -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 +164 -5
- 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 +733 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +45 -9
- package/app/Sources/IntentEngine.swift +925 -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 +1235 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- 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 +1 -1
- 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/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 +731 -0
- package/bin/{lattices-app.js → lattices-app.ts} +67 -32
- package/bin/lattices-dev +160 -0
- package/bin/{lattices.js → lattices.ts} +600 -137
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -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 +21 -10
- package/bin/client.js +0 -4
|
@@ -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", "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,10 @@ function restartPane(target) {
|
|
|
665
831
|
|
|
666
832
|
// ── Daemon-aware commands ────────────────────────────────────────────
|
|
667
833
|
|
|
668
|
-
async function daemonStatusCommand() {
|
|
834
|
+
async function daemonStatusCommand(): Promise<void> {
|
|
669
835
|
try {
|
|
670
836
|
const { daemonCall } = await getDaemonClient();
|
|
671
|
-
const status = await daemonCall("daemon.status");
|
|
837
|
+
const status = await daemonCall("daemon.status") as any;
|
|
672
838
|
const uptime = Math.round(status.uptime);
|
|
673
839
|
const h = Math.floor(uptime / 3600);
|
|
674
840
|
const m = Math.floor((uptime % 3600) / 60);
|
|
@@ -685,10 +851,10 @@ async function daemonStatusCommand() {
|
|
|
685
851
|
}
|
|
686
852
|
}
|
|
687
853
|
|
|
688
|
-
async function windowsCommand(jsonFlag) {
|
|
854
|
+
async function windowsCommand(jsonFlag: boolean): Promise<void> {
|
|
689
855
|
try {
|
|
690
856
|
const { daemonCall } = await getDaemonClient();
|
|
691
|
-
const windows = await daemonCall("windows.list");
|
|
857
|
+
const windows = await daemonCall("windows.list") as any[];
|
|
692
858
|
if (jsonFlag) {
|
|
693
859
|
console.log(JSON.stringify(windows, null, 2));
|
|
694
860
|
return;
|
|
@@ -712,7 +878,7 @@ async function windowsCommand(jsonFlag) {
|
|
|
712
878
|
}
|
|
713
879
|
}
|
|
714
880
|
|
|
715
|
-
async function windowAssignCommand(wid, layerId) {
|
|
881
|
+
async function windowAssignCommand(wid?: string, layerId?: string): Promise<void> {
|
|
716
882
|
if (!wid || !layerId) {
|
|
717
883
|
console.log("Usage: lattices window assign <wid> <layer-id>");
|
|
718
884
|
return;
|
|
@@ -721,15 +887,15 @@ async function windowAssignCommand(wid, layerId) {
|
|
|
721
887
|
const { daemonCall } = await getDaemonClient();
|
|
722
888
|
await daemonCall("window.assignLayer", { wid: parseInt(wid), layer: layerId });
|
|
723
889
|
console.log(`Tagged wid:${wid} → layer:${layerId}`);
|
|
724
|
-
} catch (e) {
|
|
725
|
-
console.log(`Error: ${e.message}`);
|
|
890
|
+
} catch (e: unknown) {
|
|
891
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
726
892
|
}
|
|
727
893
|
}
|
|
728
894
|
|
|
729
|
-
async function windowLayerMapCommand(jsonFlag) {
|
|
895
|
+
async function windowLayerMapCommand(jsonFlag: boolean): Promise<void> {
|
|
730
896
|
try {
|
|
731
897
|
const { daemonCall } = await getDaemonClient();
|
|
732
|
-
const map = await daemonCall("window.layerMap");
|
|
898
|
+
const map = await daemonCall("window.layerMap") as any;
|
|
733
899
|
if (jsonFlag) {
|
|
734
900
|
console.log(JSON.stringify(map, null, 2));
|
|
735
901
|
return;
|
|
@@ -748,7 +914,7 @@ async function windowLayerMapCommand(jsonFlag) {
|
|
|
748
914
|
}
|
|
749
915
|
}
|
|
750
916
|
|
|
751
|
-
async function focusCommand(session) {
|
|
917
|
+
async function focusCommand(session?: string): Promise<void> {
|
|
752
918
|
if (!session) {
|
|
753
919
|
console.log("Usage: lattices focus <session-name>");
|
|
754
920
|
return;
|
|
@@ -757,16 +923,277 @@ async function focusCommand(session) {
|
|
|
757
923
|
const { daemonCall } = await getDaemonClient();
|
|
758
924
|
await daemonCall("window.focus", { session });
|
|
759
925
|
console.log(`Focused: ${session}`);
|
|
760
|
-
} catch (e) {
|
|
761
|
-
console.log(`Error: ${e.message}`);
|
|
926
|
+
} catch (e: unknown) {
|
|
927
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Search ───────────────────────────────────────────────────────────
|
|
932
|
+
|
|
933
|
+
interface SearchResult {
|
|
934
|
+
score: number;
|
|
935
|
+
window: any;
|
|
936
|
+
tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
|
|
937
|
+
reasons: string[];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function relativeTime(iso: string): string {
|
|
941
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
942
|
+
const s = Math.floor(ms / 1000);
|
|
943
|
+
if (s < 60) return "just now";
|
|
944
|
+
const m = Math.floor(s / 60);
|
|
945
|
+
if (m < 60) return `${m}m ago`;
|
|
946
|
+
const h = Math.floor(m / 60);
|
|
947
|
+
if (h < 24) return `${h}h ago`;
|
|
948
|
+
const d = Math.floor(h / 24);
|
|
949
|
+
return `${d}d ago`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Unified search via lattices.search daemon API.
|
|
953
|
+
// All search surfaces should go through this one function.
|
|
954
|
+
interface SearchOptions {
|
|
955
|
+
sources?: string[]; // e.g. ["titles", "apps", "cwd", "ocr"] — omit for smart default
|
|
956
|
+
after?: string; // ISO8601 — only windows interacted after this time
|
|
957
|
+
before?: string; // ISO8601 — only windows interacted before this time
|
|
958
|
+
recency?: boolean; // boost recently-focused windows (default true)
|
|
959
|
+
mode?: string; // legacy compat: "quick", "complete", "terminal"
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
|
|
963
|
+
const { daemonCall } = await getDaemonClient();
|
|
964
|
+
const params: Record<string, any> = { query };
|
|
965
|
+
if (opts.sources) params.sources = opts.sources;
|
|
966
|
+
if (opts.after) params.after = opts.after;
|
|
967
|
+
if (opts.before) params.before = opts.before;
|
|
968
|
+
if (opts.recency !== undefined) params.recency = opts.recency;
|
|
969
|
+
if (opts.mode) params.mode = opts.mode; // legacy fallback
|
|
970
|
+
const hits = await daemonCall("lattices.search", params, 10000) as any[];
|
|
971
|
+
return hits.map((w: any) => ({
|
|
972
|
+
score: w.score || 0,
|
|
973
|
+
window: w,
|
|
974
|
+
tabs: (w.terminalTabs || []).map((t: any) => ({
|
|
975
|
+
tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
|
|
976
|
+
})),
|
|
977
|
+
reasons: w.matchSources || [],
|
|
978
|
+
}));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Convenience aliases
|
|
982
|
+
async function deepSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["all"] }); }
|
|
983
|
+
async function terminalSearch(query: string): Promise<SearchResult[]> { return search(query, { sources: ["terminals"] }); }
|
|
984
|
+
|
|
985
|
+
// Format and print search results
|
|
986
|
+
function printResults(ranked: SearchResult[]): void {
|
|
987
|
+
if (!ranked.length) return;
|
|
988
|
+
for (const r of ranked) {
|
|
989
|
+
const w = r.window;
|
|
990
|
+
const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
|
|
991
|
+
console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
|
|
992
|
+
for (const t of r.tabs) {
|
|
993
|
+
const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
|
|
994
|
+
const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
|
|
995
|
+
console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
|
|
996
|
+
}
|
|
997
|
+
if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
|
|
762
998
|
}
|
|
999
|
+
console.log();
|
|
763
1000
|
}
|
|
764
1001
|
|
|
765
|
-
|
|
1002
|
+
// ── search command ───────────────────────────────────────────────────
|
|
1003
|
+
|
|
1004
|
+
async function searchCommand(query: string | undefined, flags: Set<string>, rawArgs: string[] = []): Promise<void> {
|
|
1005
|
+
if (!query) {
|
|
1006
|
+
console.log("Usage: lattices search <query> [--quick | --terminal | --all | --sources=... | --after=... | --before=... | --json | --wid]");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Build search options from flags
|
|
1011
|
+
const opts: SearchOptions = {};
|
|
1012
|
+
|
|
1013
|
+
// Source selection: explicit --sources, or legacy --quick/--terminal, or default
|
|
1014
|
+
const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
|
|
1015
|
+
if (sourcesFlag) {
|
|
1016
|
+
opts.sources = sourcesFlag.slice("--sources=".length).split(",");
|
|
1017
|
+
} else if (flags.has("--all")) {
|
|
1018
|
+
opts.sources = ["all"];
|
|
1019
|
+
} else if (flags.has("--quick")) {
|
|
1020
|
+
opts.sources = ["titles", "apps", "sessions"];
|
|
1021
|
+
} else if (flags.has("--terminal")) {
|
|
1022
|
+
opts.sources = ["terminals"];
|
|
1023
|
+
}
|
|
1024
|
+
// else: omit → smart default on daemon side
|
|
1025
|
+
|
|
1026
|
+
// Time filters
|
|
1027
|
+
const afterFlag = rawArgs.find(a => a.startsWith("--after="));
|
|
1028
|
+
if (afterFlag) opts.after = afterFlag.slice("--after=".length);
|
|
1029
|
+
const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
|
|
1030
|
+
if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
|
|
1031
|
+
|
|
1032
|
+
// No-recency flag
|
|
1033
|
+
if (flags.has("--no-recency")) opts.recency = false;
|
|
1034
|
+
|
|
1035
|
+
const ranked = await search(query, opts);
|
|
1036
|
+
const jsonOut = flags.has("--json");
|
|
1037
|
+
const widOnly = flags.has("--wid");
|
|
1038
|
+
|
|
1039
|
+
if (jsonOut) {
|
|
1040
|
+
console.log(JSON.stringify(ranked.map(r => ({
|
|
1041
|
+
wid: r.window.wid, app: r.window.app, title: r.window.title,
|
|
1042
|
+
score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
|
|
1043
|
+
})), null, 2));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (widOnly) {
|
|
1048
|
+
for (const r of ranked) console.log(r.window.wid);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (!ranked.length) {
|
|
1053
|
+
console.log(`No results for "${query}"`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
printResults(ranked);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ── place command ────────────────────────────────────────────────────
|
|
1061
|
+
|
|
1062
|
+
async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
|
|
1063
|
+
if (!query) {
|
|
1064
|
+
console.log("Usage: lattices place <query> [position]");
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const { daemonCall } = await getDaemonClient();
|
|
1069
|
+
const ranked = await deepSearch(query);
|
|
1070
|
+
|
|
1071
|
+
if (!ranked.length) {
|
|
1072
|
+
console.log(`No window matching "${query}"`);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const pos = tilePosition || "bottom-right";
|
|
1077
|
+
const win = ranked[0].window;
|
|
1078
|
+
await daemonCall("window.focus", { wid: win.wid });
|
|
1079
|
+
await daemonCall("intents.execute", {
|
|
1080
|
+
intent: "tile_window",
|
|
1081
|
+
slots: { position: pos, wid: win.wid }
|
|
1082
|
+
}, 3000);
|
|
1083
|
+
console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
|
|
1084
|
+
} catch (e: unknown) {
|
|
1085
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function sessionsCommand(jsonFlag: boolean): Promise<void> {
|
|
1090
|
+
try {
|
|
1091
|
+
const { daemonCall } = await getDaemonClient();
|
|
1092
|
+
const sessions = await daemonCall("tmux.sessions") as any[];
|
|
1093
|
+
if (jsonFlag) {
|
|
1094
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (!sessions.length) {
|
|
1098
|
+
console.log("No active sessions.");
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
console.log(`Sessions (${sessions.length}):\n`);
|
|
1102
|
+
for (const s of sessions) {
|
|
1103
|
+
const windows = s.windowCount || s.windows || "?";
|
|
1104
|
+
console.log(` \x1b[1m${s.name}\x1b[0m (${windows} windows)`);
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
console.log("Daemon not running. Start with: lattices app");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function voiceCommand(subcommand?: string, ...rest: string[]): Promise<void> {
|
|
1112
|
+
const { daemonCall } = await getDaemonClient();
|
|
1113
|
+
try {
|
|
1114
|
+
switch (subcommand) {
|
|
1115
|
+
case "status": {
|
|
1116
|
+
const status = await daemonCall("voice.status") as any;
|
|
1117
|
+
console.log(`Provider: ${status.provider}`);
|
|
1118
|
+
console.log(`Available: ${status.available}`);
|
|
1119
|
+
console.log(`Listening: ${status.listening}`);
|
|
1120
|
+
if (status.lastTranscript) console.log(`Last: "${status.lastTranscript}"`);
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
case "simulate":
|
|
1124
|
+
case "sim": {
|
|
1125
|
+
const text = rest.join(" ");
|
|
1126
|
+
if (!text) {
|
|
1127
|
+
console.log("Usage: lattices voice simulate <text>");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const execute = !rest.includes("--dry-run");
|
|
1131
|
+
const dryFlag = rest.includes("--dry-run");
|
|
1132
|
+
const cleanText = dryFlag ? rest.filter(r => r !== "--dry-run").join(" ") : text;
|
|
1133
|
+
const result = await daemonCall("voice.simulate", { text: cleanText, execute }, 15000) as any;
|
|
1134
|
+
if (!result.parsed) {
|
|
1135
|
+
console.log(`\x1b[33mNo match:\x1b[0m "${cleanText}"`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const slots = Object.entries(result.slots || {}).map(([k,v]) => `${k}: ${v}`).join(", ");
|
|
1139
|
+
const conf = result.confidence ? ` (${(result.confidence * 100).toFixed(0)}%)` : "";
|
|
1140
|
+
console.log(`\x1b[36m${result.intent}\x1b[0m${slots ? ` ${slots}` : ""}${conf}`);
|
|
1141
|
+
if (result.executed) {
|
|
1142
|
+
console.log(`\x1b[32mExecuted\x1b[0m`);
|
|
1143
|
+
} else if (result.error) {
|
|
1144
|
+
console.log(`\x1b[31mError:\x1b[0m ${result.error}`);
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
case "intents": {
|
|
1149
|
+
const intents = await daemonCall("intents.list") as any[];
|
|
1150
|
+
for (const intent of intents) {
|
|
1151
|
+
const slots = intent.slots.map((s: any) => `${s.name}:${s.type}${s.required ? "*" : ""}`).join(", ");
|
|
1152
|
+
console.log(` \x1b[1m${intent.intent}\x1b[0m ${intent.description}`);
|
|
1153
|
+
if (slots) console.log(` slots: ${slots}`);
|
|
1154
|
+
console.log(` e.g. "${intent.examples[0]}"`);
|
|
1155
|
+
console.log();
|
|
1156
|
+
}
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
default:
|
|
1160
|
+
console.log("Usage: lattices voice <subcommand>\n");
|
|
1161
|
+
console.log(" status Show voice provider status");
|
|
1162
|
+
console.log(" simulate Parse and execute a voice command");
|
|
1163
|
+
console.log(" intents List all available intents");
|
|
1164
|
+
console.log("\nExamples:");
|
|
1165
|
+
console.log(' lattices voice simulate "tile this left"');
|
|
1166
|
+
console.log(' lattices voice simulate "focus chrome" --dry-run');
|
|
1167
|
+
}
|
|
1168
|
+
} catch (e: unknown) {
|
|
1169
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function callCommand(method?: string, ...rest: string[]): Promise<void> {
|
|
1174
|
+
if (!method) {
|
|
1175
|
+
console.log("Usage: lattices call <method> [params-json]");
|
|
1176
|
+
console.log("\nExamples:");
|
|
1177
|
+
console.log(" lattices call daemon.status");
|
|
1178
|
+
console.log(" lattices call api.schema");
|
|
1179
|
+
console.log(' lattices call window.place \'{"session":"vox","placement":"left"}\'');
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const { daemonCall } = await getDaemonClient();
|
|
1184
|
+
const params = rest[0] ? JSON.parse(rest[0]) : null;
|
|
1185
|
+
const result = await daemonCall(method, params, 15000);
|
|
1186
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1187
|
+
} catch (e: unknown) {
|
|
1188
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function layerCommand(index?: string): Promise<void> {
|
|
766
1193
|
try {
|
|
767
1194
|
const { daemonCall } = await getDaemonClient();
|
|
768
1195
|
if (index === undefined || index === null || index === "") {
|
|
769
|
-
const result = await daemonCall("layers.list");
|
|
1196
|
+
const result = await daemonCall("layers.list") as any;
|
|
770
1197
|
if (!result.layers.length) {
|
|
771
1198
|
console.log("No layers configured.");
|
|
772
1199
|
return;
|
|
@@ -780,21 +1207,21 @@ async function layerCommand(index) {
|
|
|
780
1207
|
}
|
|
781
1208
|
const idx = parseInt(index, 10);
|
|
782
1209
|
if (!isNaN(idx)) {
|
|
783
|
-
await daemonCall("layer.
|
|
784
|
-
console.log(`
|
|
1210
|
+
await daemonCall("layer.activate", { index: idx, mode: "launch" });
|
|
1211
|
+
console.log(`Activated layer ${idx}`);
|
|
785
1212
|
} else {
|
|
786
|
-
await daemonCall("layer.
|
|
787
|
-
console.log(`
|
|
1213
|
+
await daemonCall("layer.activate", { name: index, mode: "launch" });
|
|
1214
|
+
console.log(`Activated layer "${index}"`);
|
|
788
1215
|
}
|
|
789
|
-
} catch (e) {
|
|
790
|
-
console.log(`Error: ${e.message}`);
|
|
1216
|
+
} catch (e: unknown) {
|
|
1217
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
791
1218
|
}
|
|
792
1219
|
}
|
|
793
1220
|
|
|
794
|
-
async function diagCommand(limit) {
|
|
1221
|
+
async function diagCommand(limit?: string): Promise<void> {
|
|
795
1222
|
try {
|
|
796
1223
|
const { daemonCall } = await getDaemonClient();
|
|
797
|
-
const result = await daemonCall("diagnostics.list", { limit: parseInt(limit, 10) || 40 });
|
|
1224
|
+
const result = await daemonCall("diagnostics.list", { limit: parseInt(limit || "", 10) || 40 }) as any;
|
|
798
1225
|
if (!result.entries || !result.entries.length) {
|
|
799
1226
|
console.log("No diagnostic entries.");
|
|
800
1227
|
return;
|
|
@@ -805,26 +1232,26 @@ async function diagCommand(limit) {
|
|
|
805
1232
|
entry.level === "error" ? "\x1b[31m✗\x1b[0m" : "›";
|
|
806
1233
|
console.log(` \x1b[90m${entry.time}\x1b[0m ${icon} ${entry.message}`);
|
|
807
1234
|
}
|
|
808
|
-
} catch (e) {
|
|
809
|
-
console.log(`Error: ${e.message}`);
|
|
1235
|
+
} catch (e: unknown) {
|
|
1236
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
810
1237
|
}
|
|
811
1238
|
}
|
|
812
1239
|
|
|
813
|
-
async function distributeCommand() {
|
|
1240
|
+
async function distributeCommand(): Promise<void> {
|
|
814
1241
|
try {
|
|
815
1242
|
const { daemonCall } = await getDaemonClient();
|
|
816
|
-
await daemonCall("
|
|
1243
|
+
await daemonCall("space.optimize", { scope: "visible", strategy: "balanced" });
|
|
817
1244
|
console.log("Distributed visible windows into grid");
|
|
818
1245
|
} catch {
|
|
819
1246
|
console.log("Daemon not running. Start with: lattices app");
|
|
820
1247
|
}
|
|
821
1248
|
}
|
|
822
1249
|
|
|
823
|
-
async function daemonLsCommand() {
|
|
1250
|
+
async function daemonLsCommand(): Promise<boolean> {
|
|
824
1251
|
try {
|
|
825
1252
|
const { daemonCall, isDaemonRunning } = await getDaemonClient();
|
|
826
1253
|
if (!(await isDaemonRunning())) return false;
|
|
827
|
-
const sessions = await daemonCall("tmux.sessions");
|
|
1254
|
+
const sessions = await daemonCall("tmux.sessions") as any[];
|
|
828
1255
|
if (!sessions.length) {
|
|
829
1256
|
console.log("No active tmux sessions.");
|
|
830
1257
|
return true;
|
|
@@ -832,7 +1259,7 @@ async function daemonLsCommand() {
|
|
|
832
1259
|
|
|
833
1260
|
// Annotate sessions with workspace group info
|
|
834
1261
|
const ws = readWorkspaceConfig();
|
|
835
|
-
const sessionGroupMap = new Map();
|
|
1262
|
+
const sessionGroupMap = new Map<string, { group: string; tab: string }>();
|
|
836
1263
|
if (ws?.groups) {
|
|
837
1264
|
for (const g of ws.groups) {
|
|
838
1265
|
for (const tab of g.tabs || []) {
|
|
@@ -858,14 +1285,14 @@ async function daemonLsCommand() {
|
|
|
858
1285
|
}
|
|
859
1286
|
}
|
|
860
1287
|
|
|
861
|
-
async function daemonStatusInventory() {
|
|
1288
|
+
async function daemonStatusInventory(): Promise<boolean> {
|
|
862
1289
|
try {
|
|
863
1290
|
const { daemonCall, isDaemonRunning } = await getDaemonClient();
|
|
864
1291
|
if (!(await isDaemonRunning())) return false;
|
|
865
|
-
const inv = await daemonCall("tmux.inventory");
|
|
1292
|
+
const inv = await daemonCall("tmux.inventory") as any;
|
|
866
1293
|
|
|
867
1294
|
// Build managed session name set
|
|
868
|
-
const managed = new Map();
|
|
1295
|
+
const managed = new Map<string, string>();
|
|
869
1296
|
const ws = readWorkspaceConfig();
|
|
870
1297
|
if (ws?.groups) {
|
|
871
1298
|
for (const g of ws.groups) {
|
|
@@ -879,7 +1306,7 @@ async function daemonStatusInventory() {
|
|
|
879
1306
|
for (const s of inv.all) {
|
|
880
1307
|
if (!managed.has(s.name)) {
|
|
881
1308
|
// Check if it matches a scanned project (via daemon)
|
|
882
|
-
const projects = await daemonCall("projects.list");
|
|
1309
|
+
const projects = await daemonCall("projects.list") as any[];
|
|
883
1310
|
for (const p of projects) {
|
|
884
1311
|
managed.set(p.sessionName, p.name);
|
|
885
1312
|
}
|
|
@@ -887,7 +1314,7 @@ async function daemonStatusInventory() {
|
|
|
887
1314
|
}
|
|
888
1315
|
}
|
|
889
1316
|
|
|
890
|
-
const managedSessions = inv.all.filter((s) => managed.has(s.name));
|
|
1317
|
+
const managedSessions = inv.all.filter((s: any) => managed.has(s.name));
|
|
891
1318
|
const orphanSessions = inv.orphans;
|
|
892
1319
|
|
|
893
1320
|
if (managedSessions.length > 0) {
|
|
@@ -926,14 +1353,14 @@ async function daemonStatusInventory() {
|
|
|
926
1353
|
|
|
927
1354
|
// ── OCR commands ──────────────────────────────────────────────────────
|
|
928
1355
|
|
|
929
|
-
async function scanCommand(sub, ...rest) {
|
|
1356
|
+
async function scanCommand(sub?: string, ...rest: string[]): Promise<void> {
|
|
930
1357
|
const { daemonCall } = await getDaemonClient();
|
|
931
1358
|
|
|
932
1359
|
if (!sub || sub === "snapshot" || sub === "ls" || sub === "--full" || sub === "-f" || sub === "--json") {
|
|
933
1360
|
const full = sub === "--full" || sub === "-f" || rest.includes("--full") || rest.includes("-f");
|
|
934
1361
|
const json = sub === "--json" || rest.includes("--json");
|
|
935
1362
|
try {
|
|
936
|
-
const results = await daemonCall("ocr.snapshot", null, 5000);
|
|
1363
|
+
const results = await daemonCall("ocr.snapshot", null, 5000) as any[];
|
|
937
1364
|
if (!results.length) {
|
|
938
1365
|
console.log("No scan results yet. The first scan runs ~60s after launch.");
|
|
939
1366
|
return;
|
|
@@ -957,7 +1384,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
957
1384
|
}
|
|
958
1385
|
} else {
|
|
959
1386
|
const maxPreview = 5;
|
|
960
|
-
const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
1387
|
+
const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
961
1388
|
for (const line of preview) {
|
|
962
1389
|
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
963
1390
|
}
|
|
@@ -983,7 +1410,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
983
1410
|
return;
|
|
984
1411
|
}
|
|
985
1412
|
try {
|
|
986
|
-
const results = await daemonCall("ocr.search", { query }, 5000);
|
|
1413
|
+
const results = await daemonCall("ocr.search", { query }, 5000) as any[];
|
|
987
1414
|
if (!results.length) {
|
|
988
1415
|
console.log(`No matches for "${query}".`);
|
|
989
1416
|
return;
|
|
@@ -997,8 +1424,8 @@ async function scanCommand(sub, ...rest) {
|
|
|
997
1424
|
console.log(` ${snippet}`);
|
|
998
1425
|
console.log();
|
|
999
1426
|
}
|
|
1000
|
-
} catch (e) {
|
|
1001
|
-
console.log(`Error: ${e.message}`);
|
|
1427
|
+
} catch (e: unknown) {
|
|
1428
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1002
1429
|
}
|
|
1003
1430
|
return;
|
|
1004
1431
|
}
|
|
@@ -1006,9 +1433,9 @@ async function scanCommand(sub, ...rest) {
|
|
|
1006
1433
|
if (sub === "recent" || sub === "log") {
|
|
1007
1434
|
const full = rest.includes("--full") || rest.includes("-f");
|
|
1008
1435
|
const numArg = rest.find(a => !a.startsWith("-"));
|
|
1009
|
-
const limit = parseInt(numArg, 10) || 20;
|
|
1436
|
+
const limit = parseInt(numArg || "", 10) || 20;
|
|
1010
1437
|
try {
|
|
1011
|
-
const results = await daemonCall("ocr.recent", { limit }, 5000);
|
|
1438
|
+
const results = await daemonCall("ocr.recent", { limit }, 5000) as any[];
|
|
1012
1439
|
if (!results.length) {
|
|
1013
1440
|
console.log("No history yet. The first scan runs ~60s after launch.");
|
|
1014
1441
|
return;
|
|
@@ -1026,7 +1453,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
1026
1453
|
}
|
|
1027
1454
|
} else {
|
|
1028
1455
|
const maxPreview = 5;
|
|
1029
|
-
const preview = lines.slice(0, maxPreview).map(l => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
1456
|
+
const preview = lines.slice(0, maxPreview).map((l: string) => l.length > 100 ? l.slice(0, 97) + "..." : l);
|
|
1030
1457
|
for (const line of preview) {
|
|
1031
1458
|
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
1032
1459
|
}
|
|
@@ -1047,8 +1474,8 @@ async function scanCommand(sub, ...rest) {
|
|
|
1047
1474
|
console.log("Triggering deep scan (Vision OCR)...");
|
|
1048
1475
|
await daemonCall("ocr.scan", null, 30000);
|
|
1049
1476
|
console.log("Done.");
|
|
1050
|
-
} catch (e) {
|
|
1051
|
-
console.log(`Error: ${e.message}`);
|
|
1477
|
+
} catch (e: unknown) {
|
|
1478
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1052
1479
|
}
|
|
1053
1480
|
return;
|
|
1054
1481
|
}
|
|
@@ -1060,7 +1487,7 @@ async function scanCommand(sub, ...rest) {
|
|
|
1060
1487
|
return;
|
|
1061
1488
|
}
|
|
1062
1489
|
try {
|
|
1063
|
-
const results = await daemonCall("ocr.history", { wid }, 5000);
|
|
1490
|
+
const results = await daemonCall("ocr.history", { wid }, 5000) as any[];
|
|
1064
1491
|
if (!results.length) {
|
|
1065
1492
|
console.log(`No history for wid:${wid}.`);
|
|
1066
1493
|
return;
|
|
@@ -1070,15 +1497,15 @@ async function scanCommand(sub, ...rest) {
|
|
|
1070
1497
|
const ts = new Date(r.timestamp * 1000).toLocaleTimeString();
|
|
1071
1498
|
const src = r.source === "accessibility" ? "\x1b[33mAX\x1b[0m" : "\x1b[35mOCR\x1b[0m";
|
|
1072
1499
|
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);
|
|
1500
|
+
const preview = lines.slice(0, 2).map((l: string) => l.length > 80 ? l.slice(0, 77) + "..." : l);
|
|
1074
1501
|
console.log(` \x1b[90m${ts}\x1b[0m ${src} \x1b[1m${r.app}\x1b[0m — "${r.title}"`);
|
|
1075
1502
|
for (const line of preview) {
|
|
1076
1503
|
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
1077
1504
|
}
|
|
1078
1505
|
console.log();
|
|
1079
1506
|
}
|
|
1080
|
-
} catch (e) {
|
|
1081
|
-
console.log(`Error: ${e.message}`);
|
|
1507
|
+
} catch (e: unknown) {
|
|
1508
|
+
console.log(`Error: ${(e as Error).message}`);
|
|
1082
1509
|
}
|
|
1083
1510
|
return;
|
|
1084
1511
|
}
|
|
@@ -1097,7 +1524,7 @@ Usage:
|
|
|
1097
1524
|
`);
|
|
1098
1525
|
}
|
|
1099
1526
|
|
|
1100
|
-
function printUsage() {
|
|
1527
|
+
function printUsage(): void {
|
|
1101
1528
|
console.log(`lattices — Claude Code + dev server in tmux
|
|
1102
1529
|
|
|
1103
1530
|
Usage:
|
|
@@ -1111,17 +1538,31 @@ Usage:
|
|
|
1111
1538
|
lattices group [id] List tab groups or launch/attach a group
|
|
1112
1539
|
lattices groups List all tab groups with status
|
|
1113
1540
|
lattices tab <group> [tab] Switch tab within a group (by label or index)
|
|
1541
|
+
lattices search <query> Search windows by title, app, session, OCR
|
|
1542
|
+
lattices search <q> --deep Deep search: index + live terminal inspection
|
|
1543
|
+
lattices search <q> --wid Print matching window IDs only (pipeable)
|
|
1544
|
+
lattices search <q> --json JSON output
|
|
1545
|
+
lattices place <query> [pos] Deep search + focus + tile (default: bottom-right)
|
|
1546
|
+
lattices focus <session> Raise a session's window
|
|
1114
1547
|
lattices windows [--json] List all desktop windows (daemon required)
|
|
1115
|
-
lattices
|
|
1548
|
+
lattices sessions [--json] List active tmux sessions via daemon
|
|
1116
1549
|
lattices tile <position> Tile the frontmost window (left, right, top, etc.)
|
|
1117
1550
|
lattices distribute Smart-grid all visible windows (daemon required)
|
|
1118
1551
|
lattices layer [name|index] List layers or switch by name/index (daemon required)
|
|
1552
|
+
lattices voice status Voice provider status
|
|
1553
|
+
lattices voice simulate <t> Parse and execute a voice command
|
|
1554
|
+
lattices voice intents List all available intents
|
|
1555
|
+
lattices call <method> [p] Raw daemon API call (params as JSON)
|
|
1119
1556
|
lattices scan Show text from all visible windows
|
|
1120
1557
|
lattices scan --full Full text dump
|
|
1121
1558
|
lattices scan search <q> Full-text search across scanned windows
|
|
1122
1559
|
lattices scan recent [n] Show recent scans chronologically
|
|
1123
1560
|
lattices scan deep Trigger a deep Vision OCR scan
|
|
1124
1561
|
lattices scan history <wid> Scan timeline for a specific window
|
|
1562
|
+
lattices dev Run dev server (auto-detected)
|
|
1563
|
+
lattices dev build Build the project (swift/node/rust/go/make)
|
|
1564
|
+
lattices dev restart Build + restart (swift app) or just build
|
|
1565
|
+
lattices dev type Print detected project type
|
|
1125
1566
|
lattices daemon status Show daemon status
|
|
1126
1567
|
lattices diag [limit] Show diagnostic log entries
|
|
1127
1568
|
lattices app Launch the menu bar companion app
|
|
@@ -1172,7 +1613,7 @@ Layouts:
|
|
|
1172
1613
|
`);
|
|
1173
1614
|
}
|
|
1174
1615
|
|
|
1175
|
-
function initConfig() {
|
|
1616
|
+
function initConfig(): void {
|
|
1176
1617
|
const dir = process.cwd();
|
|
1177
1618
|
const configPath = resolve(dir, ".lattices.json");
|
|
1178
1619
|
|
|
@@ -1192,7 +1633,7 @@ function initConfig() {
|
|
|
1192
1633
|
console.log(JSON.stringify(config, null, 2));
|
|
1193
1634
|
}
|
|
1194
1635
|
|
|
1195
|
-
function listSessions() {
|
|
1636
|
+
function listSessions(): void {
|
|
1196
1637
|
const out = runQuiet(
|
|
1197
1638
|
"tmux list-sessions -F '#{session_name} (#{session_windows} windows, created #{session_created_string})'"
|
|
1198
1639
|
);
|
|
@@ -1203,7 +1644,7 @@ function listSessions() {
|
|
|
1203
1644
|
|
|
1204
1645
|
// Annotate sessions that belong to tab groups
|
|
1205
1646
|
const ws = readWorkspaceConfig();
|
|
1206
|
-
const sessionGroupMap = new Map();
|
|
1647
|
+
const sessionGroupMap = new Map<string, { group: string; tab: string }>();
|
|
1207
1648
|
if (ws?.groups) {
|
|
1208
1649
|
for (const g of ws.groups) {
|
|
1209
1650
|
for (const tab of g.tabs || []) {
|
|
@@ -1216,7 +1657,7 @@ function listSessions() {
|
|
|
1216
1657
|
}
|
|
1217
1658
|
}
|
|
1218
1659
|
|
|
1219
|
-
const lines = out.split("\n").map((line) => {
|
|
1660
|
+
const lines = out.split("\n").map((line: string) => {
|
|
1220
1661
|
const sessionName = line.split(" ")[0];
|
|
1221
1662
|
const info = sessionGroupMap.get(sessionName);
|
|
1222
1663
|
return info
|
|
@@ -1228,7 +1669,7 @@ function listSessions() {
|
|
|
1228
1669
|
console.log(lines.join("\n"));
|
|
1229
1670
|
}
|
|
1230
1671
|
|
|
1231
|
-
function killSession(name) {
|
|
1672
|
+
function killSession(name?: string): void {
|
|
1232
1673
|
if (!name) name = toSessionName(process.cwd());
|
|
1233
1674
|
if (!sessionExists(name)) {
|
|
1234
1675
|
console.log(`No session "${name}".`);
|
|
@@ -1240,7 +1681,14 @@ function killSession(name) {
|
|
|
1240
1681
|
|
|
1241
1682
|
// ── Window tiling ────────────────────────────────────────────────────
|
|
1242
1683
|
|
|
1243
|
-
|
|
1684
|
+
interface ScreenBounds {
|
|
1685
|
+
x: number;
|
|
1686
|
+
y: number;
|
|
1687
|
+
w: number;
|
|
1688
|
+
h: number;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function getScreenBounds(): ScreenBounds {
|
|
1244
1692
|
// Get the visible area (excludes menu bar and dock) in AppleScript coordinates (top-left origin)
|
|
1245
1693
|
const script = `
|
|
1246
1694
|
tell application "Finder"
|
|
@@ -1255,7 +1703,7 @@ function getScreenBounds() {
|
|
|
1255
1703
|
}
|
|
1256
1704
|
|
|
1257
1705
|
// Presets return AppleScript bounds: [left, top, right, bottom] within the visible area
|
|
1258
|
-
const tilePresets = {
|
|
1706
|
+
const tilePresets: Record<string, (s: ScreenBounds) => number[]> = {
|
|
1259
1707
|
"left": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
|
|
1260
1708
|
"left-half": (s) => [s.x, s.y, s.x + s.w / 2, s.y + s.h],
|
|
1261
1709
|
"right": (s) => [s.x + s.w / 2, s.y, s.x + s.w, s.y + s.h],
|
|
@@ -1282,7 +1730,7 @@ const tilePresets = {
|
|
|
1282
1730
|
"right-third": (s) => [s.x + Math.round(s.w * 0.667), s.y, s.x + s.w, s.y + s.h],
|
|
1283
1731
|
};
|
|
1284
1732
|
|
|
1285
|
-
function tileWindow(position) {
|
|
1733
|
+
function tileWindow(position: string): void {
|
|
1286
1734
|
const preset = tilePresets[position];
|
|
1287
1735
|
if (!preset) {
|
|
1288
1736
|
console.log(`Unknown position: ${position}`);
|
|
@@ -1302,7 +1750,7 @@ function tileWindow(position) {
|
|
|
1302
1750
|
console.log(`Tiled → ${position}`);
|
|
1303
1751
|
}
|
|
1304
1752
|
|
|
1305
|
-
function createOrAttach() {
|
|
1753
|
+
function createOrAttach(): void {
|
|
1306
1754
|
const dir = process.cwd();
|
|
1307
1755
|
const name = toSessionName(dir);
|
|
1308
1756
|
|
|
@@ -1323,7 +1771,7 @@ function createOrAttach() {
|
|
|
1323
1771
|
attach(name);
|
|
1324
1772
|
}
|
|
1325
1773
|
|
|
1326
|
-
function attach(name) {
|
|
1774
|
+
function attach(name: string): void {
|
|
1327
1775
|
if (isInsideTmux()) {
|
|
1328
1776
|
execSync(`tmux switch-client -t "${name}"`, { stdio: "inherit" });
|
|
1329
1777
|
} else {
|
|
@@ -1333,7 +1781,7 @@ function attach(name) {
|
|
|
1333
1781
|
|
|
1334
1782
|
// ── Status / Inventory ───────────────────────────────────────────────
|
|
1335
1783
|
|
|
1336
|
-
function statusInventory() {
|
|
1784
|
+
function statusInventory(): void {
|
|
1337
1785
|
// Query all tmux sessions
|
|
1338
1786
|
const sessionsRaw = runQuiet(
|
|
1339
1787
|
'tmux list-sessions -F "#{session_name}\t#{session_windows}\t#{session_attached}"'
|
|
@@ -1349,17 +1797,17 @@ function statusInventory() {
|
|
|
1349
1797
|
);
|
|
1350
1798
|
|
|
1351
1799
|
// Parse panes grouped by session
|
|
1352
|
-
const panesBySession = new Map();
|
|
1800
|
+
const panesBySession = new Map<string, { title: string; cmd: string }[]>();
|
|
1353
1801
|
if (panesRaw) {
|
|
1354
1802
|
for (const line of panesRaw.split("\n").filter(Boolean)) {
|
|
1355
1803
|
const [sess, title, cmd] = line.split("\t");
|
|
1356
1804
|
if (!panesBySession.has(sess)) panesBySession.set(sess, []);
|
|
1357
|
-
panesBySession.get(sess)
|
|
1805
|
+
panesBySession.get(sess)!.push({ title, cmd });
|
|
1358
1806
|
}
|
|
1359
1807
|
}
|
|
1360
1808
|
|
|
1361
1809
|
// Build managed session name set
|
|
1362
|
-
const managed = new Map(); // name -> label
|
|
1810
|
+
const managed = new Map<string, string>(); // name -> label
|
|
1363
1811
|
|
|
1364
1812
|
// From workspace groups
|
|
1365
1813
|
const ws = readWorkspaceConfig();
|
|
@@ -1391,7 +1839,7 @@ function statusInventory() {
|
|
|
1391
1839
|
}
|
|
1392
1840
|
|
|
1393
1841
|
// Parse sessions and classify
|
|
1394
|
-
const sessions = sessionsRaw.split("\n").filter(Boolean).map((line) => {
|
|
1842
|
+
const sessions = sessionsRaw.split("\n").filter(Boolean).map((line: string) => {
|
|
1395
1843
|
const [name, windows, attached] = line.split("\t");
|
|
1396
1844
|
return { name, windows: parseInt(windows) || 1, attached: attached !== "0" };
|
|
1397
1845
|
});
|
|
@@ -1437,10 +1885,7 @@ function statusInventory() {
|
|
|
1437
1885
|
|
|
1438
1886
|
// ── Main ─────────────────────────────────────────────────────────────
|
|
1439
1887
|
|
|
1440
|
-
|
|
1441
|
-
console.error("tmux is not installed. Install with: brew install tmux");
|
|
1442
|
-
process.exit(1);
|
|
1443
|
-
}
|
|
1888
|
+
requireTmux(command);
|
|
1444
1889
|
|
|
1445
1890
|
switch (command) {
|
|
1446
1891
|
case "init":
|
|
@@ -1509,9 +1954,25 @@ switch (command) {
|
|
|
1509
1954
|
console.log(" lattices window map [--json] Show all layer tags");
|
|
1510
1955
|
}
|
|
1511
1956
|
break;
|
|
1957
|
+
case "search":
|
|
1958
|
+
case "s":
|
|
1959
|
+
await searchCommand(args[1], new Set(args.slice(2)));
|
|
1960
|
+
break;
|
|
1512
1961
|
case "focus":
|
|
1513
1962
|
await focusCommand(args[1]);
|
|
1514
1963
|
break;
|
|
1964
|
+
case "place":
|
|
1965
|
+
await placeCommand(args[1], args[2]);
|
|
1966
|
+
break;
|
|
1967
|
+
case "sessions":
|
|
1968
|
+
await sessionsCommand(args[1] === "--json");
|
|
1969
|
+
break;
|
|
1970
|
+
case "voice":
|
|
1971
|
+
await voiceCommand(args[1], ...args.slice(2));
|
|
1972
|
+
break;
|
|
1973
|
+
case "call":
|
|
1974
|
+
await callCommand(args[1], ...args.slice(2));
|
|
1975
|
+
break;
|
|
1515
1976
|
case "layer":
|
|
1516
1977
|
case "layers":
|
|
1517
1978
|
await layerCommand(args[1]);
|
|
@@ -1531,13 +1992,15 @@ switch (command) {
|
|
|
1531
1992
|
console.log("Usage: lattices daemon status");
|
|
1532
1993
|
}
|
|
1533
1994
|
break;
|
|
1995
|
+
case "dev":
|
|
1996
|
+
await devCommand(args[1], ...args.slice(2));
|
|
1997
|
+
break;
|
|
1534
1998
|
case "app": {
|
|
1535
1999
|
// Forward to lattices-app script
|
|
1536
2000
|
const { execFileSync } = await import("node:child_process");
|
|
1537
|
-
const
|
|
1538
|
-
const appScript = resolve(__dirname2, "lattices-app.js");
|
|
2001
|
+
const appScript = resolve(import.meta.dir, "lattices-app.ts");
|
|
1539
2002
|
try {
|
|
1540
|
-
execFileSync("
|
|
2003
|
+
execFileSync("bun", [appScript, ...args.slice(1)], { stdio: "inherit" });
|
|
1541
2004
|
} catch { /* exit code forwarded */ }
|
|
1542
2005
|
break;
|
|
1543
2006
|
}
|