@lattices/cli 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +14 -5
  2. package/apps/mac/Info.plist +4 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -2
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  6. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +11 -0
  7. package/apps/mac/Lattices.entitlements +6 -0
  8. package/bin/assistant-intelligence.ts +41 -3
  9. package/bin/cli/capture.ts +252 -0
  10. package/bin/cli/daemon.ts +22 -0
  11. package/bin/cli/helpers.ts +105 -0
  12. package/bin/cli/layer.ts +178 -0
  13. package/bin/cli/runs.ts +43 -0
  14. package/bin/cli/search.ts +141 -0
  15. package/bin/cli/session.ts +32 -0
  16. package/bin/client.ts +2 -1
  17. package/bin/cua.ts +26 -0
  18. package/bin/infer.ts +22 -4
  19. package/bin/keychain.ts +75 -0
  20. package/bin/lattices-app.ts +111 -12
  21. package/bin/lattices-build-env.ts +77 -0
  22. package/bin/lattices-dev +29 -2
  23. package/bin/lattices.ts +729 -769
  24. package/docs/api.md +496 -3
  25. package/docs/app.md +5 -4
  26. package/docs/assistant-knowledge.md +130 -0
  27. package/docs/config.md +5 -0
  28. package/docs/hyperspace-grid-snappiness.md +210 -0
  29. package/docs/layers.md +53 -0
  30. package/docs/mouse-gestures.md +40 -3
  31. package/docs/ocr.md +3 -0
  32. package/docs/prompts/hands-off-system.md +9 -1
  33. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  34. package/docs/proposals/{LAT-006-mira-in-lattices.md → LAT-006-runs-and-capture-in-lattices.md} +83 -70
  35. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  36. package/docs/quickstart.md +3 -1
  37. package/docs/reference/dewey.config.ts +1 -1
  38. package/docs/release.md +4 -3
  39. package/docs/repo-structure.md +1 -0
  40. package/docs/terminal-kit.md +87 -0
  41. package/docs/tiling-reference.md +5 -3
  42. package/docs/voice.md +3 -3
  43. package/package.json +29 -5
  44. package/packages/npm/sdk/cua.d.mts +1 -0
  45. package/packages/npm/sdk/cua.d.ts +188 -0
  46. package/packages/npm/sdk/cua.mjs +376 -0
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { join, resolve } from "node:path";
7
7
  import { get } from "node:https";
8
8
  import type { IncomingMessage } from "node:http";
9
+ import { resolveBuildEnv } from "./lattices-build-env";
9
10
 
10
11
  const __dirname = import.meta.dir;
11
12
  const appDir = resolve(__dirname, "../apps/mac");
@@ -37,6 +38,50 @@ function isRunning(): boolean {
37
38
  }
38
39
  }
39
40
 
41
+ type RunningProcess = { pid: string; command: string };
42
+
43
+ function runningLatticesProcesses(): RunningProcess[] {
44
+ let pids: string[];
45
+ try {
46
+ pids = execFileSync("pgrep", ["-x", "Lattices"], {
47
+ encoding: "utf8",
48
+ stdio: ["ignore", "pipe", "ignore"],
49
+ }).trim().split(/\s+/).filter(Boolean);
50
+ } catch {
51
+ return [];
52
+ }
53
+
54
+ const processes: RunningProcess[] = [];
55
+ for (const pid of pids) {
56
+ try {
57
+ const command = execFileSync("ps", ["-p", pid, "-o", "command="], {
58
+ encoding: "utf8",
59
+ stdio: ["ignore", "pipe", "ignore"],
60
+ }).trim();
61
+ if (command) processes.push({ pid, command });
62
+ } catch {}
63
+ }
64
+ return processes;
65
+ }
66
+
67
+ function runningBundleProcesses(targetBundlePath = bundlePath): RunningProcess[] {
68
+ const executable = resolve(targetBundlePath, "Contents/MacOS/Lattices");
69
+ return runningLatticesProcesses().filter(({ command }) =>
70
+ command === executable || command.startsWith(`${executable} `)
71
+ );
72
+ }
73
+
74
+ function requireBundleNotRunningForBuild(): void {
75
+ const running = runningBundleProcesses();
76
+ if (!running.length) return;
77
+
78
+ console.error("Refusing to rebuild Lattices.app while that bundle is running.");
79
+ console.error("Rewriting or re-signing a live Mach-O can make macOS kill it later with Code Signature Invalid.");
80
+ console.error(`Running PID(s): ${running.map((proc) => proc.pid).join(", ")}`);
81
+ console.error("Use `lattices app restart` to quit, rebuild, and relaunch, or run `lattices app quit` before `lattices app build`.");
82
+ process.exit(1);
83
+ }
84
+
40
85
  function sleep(ms: number): void {
41
86
  execFileSync("/bin/sleep", [(ms / 1000).toString()], { stdio: "ignore" });
42
87
  }
