@arach/lattices 0.2.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -86
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. package/bin/lattices.js +0 -1418
@@ -0,0 +1,252 @@
1
+ import {
2
+ hasFlag,
3
+ nonFlagArgs,
4
+ parseFlagValue,
5
+ parseOptionalNumber,
6
+ } from "./helpers.ts";
7
+ import { withDaemon } from "./daemon.ts";
8
+
9
+ export async function captureCommand(subcommand?: string, ...rawArgs: string[]): Promise<void> {
10
+ const sub = subcommand || "window";
11
+ const dashIndex = rawArgs.indexOf("--");
12
+ const commandArgs = dashIndex >= 0 ? rawArgs.slice(0, dashIndex) : rawArgs;
13
+ const childArgs = dashIndex >= 0 ? rawArgs.slice(dashIndex + 1) : [];
14
+ const jsonFlag = hasFlag(commandArgs, "json");
15
+ const positional = nonFlagArgs(commandArgs);
16
+
17
+ if (["stop", "stop-recording", "stopRecording"].includes(sub)) {
18
+ const params: Record<string, unknown> = {};
19
+ const runId = positional[0] || parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId") || parseFlagValue(commandArgs, "id");
20
+ const stopFile = parseFlagValue(commandArgs, "stop-file") || parseFlagValue(commandArgs, "stopFile");
21
+ const finishedFile = parseFlagValue(commandArgs, "finished-file") || parseFlagValue(commandArgs, "finishedFile");
22
+ const timeoutMs = Number(parseFlagValue(commandArgs, "timeout-ms") || parseFlagValue(commandArgs, "timeoutMs") || 30000);
23
+ if (runId) params.runId = runId;
24
+ if (stopFile) params.stopFile = stopFile;
25
+ if (finishedFile) params.finishedFile = finishedFile;
26
+ if (Number.isFinite(timeoutMs)) params.timeoutMs = timeoutMs;
27
+ params.wait = !hasFlag(commandArgs, "no-wait");
28
+
29
+ await withDaemon(async ({ daemonCall }) => {
30
+ const result = await daemonCall("capture.stopRecording", params, timeoutMs + 5000) as any;
31
+ if (jsonFlag) {
32
+ console.log(JSON.stringify(result, null, 2));
33
+ return;
34
+ }
35
+ console.log(result.finished ? "Recording finished." : "Recording stop requested.");
36
+ if (result.run?.id) console.log(` run: ${result.run.id}`);
37
+ if (result.marker) console.log(` marker: ${result.marker}`);
38
+ });
39
+ return;
40
+ }
41
+
42
+ const isRecordCommand = [
43
+ "record-command",
44
+ "recordCommand",
45
+ "record-run",
46
+ "recordRun",
47
+ "record-exec",
48
+ "recordExec",
49
+ ].includes(sub);
50
+
51
+ if (isRecordCommand) {
52
+ if (!childArgs.length) {
53
+ console.log(`lattices capture record-command — record while running a command
54
+
55
+ Usage:
56
+ lattices capture record-command --app Scout --filename demo.mov -- <command> [...args]
57
+ `);
58
+ return;
59
+ }
60
+
61
+ const params: Record<string, unknown> = { source: "cli" };
62
+ const explicitWid = positional[0] ? Number(positional[0]) : NaN;
63
+ if (Number.isFinite(explicitWid)) params.wid = explicitWid;
64
+
65
+ const session = parseFlagValue(commandArgs, "session");
66
+ const app = parseFlagValue(commandArgs, "app");
67
+ const title = parseFlagValue(commandArgs, "title");
68
+ const filename = parseFlagValue(commandArgs, "filename");
69
+ const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
70
+ const mode = parseFlagValue(commandArgs, "mode");
71
+ const fps = parseOptionalNumber(commandArgs, "fps");
72
+ const scale = parseOptionalNumber(commandArgs, "scale");
73
+ const timeoutMs = Number(parseFlagValue(commandArgs, "timeout-ms") || parseFlagValue(commandArgs, "timeoutMs") || 30000);
74
+ if (session) params.session = session;
75
+ if (app) params.app = app;
76
+ if (title) params.title = title;
77
+ if (filename) params.filename = filename;
78
+ if (runId) params.runId = runId;
79
+ if (mode) params.mode = mode;
80
+ if (fps !== undefined) params.fps = fps;
81
+ if (scale !== undefined) params.scale = scale;
82
+
83
+ for (const [flag, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["w", "w"], ["h", "h"]] as const) {
84
+ const value = parseOptionalNumber(commandArgs, flag);
85
+ if (value !== undefined) params[key] = value;
86
+ }
87
+
88
+ const recordsRegion = hasFlag(commandArgs, "region") ||
89
+ (params.x !== undefined && params.y !== undefined &&
90
+ (params.width !== undefined || params.w !== undefined) &&
91
+ (params.height !== undefined || params.h !== undefined));
92
+ const method = recordsRegion ? "capture.recordRegion" : "capture.recordWindow";
93
+
94
+ await withDaemon(async ({ daemonCall }) => {
95
+ const start = await daemonCall(method, params, 20000) as any;
96
+ let childExitCode = 0;
97
+ let childError: string | undefined;
98
+
99
+ try {
100
+ const proc = Bun.spawn(childArgs, {
101
+ cwd: process.cwd(),
102
+ env: process.env,
103
+ stdin: "inherit",
104
+ stdout: "inherit",
105
+ stderr: "inherit",
106
+ });
107
+ childExitCode = await proc.exited;
108
+ } catch (error) {
109
+ childExitCode = 127;
110
+ childError = (error as Error).message;
111
+ }
112
+
113
+ const stop = await daemonCall(
114
+ "capture.stopRecording",
115
+ { runId: start.run?.id, wait: true, timeoutMs },
116
+ timeoutMs + 5000
117
+ ) as any;
118
+
119
+ if (jsonFlag) {
120
+ console.log(JSON.stringify({
121
+ ok: childExitCode === 0 && stop.ok !== false,
122
+ child: {
123
+ command: childArgs,
124
+ exitCode: childExitCode,
125
+ error: childError,
126
+ },
127
+ recording: start,
128
+ stopResult: stop,
129
+ }, null, 2));
130
+ } else {
131
+ const artifact = start.artifact || {};
132
+ const run = stop.run || start.run || {};
133
+ console.log(`Recording finished.`);
134
+ console.log(` run: ${run.id || start.run?.id || "?"}`);
135
+ console.log(` artifact: ${artifact.path || "?"}`);
136
+ console.log(` child exit: ${childExitCode}`);
137
+ if (childError) console.log(` child error: ${childError}`);
138
+ }
139
+
140
+ if (childExitCode !== 0 && !hasFlag(commandArgs, "ignore-child-failure")) {
141
+ process.exitCode = childExitCode;
142
+ }
143
+ });
144
+ return;
145
+ }
146
+
147
+ const isRecord = ["record", "record-window", "recording", "video"].includes(sub);
148
+ const isRecordRegion = ["record-region", "recordRegion", "region-recording"].includes(sub) ||
149
+ (sub === "record" && ["region", "rect"].includes(positional[0] || ""));
150
+
151
+ if (isRecord || isRecordRegion) {
152
+ const params: Record<string, unknown> = { source: "cli" };
153
+ const targetKind = sub === "record" ? positional[0] : undefined;
154
+ const positionalOffset = targetKind === "window" || targetKind === "region" || targetKind === "rect" ? 1 : 0;
155
+ const explicitWid = positional[positionalOffset] ? Number(positional[positionalOffset]) : NaN;
156
+ if (Number.isFinite(explicitWid)) params.wid = explicitWid;
157
+
158
+ const session = parseFlagValue(commandArgs, "session");
159
+ const app = parseFlagValue(commandArgs, "app");
160
+ const title = parseFlagValue(commandArgs, "title");
161
+ const filename = parseFlagValue(commandArgs, "filename");
162
+ const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
163
+ const mode = parseFlagValue(commandArgs, "mode");
164
+ const fps = parseOptionalNumber(commandArgs, "fps");
165
+ const scale = parseOptionalNumber(commandArgs, "scale");
166
+ const durationMs = parseOptionalNumber(commandArgs, "duration-ms", "durationMs", "duration");
167
+ if (session) params.session = session;
168
+ if (app) params.app = app;
169
+ if (title) params.title = title;
170
+ if (filename) params.filename = filename;
171
+ if (runId) params.runId = runId;
172
+ if (mode) params.mode = mode;
173
+ if (fps !== undefined) params.fps = fps;
174
+ if (scale !== undefined) params.scale = scale;
175
+
176
+ for (const [flag, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["w", "w"], ["h", "h"]] as const) {
177
+ const value = parseOptionalNumber(commandArgs, flag);
178
+ if (value !== undefined) params[key] = value;
179
+ }
180
+
181
+ await withDaemon(async ({ daemonCall }) => {
182
+ const method = isRecordRegion ? "capture.recordRegion" : "capture.recordWindow";
183
+ const result = await daemonCall(method, params, 20000) as any;
184
+
185
+ if (durationMs !== undefined && durationMs > 0) {
186
+ await new Promise((resolve) => setTimeout(resolve, durationMs));
187
+ const stop = await daemonCall(
188
+ "capture.stopRecording",
189
+ { runId: result.run?.id, wait: true, timeoutMs: 30000 },
190
+ 35000
191
+ ) as any;
192
+ result.stopResult = stop;
193
+ }
194
+
195
+ if (jsonFlag) {
196
+ console.log(JSON.stringify(result, null, 2));
197
+ return;
198
+ }
199
+ const artifact = result.artifact || {};
200
+ const run = result.stopResult?.run || result.run || {};
201
+ console.log(`Recording ${result.stopResult ? "finished" : "started"}.`);
202
+ console.log(` run: ${run.id || result.run?.id || "?"}`);
203
+ console.log(` artifact: ${artifact.path || "?"}`);
204
+ if (!result.stopResult) {
205
+ console.log(` stop: lattices capture stop ${result.run?.id || ""}`);
206
+ }
207
+ });
208
+ return;
209
+ }
210
+
211
+ if (!["window", "screenshot", "shot"].includes(sub)) {
212
+ console.log(`lattices capture — capture run artifacts
213
+
214
+ Usage:
215
+ lattices capture window [wid] [--json]
216
+ lattices capture screenshot [wid] [--session name] [--app name]
217
+ lattices capture record window [wid] [--app name] [--duration-ms 5000] [--json]
218
+ lattices capture record region --x N --y N --width N --height N [--duration-ms 5000]
219
+ lattices capture record-command --app Scout --filename demo.mov -- <command> [...args]
220
+ lattices capture stop <run-id>
221
+ `);
222
+ return;
223
+ }
224
+
225
+ const params: Record<string, unknown> = { source: "cli" };
226
+ const explicitWid = positional[0] ? Number(positional[0]) : NaN;
227
+ if (Number.isFinite(explicitWid)) params.wid = explicitWid;
228
+ const session = parseFlagValue(commandArgs, "session");
229
+ const app = parseFlagValue(commandArgs, "app");
230
+ const title = parseFlagValue(commandArgs, "title");
231
+ const filename = parseFlagValue(commandArgs, "filename");
232
+ const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
233
+ if (session) params.session = session;
234
+ if (app) params.app = app;
235
+ if (title) params.title = title;
236
+ if (filename) params.filename = filename;
237
+ if (runId) params.runId = runId;
238
+
239
+ await withDaemon(async ({ daemonCall }) => {
240
+ const result = await daemonCall("capture.screenshotWindow", params, 20000) as any;
241
+ if (jsonFlag) {
242
+ console.log(JSON.stringify(result, null, 2));
243
+ return;
244
+ }
245
+ const artifact = result.artifact || {};
246
+ const run = result.run || {};
247
+ const target = result.target || {};
248
+ console.log(`Captured ${target.app || "window"} ${target.wid ? `wid:${target.wid}` : ""}`);
249
+ console.log(` run: ${run.id || "?"}`);
250
+ console.log(` artifact: ${artifact.path || "?"}`);
251
+ });
252
+ }
@@ -0,0 +1,22 @@
1
+ export type DaemonClient = typeof import("../daemon-client.ts");
2
+
3
+ export async function withDaemon<T>(
4
+ fn: (client: DaemonClient) => Promise<T>,
5
+ opts?: { message?: string; exitCode?: number }
6
+ ): Promise<T> {
7
+ const message = opts?.message ?? "Daemon not running. Start with: lattices app";
8
+ const exitCode = opts?.exitCode ?? 1;
9
+
10
+ const client = await import("../daemon-client.ts");
11
+ if (!(await client.isDaemonRunning())) {
12
+ console.error(message);
13
+ process.exit(exitCode);
14
+ }
15
+
16
+ try {
17
+ return await fn(client);
18
+ } catch (e: unknown) {
19
+ console.error(`Error: ${(e as Error).message}`);
20
+ process.exit(exitCode);
21
+ }
22
+ }
@@ -0,0 +1,105 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export interface ExecOpts {
4
+ encoding?: string;
5
+ stdio?: string | string[];
6
+ cwd?: string;
7
+ [key: string]: any;
8
+ }
9
+
10
+ export function run(cmd: string, opts: ExecOpts = {}): string {
11
+ return execSync(cmd, { encoding: "utf8", ...opts } as any).trim();
12
+ }
13
+
14
+ export function runQuiet(cmd: string): string | null {
15
+ try {
16
+ return run(cmd, { stdio: "pipe" });
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function parseFlagValue(args: string[], name: string): string | undefined {
23
+ const prefix = `--${name}=`;
24
+ const exact = `--${name}`;
25
+ for (let i = 0; i < args.length; i++) {
26
+ if (args[i].startsWith(prefix)) return args[i].slice(prefix.length);
27
+ if (args[i] === exact) return args[i + 1];
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ export function parseOptionalNumber(args: string[], ...names: string[]): number | undefined {
33
+ for (const name of names) {
34
+ const raw = parseFlagValue(args, name);
35
+ if (raw === undefined || raw === "") continue;
36
+ const value = Number(raw);
37
+ if (Number.isFinite(value)) return value;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ export function hasFlag(args: string[], name: string): boolean {
43
+ return args.includes(`--${name}`);
44
+ }
45
+
46
+ export function nonFlagArgs(args: string[]): string[] {
47
+ const valueFlags = new Set([
48
+ "id", "state", "ttl", "ttlMs", "x", "y", "gap", "placement", "style", "name", "scale",
49
+ "hud-url", "hudUrl", "hud-html", "hudHTML", "hudHtml", "hud-title", "hudTitle",
50
+ "hud-width", "hudWidth", "hud-height", "hudHeight", "width", "height",
51
+ "manifest", "root", "max-depth", "maxDepth", "read-access", "readAccess",
52
+ "pause", "limit", "session", "app", "name", "bundle-id", "bundleId", "bundleIdentifier",
53
+ "path", "app-path", "appPath", "title", "filename", "run-id", "runId",
54
+ "text", "tty", "wid", "treatment", "mode", "phase", "transport", "capture",
55
+ "appearance", "cursor-style", "cursorStyle", "shape", "marker-shape", "markerShape",
56
+ "cursor-shape", "cursorShape", "angle-deg", "angleDeg", "rotation-deg", "rotationDeg",
57
+ "rotation", "angle", "color", "duration-ms", "durationMs",
58
+ "type-interval-ms", "typeIntervalMs", "typing-interval-ms", "typingIntervalMs", "label",
59
+ "caption", "treatment-label", "treatmentLabel", "variant",
60
+ "caption-title", "captionTitle", "caption-body", "captionBody",
61
+ "caption-detail", "captionDetail", "caption-tags", "captionTags",
62
+ "caption-mode", "captionMode", "caption-eyebrow", "captionEyebrow",
63
+ "caption-lead-ms", "captionLeadMs", "caption-sound", "captionSound",
64
+ "caption-placement", "captionPlacement", "caption-margin", "captionMargin",
65
+ "caption-x", "captionX", "caption-y", "captionY",
66
+ "caption-x-ratio", "captionXRatio", "caption-y-ratio", "captionYRatio",
67
+ "caption-left-ratio", "captionLeftRatio", "caption-top-ratio", "captionTopRatio",
68
+ "sound", "sfx",
69
+ "trail", "effect", "path-style", "pathStyle", "motion", "easing", "velocity",
70
+ "trajectory", "curve", "arc", "glow", "bloom", "idle", "settle", "presence",
71
+ "edge", "edge-effect", "edgeEffect", "arrival",
72
+ "fps", "w", "h", "stop-file", "stopFile", "finished-file", "finishedFile",
73
+ "timeout-ms", "timeoutMs", "duration",
74
+ "x", "y", "x-ratio", "xRatio", "y-ratio", "yRatio",
75
+ "relative-x", "relativeX", "relative-y", "relativeY",
76
+ "window-x", "windowX", "window-y", "windowY", "button",
77
+ ]);
78
+ const out: string[] = [];
79
+ for (let i = 0; i < args.length; i++) {
80
+ const arg = args[i];
81
+ if (!arg.startsWith("--")) {
82
+ out.push(arg);
83
+ continue;
84
+ }
85
+ const flagName = arg.slice(2);
86
+ if (!arg.includes("=") && valueFlags.has(flagName)) i++;
87
+ }
88
+ return out;
89
+ }
90
+
91
+ export function relativeTime(iso: string): string {
92
+ const ms = Date.now() - new Date(iso).getTime();
93
+ const s = Math.floor(ms / 1000);
94
+ if (s < 60) return "just now";
95
+ const m = Math.floor(s / 60);
96
+ if (m < 60) return `${m}m ago`;
97
+ const h = Math.floor(m / 60);
98
+ if (h < 24) return `${h}h ago`;
99
+ const d = Math.floor(h / 24);
100
+ return `${d}d ago`;
101
+ }
102
+
103
+ export function pause(ms: number): Promise<void> {
104
+ return new Promise(resolve => setTimeout(resolve, ms));
105
+ }
@@ -0,0 +1,178 @@
1
+ import { withDaemon, type DaemonClient } from "./daemon.ts";
2
+
3
+ export async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
4
+ await withDaemon(async (client) => {
5
+ const { daemonCall } = client;
6
+
7
+ if (sub === "create") {
8
+ await layerCreateCommand(client, rest);
9
+ return;
10
+ }
11
+ if (sub === "snap") {
12
+ await layerSnapCommand(client, rest[0]);
13
+ return;
14
+ }
15
+ if (sub === "session" || sub === "sessions") {
16
+ await layerSessionCommand(client, rest[0]);
17
+ return;
18
+ }
19
+ if (sub === "clear") {
20
+ await daemonCall("session.layers.clear");
21
+ console.log("Cleared all session layers.");
22
+ return;
23
+ }
24
+ if (sub === "delete" || sub === "rm") {
25
+ if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
26
+ await daemonCall("session.layers.delete", { name: rest[0] });
27
+ console.log(`Deleted session layer "${rest[0]}".`);
28
+ return;
29
+ }
30
+
31
+ if (sub === undefined || sub === null || sub === "") {
32
+ const result = await daemonCall("layers.list") as any;
33
+ if (!result.layers.length) {
34
+ console.log("No layers configured.");
35
+ return;
36
+ }
37
+ console.log("Layers:\n");
38
+ for (const layer of result.layers) {
39
+ const active = layer.index === result.active ? " \x1b[32m● active\x1b[0m" : "";
40
+ console.log(` [${layer.index}] ${layer.label} (${layer.projectCount} projects)${active}`);
41
+ }
42
+ return;
43
+ }
44
+ const idx = parseInt(sub, 10);
45
+ if (!isNaN(idx)) {
46
+ await daemonCall("layer.activate", { index: idx, mode: "launch" });
47
+ console.log(`Activated layer ${idx}`);
48
+ } else {
49
+ await daemonCall("layer.activate", { name: sub, mode: "launch" });
50
+ console.log(`Activated layer "${sub}"`);
51
+ }
52
+ });
53
+ }
54
+
55
+ // ── Layer create: build a session layer from window specs ────────────
56
+ // Usage: lattices layer create <name> [wid:123 wid:456 ...]
57
+ // lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
58
+ export async function layerCreateCommand(client: DaemonClient, args: string[]): Promise<void> {
59
+ const { daemonCall } = client;
60
+ const name = args[0];
61
+ if (!name) {
62
+ console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
63
+ return;
64
+ }
65
+
66
+ const jsonIdx = args.indexOf("--json");
67
+ if (jsonIdx !== -1 && args[jsonIdx + 1]) {
68
+ // JSON mode: parse window specs with tile positions
69
+ const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
70
+ wid?: number; app?: string; title?: string; tile?: string;
71
+ }>;
72
+
73
+ // Collect wids, resolve app-based specs
74
+ const windowIds: number[] = [];
75
+ const windows: Array<{ app: string; contentHint?: string }> = [];
76
+ const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
77
+
78
+ for (const spec of specs) {
79
+ if (spec.wid) {
80
+ windowIds.push(spec.wid);
81
+ if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
82
+ } else if (spec.app) {
83
+ windows.push({ app: spec.app, contentHint: spec.title });
84
+ if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
85
+ }
86
+ }
87
+
88
+ await daemonCall("session.layers.create", {
89
+ name,
90
+ ...(windowIds.length ? { windowIds } : {}),
91
+ ...(windows.length ? { windows } : {}),
92
+ }) as any;
93
+
94
+ console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
95
+
96
+ // Apply tile positions
97
+ for (const t of tiles) {
98
+ try {
99
+ await daemonCall("window.place", {
100
+ ...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
101
+ placement: t.tile,
102
+ });
103
+ } catch { /* window may not be resolved yet */ }
104
+ }
105
+
106
+ if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
107
+ return;
108
+ }
109
+
110
+ // Simple wid mode: lattices layer create <name> wid:123 wid:456
111
+ const wids = args.slice(1)
112
+ .filter(a => a.startsWith("wid:"))
113
+ .map(a => parseInt(a.slice(4), 10))
114
+ .filter(n => !isNaN(n));
115
+
116
+ await daemonCall("session.layers.create", {
117
+ name,
118
+ ...(wids.length ? { windowIds: wids } : {}),
119
+ }) as any;
120
+
121
+ console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
122
+ }
123
+
124
+ // ── Layer snap: snapshot current visible windows into a session layer ─
125
+ export async function layerSnapCommand(client: DaemonClient, name?: string): Promise<void> {
126
+ const { daemonCall } = client;
127
+ const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
128
+
129
+ // Get all current windows
130
+ const windows = await daemonCall("windows.list") as any[];
131
+ const visibleWids = windows
132
+ .filter((w: any) => !w.isMinimized && w.app !== "lattices")
133
+ .map((w: any) => w.wid);
134
+
135
+ if (!visibleWids.length) {
136
+ console.log("No visible windows to snapshot.");
137
+ return;
138
+ }
139
+
140
+ await daemonCall("session.layers.create", {
141
+ name: layerName,
142
+ windowIds: visibleWids,
143
+ });
144
+
145
+ console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
146
+ }
147
+
148
+ // ── Layer session: list or switch session layers ─────────────────────
149
+ export async function layerSessionCommand(client: DaemonClient, nameOrIndex?: string): Promise<void> {
150
+ const { daemonCall } = client;
151
+ const result = await daemonCall("session.layers.list") as any;
152
+
153
+ if (!nameOrIndex) {
154
+ // List session layers
155
+ if (!result.layers.length) {
156
+ console.log("No session layers. Create one with: lattices layer create <name>");
157
+ return;
158
+ }
159
+ console.log("Session layers:\n");
160
+ for (let i = 0; i < result.layers.length; i++) {
161
+ const l = result.layers[i];
162
+ const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
163
+ const winCount = l.windows?.length || 0;
164
+ console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
165
+ }
166
+ return;
167
+ }
168
+
169
+ // Switch by index or name
170
+ const idx = parseInt(nameOrIndex, 10);
171
+ if (!isNaN(idx)) {
172
+ await daemonCall("session.layers.switch", { index: idx });
173
+ console.log(`Switched to session layer ${idx}.`);
174
+ } else {
175
+ await daemonCall("session.layers.switch", { name: nameOrIndex });
176
+ console.log(`Switched to session layer "${nameOrIndex}".`);
177
+ }
178
+ }
@@ -0,0 +1,43 @@
1
+ import { hasFlag, nonFlagArgs, parseFlagValue } from "./helpers.ts";
2
+ import { withDaemon } from "./daemon.ts";
3
+
4
+ function runLine(run: any): string {
5
+ const count = Array.isArray(run.artifacts) ? run.artifacts.length : 0;
6
+ const completed = run.completedAt ? ` completed=${run.completedAt}` : "";
7
+ return ` ${run.id} ${run.state || "?"} artifacts=${count} ${run.title || "Untitled run"}${completed}`;
8
+ }
9
+
10
+ export async function runsCommand(rawArgs: string[] = []): Promise<void> {
11
+ const jsonFlag = hasFlag(rawArgs, "json");
12
+ const positional = nonFlagArgs(rawArgs);
13
+ const sub = positional[0];
14
+
15
+ await withDaemon(async ({ daemonCall }) => {
16
+ if (sub && sub !== "list") {
17
+ const run = await daemonCall("runs.get", { id: sub }) as any;
18
+ if (jsonFlag) {
19
+ console.log(JSON.stringify(run, null, 2));
20
+ return;
21
+ }
22
+ console.log(runLine(run));
23
+ console.log(` artifacts: ${run.artifactDirectoryPath}`);
24
+ for (const artifact of run.artifacts || []) {
25
+ console.log(` ${artifact.kind} ${artifact.path}`);
26
+ }
27
+ return;
28
+ }
29
+
30
+ const limit = Number(parseFlagValue(rawArgs, "limit") || 20);
31
+ const runs = await daemonCall("runs.list", { limit }) as any[];
32
+ if (jsonFlag) {
33
+ console.log(JSON.stringify(runs, null, 2));
34
+ return;
35
+ }
36
+ if (!runs.length) {
37
+ console.log("No runs yet.");
38
+ return;
39
+ }
40
+ console.log(`Runs (${runs.length}):\n`);
41
+ for (const run of runs) console.log(runLine(run));
42
+ });
43
+ }