@@ -213,34 +258,73 @@ function resolveSigningIdentity(): string | null {
213
258
  }
214
259
  }
215
260
 
261
+ function bundleTeamIdentifier(): string | null {
262
+ try {
263
+ const output = execSync(`codesign -dv '${bundlePath}' 2>&1`, { encoding: "utf8" });
264
+ const match = output.match(/TeamIdentifier=(.+)/);
265
+ const team = match?.[1]?.trim();
266
+ return team && team !== "not set" ? team : null;
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
271
+
272
+ function runCodesign(args: string[]): void {
273
+ try {
274
+ execFileSync("codesign", args, { stdio: "pipe" });
275
+ } catch (error) {
276
+ const stderr = (error as { stderr?: Buffer | string }).stderr;
277
+ const detail = stderr ? `\n${String(stderr).trim()}` : "";
278
+ throw new Error(`codesign ${args.join(" ")}${detail}`);
279
+ }
280
+ }
281
+
216
282
  function signBundle(): void {
217
283
  const identity = resolveSigningIdentity();
218
- const entFlag = existsSync(entitlementsPath) ? ` --entitlements '${entitlementsPath}'` : "";
284
+ const entitlements = existsSync(entitlementsPath) ? entitlementsPath : null;
219
285
  const tempBinaryPath = `${binaryPath}.cstemp`;
220
286
 
221
287
  try {
222
288
  if (existsSync(tempBinaryPath)) rmSync(tempBinaryPath, { force: true });
223
289
  } catch {}
224
290
 
291
+ const sign = (signer: string, label: string) => {
292
+ // Sign the Mach-O first, then the bundle — more reliable than --deep and
293
+ // keeps a stable TeamIdentifier so macOS TCC grants survive rebuilds.
294
+ const binaryArgs = ["--force", "--options", "runtime", "--sign", signer];
295
+ if (entitlements) binaryArgs.push("--entitlements", entitlements);
296
+ binaryArgs.push(binaryPath);
297
+ runCodesign(binaryArgs);
298
+
299
+ const bundleArgs = ["--force", "--options", "runtime", "--sign", signer];
300
+ if (entitlements) bundleArgs.push("--entitlements", entitlements);
301
+ bundleArgs.push("--identifier", "dev.lattices.app", bundlePath);
302
+ runCodesign(bundleArgs);
303
+
304
+ const team = bundleTeamIdentifier();
305
+ if (team) {
306
+ console.log(`Signed (${label}) — TeamIdentifier=${team}`);
307
+ return;
308
+ }
309
+ if (label === "ad-hoc") return;
310
+ throw new Error(`codesign reported no TeamIdentifier after ${label} signing`);
311
+ };
312
+
225
313
  if (identity) {
226
314
  console.log(`Signing with: ${identity}`);
227
315
  try {
228
- execSync(
229
- `codesign --force --options runtime --deep --sign '${identity}'${entFlag} --identifier dev.lattices.app '${bundlePath}'`,
230
- { stdio: "pipe" }
231
- );
316
+ sign(identity, "developer");
232
317
  return;
233
- } catch {
234
- console.log(`Warning: signing with '${identity}' failed — falling back to ad-hoc.`);
318
+ } catch (error) {
319
+ console.log(`Warning: signing with '${identity}' failed — ${(error as Error).message}`);
320
+ console.log("Warning: falling back to ad-hoc. Privacy permissions will not persist across rebuilds.");
235
321
  }
236
322
  } else {
237
323
  console.log("Warning: no local signing identity found — falling back to ad-hoc.");
324
+ console.log("Warning: grant Accessibility/Screen Recording again after each rebuild, or install a signing cert.");
238
325
  }
239
326
 
240
- execSync(
241
- `codesign --force --options runtime --deep --sign -${entFlag} --identifier dev.lattices.app '${bundlePath}'`,
242
- { stdio: "pipe" }
243
- );
327
+ sign("-", "ad-hoc");
244
328
 
245
329
  try {
246
330
  if (existsSync(tempBinaryPath)) rmSync(tempBinaryPath, { force: true });
@@ -313,6 +397,8 @@ ${buildMetadata} <key>LSMinimumSystemVersion</key>
313
397
  <true/>
314
398
  <key>NSHighResolutionCapable</key>
315
399
  <true/>
400
+ <key>NSMicrophoneUsageDescription</key>
401
+ <string>Lattices uses the microphone for Hudson Voice dictation and voice commands.</string>
316
402
  <key>NSSupportsAutomaticTermination</key>
317
403
  <true/>
318
404
  </dict>
@@ -329,6 +415,14 @@ function syncBundleResources(): void {
329
415
  if (existsSync(tapSoundPath)) {
330
416
  execSync(`cp '${tapSoundPath}' '${resolve(resourcesDir, "tap.wav")}'`);
331
417
  }
418
+ // Bundle the assistant knowledge base so the in-app chat assistant can load it
419
+ // in shipped builds (dev builds fall back to the repo docs/ path).
420
+ const assistantDoc = resolve(cliRoot, "docs/assistant-knowledge.md");
421
+ if (existsSync(assistantDoc)) {
422
+ const docsDir = resolve(resourcesDir, "docs");
423
+ mkdirSync(docsDir, { recursive: true });
424
+ execSync(`cp '${assistantDoc}' '${resolve(docsDir, "assistant-knowledge.md")}'`);
425
+ }
332
426
  }
333
427
 
334
428
  // ── Build from source (current arch only) ────────────────────────────
@@ -339,6 +433,10 @@ function buildFromSource(): boolean {
339
433
  execSync("swift build -c release", {
340
434
  cwd: appDir,
341
435
  stdio: "inherit",
436
+ // Build features are declared in apps/mac/build.json and resolved to the
437
+ // HUDSONKIT_WITH_* env HudsonKit gates on — one source of truth across
438
+ // every build entrypoint (see bin/lattices-build-env.ts).
439
+ env: { ...process.env, ...resolveBuildEnv() },
342
440
  });
343
441
  } catch {
344
442
  return false;
@@ -466,7 +564,7 @@ async function ensureBinary(): Promise<void> {
466
564
  console.error(
467
565
  "Could not find a bundled lattices app or download one.\n" +
468
566
  "Options:\n" +
469
- " \u2022 Reinstall or update @lattices/cli\n" +
567
+ " \u2022 Reinstall or update @arach/lattices\n" +
470
568
  " \u2022 Developers can build from source with: lattices-app build\n" +
471
569
  " \u2022 Download manually from: https://github.com/" + REPO + "/releases"
472
570
  );
@@ -517,6 +615,7 @@ const shouldDetachUpdate = flags.includes("--detach");
517
615
  const isUpdateWorker = flags.includes("--worker");
518
616
 
519
617
  if (cmd === "build") {
618
+ requireBundleNotRunningForBuild();
520
619
  if (!hasSwift()) {
521
620
  console.error("Swift is required. Install with: xcode-select --install");
522
621
  process.exit(1);
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * lattices-build-env — declarative build-feature resolver.
4
+ *
5
+ * Mirrors openscout's `hkit` style: an app names *features* in a manifest
6
+ * (apps/mac/build.json), never raw env vars. The feature → build-env mapping
7
+ * lives in the catalog below, so every build entrypoint (package / dev / dist)
8
+ * resolves the same way from one source of truth, instead of each hardcoding
9
+ * its own `HUDSONKIT_WITH_*` flags.
10
+ *
11
+ * import { resolveBuildEnv } from "./lattices-build-env"; // TS callers
12
+ * eval "$(bun bin/lattices-build-env.ts shell)" // bash callers
13
+ * bun bin/lattices-build-env.ts json // inspect
14
+ */
15
+
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+
19
+ // Feature catalog: feature name -> build env HudsonKit gates on at SwiftPM
20
+ // manifest-eval time. HudsonVoice (Vox/Parakeet dictation) is an optional
21
+ // backend HudsonKit only declares when HUDSONKIT_WITH_VOICE=1 is set at build
22
+ // time. Name a *feature* here; never sprinkle the env var across build scripts.
23
+ export const FEATURE_CATALOG: Record<string, { env: Record<string, string>; note: string }> = {
24
+ voice: { env: { HUDSONKIT_WITH_VOICE: "1" }, note: "HudsonVoice — Vox/Parakeet dictation" },
25
+ };
26
+
27
+ export interface BuildManifest {
28
+ app?: string;
29
+ features?: string[];
30
+ }
31
+
32
+ const MANIFEST_PATH = join(import.meta.dir, "../apps/mac/build.json");
33
+
34
+ export function loadManifest(path = MANIFEST_PATH): BuildManifest {
35
+ if (!existsSync(path)) return {};
36
+ return JSON.parse(readFileSync(path, "utf8")) as BuildManifest;
37
+ }
38
+
39
+ export function resolveFeatureEnv(features: string[] = []): Record<string, string> {
40
+ const env: Record<string, string> = {};
41
+ for (const f of features) {
42
+ const entry = FEATURE_CATALOG[f];
43
+ if (!entry) {
44
+ throw new Error(
45
+ `unknown build feature "${f}" — known features: ${Object.keys(FEATURE_CATALOG).join(", ")}`,
46
+ );
47
+ }
48
+ Object.assign(env, entry.env);
49
+ }
50
+ return env;
51
+ }
52
+
53
+ /** Resolve the manifest's declared features into a build-env map. */
54
+ export function resolveBuildEnv(manifestPath?: string): Record<string, string> {
55
+ const features = loadManifest(manifestPath).features ?? [];
56
+ const env = resolveFeatureEnv(features);
57
+ // HudsonKit enables HudsonVoice by default; opt out unless the voice feature is declared.
58
+ if (!features.includes("voice")) {
59
+ env.HUDSONKIT_WITH_VOICE = "0";
60
+ }
61
+ return env;
62
+ }
63
+
64
+ // --- CLI: emit the resolved env for shell / json consumers -------------------
65
+ if (import.meta.main) {
66
+ const mode = process.argv[2] ?? "shell";
67
+ const env = resolveBuildEnv();
68
+ if (mode === "json") {
69
+ console.log(JSON.stringify(env, null, 2));
70
+ } else if (mode === "shell") {
71
+ // eval-able by bash: `eval "$(bun bin/lattices-build-env.ts shell)"`
72
+ for (const [k, v] of Object.entries(env)) console.log(`export ${k}=${JSON.stringify(v)}`);
73
+ } else {
74
+ console.error(`lattices-build-env: unknown mode "${mode}" (use: shell | json)`);
75
+ process.exit(1);
76
+ }
77
+ }
package/bin/lattices-dev CHANGED
@@ -105,8 +105,9 @@ clear_lattices_drag_cache() {
105
105
  done
106
106
  }
107
107
 
108
- is_install_bundle_running() {
109
- local target="$INSTALL_BUNDLE/Contents/MacOS/Lattices"
108
+ is_bundle_running() {
109
+ local bundle="$1"
110
+ local target="$bundle/Contents/MacOS/Lattices"
110
111
  local pid args
111
112
 
112
113
  while IFS= read -r pid; do
@@ -120,6 +121,26 @@ is_install_bundle_running() {
120
121
  return 1
121
122
  }
122
123
 
124
+ is_install_bundle_running() {
125
+ is_bundle_running "$INSTALL_BUNDLE"
126
+ }
127
+
128
+ require_build_targets_not_running() {
129
+ if is_bundle_running "$BUNDLE"; then
130
+ red "Refusing to rebuild the repo-local app artifact while it is running."
131
+ dim "Rewriting or re-signing a live Mach-O can make macOS kill it later with Code Signature Invalid."
132
+ dim "Use \`lattices-dev restart\` to quit, rebuild, and relaunch."
133
+ exit 1
134
+ fi
135
+
136
+ if is_install_bundle_running; then
137
+ red "Refusing to install over the running dev app bundle."
138
+ dim "Rewriting or re-signing a live Mach-O can make macOS kill it later with Code Signature Invalid."
139
+ dim "Use \`lattices-dev restart\` or \`lattices app restart\` to quit, rebuild, and relaunch."
140
+ exit 1
141
+ fi
142
+ }
143
+
123
144
  write_info_plist() {
124
145
  mkdir -p "$BUNDLE/Contents"
125
146
  cat > "$BUNDLE/Contents/Info.plist" <<PLIST
@@ -168,6 +189,8 @@ write_info_plist() {
168
189
  <true/>
169
190
  <key>NSHighResolutionCapable</key>
170
191
  <true/>
192
+ <key>NSMicrophoneUsageDescription</key>
193
+ <string>Lattices uses the microphone for Hudson Voice dictation and voice commands.</string>
171
194
  <key>NSSupportsAutomaticTermination</key>
172
195
  <true/>
173
196
  </dict>
@@ -177,6 +200,10 @@ PLIST
177
200
 
178
201
  cmd_build() {
179
202
  echo "Building dev app..."
203
+ require_build_targets_not_running
204
+ # Build features are declared in apps/mac/build.json and resolved to the
205
+ # HUDSONKIT_WITH_* env HudsonKit gates on (see bin/lattices-build-env.ts).
206
+ eval "$(bun "$ROOT/bin/lattices-build-env.ts" shell)"
180
207
  cd "$APP_DIR" && swift build -c release
181
208
  # Build into a repo-local dev artifact, then install to a stable app path
182
209
  # that macOS permissions can trust across rebuilds.