@hraness/slopcamera 3.2.6 → 3.2.8

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 (78) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +41 -13
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +0 -9
  6. package/apps/desktop/application/default-registry.ts +0 -10
  7. package/apps/desktop/application/operation.ts +1 -5
  8. package/apps/desktop/application/operations/index.ts +0 -1
  9. package/apps/desktop/cli/args.ts +22 -86
  10. package/apps/desktop/cli/command-host-resources.ts +5 -1
  11. package/apps/desktop/cli/commands.ts +37 -67
  12. package/apps/desktop/cli/help.ts +24 -20
  13. package/apps/desktop/cli/html-scene.ts +479 -0
  14. package/apps/desktop/cli/io.ts +2 -1
  15. package/apps/desktop/cli/main.ts +28 -47
  16. package/apps/desktop/cli/media-ingest-model.ts +2 -0
  17. package/apps/desktop/cli/media-ingest-program.ts +9 -4
  18. package/apps/desktop/cli/menubar.ts +244 -0
  19. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  20. package/apps/desktop/cli/portable-surface.ts +13 -4
  21. package/apps/desktop/cli/root-help-intro.ts +14 -0
  22. package/apps/desktop/code/application-node-planner.ts +0 -47
  23. package/apps/desktop/code/host-source-layout.ts +46 -0
  24. package/apps/desktop/code/public.ts +0 -1
  25. package/apps/desktop/code/runtime-identity.ts +50 -8
  26. package/apps/desktop/code/semantic-builder.ts +0 -43
  27. package/apps/desktop/code/source-bundle.ts +23 -6
  28. package/apps/desktop/code/source-typecheck.ts +131 -43
  29. package/apps/desktop/code/worker-client.ts +14 -2
  30. package/apps/desktop/core/music-analysis.ts +124 -1
  31. package/apps/desktop/dist/cli/main.js +261 -248
  32. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  33. package/apps/desktop/html-overlay/contracts.ts +3 -0
  34. package/apps/desktop/html-overlay/index.ts +28 -0
  35. package/apps/desktop/html-overlay/music-clock.ts +126 -0
  36. package/apps/desktop/html-overlay/rigged-glb.ts +429 -0
  37. package/apps/desktop/html-overlay/runtime.ts +4 -0
  38. package/apps/desktop/html-overlay/scaffolds.ts +2 -1
  39. package/apps/desktop/html-overlay/scene.ts +76 -0
  40. package/dist/cli.js +83 -81
  41. package/dist/{index-jchqst8w.js → index-7131pg1c.js} +43 -236
  42. package/dist/{index-mcy8z0br.js → index-fava6pge.js} +1 -1
  43. package/dist/{index-z7239b4h.js → index-h1k0fnjq.js} +1 -1
  44. package/dist/{index-p63wavx0.js → index-zfnddgay.js} +1 -1
  45. package/dist/index.js +4 -18
  46. package/dist/operations.js +2 -2
  47. package/dist/vectorize/worker.js +1 -1
  48. package/dist/workflow.js +3 -3
  49. package/docs/README.md +2 -1
  50. package/docs/studio.md +1 -1
  51. package/examples/html/music-video.html +514 -0
  52. package/examples/html/music-video.json +16 -0
  53. package/package.json +8 -21
  54. package/skills/slopcamera/SKILL.md +10 -1
  55. package/skills/slopcamera/references/diagrams.md +5 -14
  56. package/skills/slopcamera/references/install.md +2 -2
  57. package/skills/slopcamera/references/music-video.md +169 -0
  58. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  59. package/skills/slopcamera/references/support.md +25 -0
  60. package/skills/slopcamera/references/video-projects.md +6 -2
  61. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  62. package/skills/slopcamera/references/workflows-sdk.md +1 -1
  63. package/src/cli.ts +28 -98
  64. package/src/index.ts +0 -23
  65. package/src/support-completion.ts +16 -0
  66. package/src/support.ts +42 -0
  67. package/src/version.ts +1 -1
  68. package/apps/desktop/application/operations/recording/index.ts +0 -5
  69. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  70. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  71. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  72. package/apps/desktop/application/operations/recording/start.ts +0 -169
  73. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  74. package/apps/desktop/capture/protocol.ts +0 -689
  75. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  76. package/apps/desktop/cli/recording-controller.ts +0 -1567
  77. package/apps/desktop/cli/recording-daemon.ts +0 -931
  78. package/src/desktop.ts +0 -253
@@ -67,7 +67,7 @@ function removeStagedImport(
67
67
  }));
68
68
  }
69
69
 
70
- function stageImport(projectDirectory: string, sourcePath: string): Program<StagedMediaImport> {
70
+ function stageImport(projectDirectory: string, sourcePath: string, maximumBytes: number): Program<StagedMediaImport> {
71
71
  return Effect.gen(function*() {
72
72
  const platform = yield* MediaIngestPlatform;
73
73
  const source = yield* platform.sourcePath(sourcePath);
@@ -78,8 +78,8 @@ function stageImport(projectDirectory: string, sourcePath: string): Program<Stag
78
78
  if (before.isSymbolicLink() || !before.isFile()) {
79
79
  throw new CliError("unsafe-path", `Media source must be a physical regular file: ${sourcePath}`);
80
80
  }
81
- if (before.size <= 0 || before.size > MAX_MEDIA_BYTES) {
82
- throw new CliError("invalid-data", `Media source must contain 1 through ${MAX_MEDIA_BYTES} bytes.`);
81
+ if (before.size <= 0 || before.size > maximumBytes) {
82
+ throw new CliError("invalid-data", `Media source must contain 1 through ${maximumBytes} bytes.`);
83
83
  }
84
84
  });
85
85
  const imports = yield* privateImportsDirectory(projectDirectory);
@@ -178,6 +178,11 @@ function commitImport(imported: StagedMediaImport): Program<boolean> {
178
178
  export function ingestProjectMediaProgram(options: IngestProjectMediaOptions): Program<IngestedProjectMedia> {
179
179
  return Effect.gen(function*() {
180
180
  const platform = yield* MediaIngestPlatform;
181
+ const maximumBytes = yield* operationValidation("media", () => {
182
+ const bound = options.maximumBytes ?? MAX_MEDIA_BYTES;
183
+ if (!Number.isSafeInteger(bound) || bound < 1 || bound > MAX_MEDIA_BYTES) throw new CliError("invalid-data", "Invalid media import byte bound.");
184
+ return bound;
185
+ });
181
186
  const role = yield* operationValidation("media", () => ProjectAssetRoleSchema.parse(options.role));
182
187
  const [repositoryRoot, projectDirectory] = yield* Effect.all([
183
188
  platform.realpath(options.repositoryRoot), platform.realpath(options.projectDirectory),
@@ -186,7 +191,7 @@ export function ingestProjectMediaProgram(options: IngestProjectMediaOptions): P
186
191
  })));
187
192
  yield* platform.assertWithin(repositoryRoot, projectDirectory, "Project media import directory is outside the repository.");
188
193
  return yield* operationResource(
189
- stageImport(projectDirectory, options.sourcePath),
194
+ stageImport(projectDirectory, options.sourcePath, maximumBytes),
190
195
  imported => Effect.gen(function*() {
191
196
  const probe = yield* probeProjectMediaProgram(options.ffprobe, options.runner, imported.temporaryPath);
192
197
  const location = yield* platform.assetLocation(repositoryRoot, imported.absolutePath, options.sourcePath);
@@ -0,0 +1,244 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import type { Stats } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { CliError } from "./errors";
5
+ import type { CliIo } from "./io";
6
+ import { writeJson, writeLine } from "./io";
7
+ import { ensurePrivateDirectory } from "./paths";
8
+
9
+ const SETTLE_MS = 400;
10
+ const LABEL = "com.hraness.slopcamera.menubar";
11
+
12
+ function xml(value: string): string { return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;"); }
13
+ function exists(path: string): boolean { try { const info = lstatSync(path); return isAbsolute(path) && realpathSync(path) === resolve(path) && (info.uid === process.getuid?.() || info.uid === 0) && info.isFile() && (info.mode & 0o111) !== 0 && (info.mode & 0o022) === 0; } catch { return false; } }
14
+
15
+ /** The directory agents write user-facing outputs into. */
16
+ export function outputsRoot(stateRoot: string): string {
17
+ const productRoot = stateRoot.endsWith(`${sep}cli`) ? dirname(stateRoot) : stateRoot;
18
+ return join(productRoot, "outputs");
19
+ }
20
+ export function resolveMenubarBinary(repositoryRoot: string, environment: Readonly<Record<string, string | undefined>> = process.env): string | null {
21
+ const explicit = environment.SLOPCAMERA_MENUBAR;
22
+ if (explicit !== undefined) return explicit !== "" && exists(explicit) ? explicit : null;
23
+ const candidates = [ resolve(dirname(process.execPath), "slopcamera-menubar"), ...(environment.HOME === undefined || environment.HOME === "" ? [] : [installedBinaryPath(environment)]), resolve(repositoryRoot, "desktop", "target", "release", "slopcamera-menubar")];
24
+ for (const candidate of candidates) if (candidate !== undefined && candidate !== "" && exists(candidate)) return candidate;
25
+ return null;
26
+ }
27
+ export function launchAgentPath(environment: Readonly<Record<string, string | undefined>> = process.env): string {
28
+ const home = environment.HOME; if (home === undefined || home === "" || !isAbsolute(home)) throw new CliError("unavailable", "HOME is required for a per-user LaunchAgent.");
29
+ return join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
30
+ }
31
+ export function installedBinaryPath(environment: Readonly<Record<string, string | undefined>> = process.env): string {
32
+ const home = environment.HOME; if (home === undefined || home === "" || !isAbsolute(home)) throw new CliError("unavailable", "HOME is required for a per-user menu-bar install.");
33
+ return join(home, "Library", "Application Support", "Slopcamera", "menubar", "slopcamera-menubar");
34
+ }
35
+ export function launchAgentPlist(binary: string): string {
36
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${xml(LABEL)}</string><key>ProgramArguments</key><array><string>${xml(binary)}</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><false/><key>ProcessType</key><string>Interactive</string><key>LimitLoadToSessionType</key><string>Aqua</string></dict></plist>\n`;
37
+ }
38
+ type Environment = Readonly<Record<string, string | undefined>>;
39
+ const MAX_BINARY_BYTES = 128 * 1024 * 1024;
40
+
41
+ function infoAt(path: string): Stats | null {
42
+ try { return lstatSync(path); } catch (error) {
43
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ /** Existing shared directories retain their permissions; links are never followed. */
49
+ function checkParents(path: string, environment: Environment, create = false): void {
50
+ const home = environment.HOME;
51
+ if (!home || !isAbsolute(home) || realpathSync(home) !== resolve(home)) {
52
+ throw new CliError("unsafe-path", "HOME must be an absolute physical directory.");
53
+ }
54
+ const suffix = relative(home, dirname(path));
55
+ if (suffix.startsWith(`..${sep}`) || suffix === ".." || isAbsolute(suffix)) {
56
+ throw new CliError("unsafe-path", "The menu-bar install must remain under HOME.");
57
+ }
58
+ let cursor = home;
59
+ for (const part of ["", ...suffix.split(sep)]) {
60
+ if (part) cursor = join(cursor, part);
61
+ let info = infoAt(cursor);
62
+ if (info === null) {
63
+ if (!create) return;
64
+ mkdirSync(cursor, { mode: 0o700 });
65
+ info = lstatSync(cursor);
66
+ }
67
+ if (!info.isDirectory() || info.uid !== process.getuid?.() || (info.mode & 0o022) !== 0) {
68
+ throw new CliError("unsafe-path", "Menu-bar install directories must be owned physical directories without shared write access.");
69
+ }
70
+ }
71
+ }
72
+
73
+ function assertOwnedFile(path: string): void {
74
+ const info = infoAt(path);
75
+ if (info !== null && (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || (info.mode & 0o022) !== 0)) {
76
+ throw new CliError("unsafe-path", "The menu-bar install contains an unsafe file.");
77
+ }
78
+ }
79
+
80
+ function readRegular(path: string, maximum: number, executable = false): Buffer {
81
+ if (!isAbsolute(path) || realpathSync(path) !== resolve(path) || Buffer.byteLength(path) > 4096 || /[\u0000-\u001f\u007f{}"\\]/u.test(path)) {
82
+ throw new CliError("unsafe-path", "The menu-bar file must have a bounded physical absolute path.");
83
+ }
84
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
85
+ try {
86
+ const info = fstatSync(fd);
87
+ if (!info.isFile() || (info.uid !== process.getuid?.() && info.uid !== 0) || info.size > maximum || info.size === 0 || (info.mode & 0o022) !== 0 || (executable && (info.mode & 0o111) === 0)) {
88
+ throw new CliError("unsafe-path", "The menu-bar file must be bounded, regular, and protected from shared writes.");
89
+ }
90
+ const bytes = Buffer.alloc(info.size + 1);
91
+ let count = 0;
92
+ while (count < bytes.length) {
93
+ const read = readSync(fd, bytes, count, bytes.length - count, count);
94
+ if (read === 0) break;
95
+ count += read;
96
+ }
97
+ const after = fstatSync(fd);
98
+ if (count !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs || after.ctimeMs !== info.ctimeMs) {
99
+ throw new CliError("conflict", "The menu-bar file changed while it was being read.");
100
+ }
101
+ return bytes.subarray(0, count);
102
+ } finally { closeSync(fd); }
103
+ }
104
+
105
+ function readAgent(path: string, environment: Environment): string | null {
106
+ checkParents(path, environment);
107
+ if (infoAt(path) === null) return null;
108
+ assertOwnedFile(path);
109
+ return readRegular(path, 16 * 1024).toString("utf8");
110
+ }
111
+
112
+ /** Configuration state only; it does not claim the launchd service is running. */
113
+ export type LaunchAgentState = "absent" | "installed" | "conflict";
114
+ export type LaunchctlRunner = (args: readonly string[], allowMissing?: boolean) => string | null;
115
+ function launchctl(args: readonly string[], allowMissing = false): string | null {
116
+ const uid = process.getuid?.();
117
+ if (uid === undefined) throw new CliError("unavailable", "The current user has no launchd GUI domain.");
118
+ const result = Bun.spawnSync(["/bin/launchctl", ...args.map((arg) => arg.replaceAll("{uid}", String(uid)))], { stdin: "ignore", stdout: "pipe", stderr: "pipe", timeout: 10_000, maxBuffer: 64 * 1024 });
119
+ return interpretLaunchctlResult(result, uid, allowMissing);
120
+ }
121
+ export function interpretLaunchctlResult(result: { success: boolean; exitCode: number; stdout: Uint8Array; stderr: Uint8Array }, uid: number, allowMissing: boolean): string | null {
122
+ if (result.stdout.byteLength + result.stderr.byteLength > 64 * 1024) throw new CliError("unavailable", "launchctl exceeded the bounded response size.");
123
+ if (result.success && result.exitCode === 0) return Buffer.from(result.stdout).toString();
124
+ if (allowMissing && result.exitCode === 113 && result.stdout.length === 0 && Buffer.from(result.stderr).toString() === `Bad request.\nCould not find service "${LABEL}" in domain for user gui: ${uid}\n`) return null;
125
+ throw new CliError("unavailable", "launchctl could not reconcile the Slopcamera menu-bar LaunchAgent. Run menubar install again to retry.");
126
+ }
127
+
128
+ function loadedOwned(path: string, binary: string, current: string | null, run: LaunchctlRunner): boolean {
129
+ const output = run(["print", `gui/{uid}/${LABEL}`], true);
130
+ if (output === null) return false;
131
+ const uid = process.getuid?.();
132
+ if (current === null || Buffer.byteLength(output) > 64 * 1024 || !output.startsWith(`gui/${uid}/${LABEL} = {\n`)) {
133
+ throw new CliError("conflict", "The loaded menu-bar service has an unverified identity.");
134
+ }
135
+ const lines = output.split("\n").map(line => line.trim());
136
+ const field = (key: string): string | null => {
137
+ const values = lines.filter(line => line.startsWith(`${key} = `));
138
+ return values.length === 1 ? values[0]!.slice(key.length + 3) : null;
139
+ };
140
+ const argumentsStart = lines.indexOf("arguments = {");
141
+ const argumentsEnd = lines.indexOf("}", argumentsStart + 1);
142
+ if (field("path") !== path || field("program") !== binary || argumentsStart < 0 || argumentsEnd !== argumentsStart + 2 || lines[argumentsStart + 1] !== binary) {
143
+ throw new CliError("conflict", "The loaded menu-bar service does not match this install; it was left untouched.");
144
+ }
145
+ return true;
146
+ }
147
+ export function launchAgentState(binary: string, environment: Environment = process.env): LaunchAgentState {
148
+ const content = readAgent(launchAgentPath(environment), environment);
149
+ if (content === null) return "absent";
150
+ checkParents(binary, environment);
151
+ assertOwnedFile(binary);
152
+ return content === launchAgentPlist(binary) && exists(binary) ? "installed" : "conflict";
153
+ }
154
+ function assertMac(platform: NodeJS.Platform): void {
155
+ if (platform !== "darwin") throw new CliError("unsupported-plan", "Slopcamera menu-bar LaunchAgents are supported only on macOS.");
156
+ }
157
+ export function installLaunchAgent(binary: string, environment: Environment = process.env, platform: NodeJS.Platform = process.platform, runLaunchctl: LaunchctlRunner = launchctl): LaunchAgentState {
158
+ assertMac(platform);
159
+ if (!isAbsolute(binary)) throw new CliError("unsafe-path", "LaunchAgent binary must be an absolute path.");
160
+ const stable = installedBinaryPath(environment);
161
+ const path = launchAgentPath(environment);
162
+ const current = readAgent(path, environment);
163
+ const expected = launchAgentPlist(stable);
164
+ checkParents(stable, environment);
165
+ assertOwnedFile(stable);
166
+ if (current !== null && current !== expected || current === null && infoAt(stable) !== null) {
167
+ throw new CliError("conflict", "The existing Slopcamera menu-bar install is not owned by this command.");
168
+ }
169
+ const bytes = readRegular(binary, MAX_BINARY_BYTES, true);
170
+ loadedOwned(path, stable, current, runLaunchctl);
171
+ checkParents(path, environment, true);
172
+ checkParents(stable, environment, true);
173
+ // Unique private staging directories prevent predictable temporary-file clobbering.
174
+ const binaryStage = mkdtempSync(join(dirname(stable), ".install-"));
175
+ let agentStage: string | undefined;
176
+ try {
177
+ agentStage = mkdtempSync(join(dirname(path), ".slopcamera-install-"));
178
+ const stagedBinary = join(binaryStage, "binary");
179
+ const stagedAgent = join(agentStage, "agent.plist");
180
+ writeFileSync(stagedBinary, bytes, { mode: 0o700, flag: "wx" });
181
+ writeFileSync(stagedAgent, expected, { mode: 0o600, flag: "wx" });
182
+ if (readAgent(path, environment) !== current) throw new CliError("conflict", "The menu-bar install changed concurrently.");
183
+ assertOwnedFile(stable);
184
+ // Retry bootstrap even when the plist matches: a previous bootstrap may have failed.
185
+ if (loadedOwned(path, stable, current, runLaunchctl)) runLaunchctl(["bootout", `gui/{uid}/${LABEL}`], true);
186
+ renameSync(stagedBinary, stable);
187
+ renameSync(stagedAgent, path);
188
+ runLaunchctl(["bootstrap", "gui/{uid}", path]);
189
+ return "installed";
190
+ } finally {
191
+ rmSync(binaryStage, { recursive: true });
192
+ if (agentStage !== undefined) rmSync(agentStage, { recursive: true });
193
+ }
194
+ }
195
+ export function uninstallLaunchAgent(_binary: string, environment: Environment = process.env, platform: NodeJS.Platform = process.platform, runLaunchctl: LaunchctlRunner = launchctl): LaunchAgentState {
196
+ assertMac(platform);
197
+ const stable = installedBinaryPath(environment);
198
+ const path = launchAgentPath(environment);
199
+ const current = readAgent(path, environment);
200
+ if (current === null) return "absent";
201
+ if (current !== launchAgentPlist(stable)) throw new CliError("conflict", "The existing Slopcamera menu-bar LaunchAgent is not owned by this command.");
202
+ checkParents(stable, environment);
203
+ assertOwnedFile(stable);
204
+ if (loadedOwned(path, stable, current, runLaunchctl)) runLaunchctl(["bootout", `gui/{uid}/${LABEL}`], true);
205
+ if (readAgent(path, environment) !== current) throw new CliError("conflict", "The menu-bar install changed concurrently.");
206
+ assertOwnedFile(stable);
207
+ rmSync(path);
208
+ if (infoAt(stable) !== null) rmSync(stable);
209
+ return "absent";
210
+ }
211
+ async function runBinary(binary: string, foreground: boolean): Promise<number> {
212
+ let child: Bun.Subprocess;
213
+ try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar could not start."); }
214
+ if (!foreground) { child.unref(); const settled = await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); if (settled !== null) throw new CliError("unavailable", "The Slopcamera menu bar exited during startup."); return 0; }
215
+ return await child.exited;
216
+ }
217
+ export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground"): Promise<void> {
218
+ const binary = resolveMenubarBinary(repositoryRoot, io.env);
219
+ if (binary === null) throw new CliError("unavailable", "The Slopcamera menu bar is not installed. Build it separately or set SLOPCAMERA_MENUBAR.");
220
+ const code = await runBinary(binary, mode === "foreground");
221
+ if (code !== 0) throw new CliError("unavailable", "The Slopcamera menu bar exited during startup.");
222
+ if (asJson) writeJson(io, { running: mode === "background", foreground: mode === "foreground" });
223
+ else writeLine(io, mode === "foreground" ? "Slopcamera menu bar exited." : "Slopcamera menu bar is running.");
224
+ }
225
+ export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean): Promise<void> {
226
+ assertMac(io.platform);
227
+ const stable = installedBinaryPath(io.env);
228
+ let state: LaunchAgentState;
229
+ if (action === "install") {
230
+ const binary = resolveMenubarBinary(repositoryRoot, io.env);
231
+ if (binary === null) throw new CliError("unavailable", "The Slopcamera menu bar is not installed. Build it separately or set SLOPCAMERA_MENUBAR.");
232
+ state = installLaunchAgent(binary, io.env, io.platform);
233
+ } else {
234
+ state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform) : launchAgentState(stable, io.env);
235
+ }
236
+ if (asJson) writeJson(io, { launchAgent: state });
237
+ else writeLine(io, `Slopcamera menu-bar LaunchAgent: ${state}.`);
238
+ }
239
+ export async function reportOutputsRoot(io: CliIo, stateRoot: string, asJson: boolean): Promise<void> {
240
+ const directory = outputsRoot(stateRoot);
241
+ await ensurePrivateDirectory(directory);
242
+ if (asJson) writeJson(io, { outputs: directory });
243
+ else writeLine(io, directory);
244
+ }
@@ -15,18 +15,25 @@ import {
15
15
  } from "../contracts";
16
16
  import {
17
17
  analyzeMusic,
18
+ analyzeAudioReactivity,
19
+ AUDIO_REACTIVITY_MAX_DURATION_US,
20
+ AUDIO_REACTIVITY_SAMPLE_RATE_HZ,
18
21
  canonicalSlopcameraPersistenceDocument,
19
22
  canonicalJson,
20
23
  canonicalJsonSha256,
24
+ createNodeBundleFileSystem,
21
25
  saveAnalysisArtifact,
22
26
  saveVideoProject,
23
27
  sha256Hex,
24
28
  type MonoPcm,
25
29
  } from "../core";
30
+ import { HtmlOverlayAudioReactivitySchema } from "../html-overlay/audio-reactivity";
26
31
  import { decodeAlignmentPcm, resolveAudioAnalysisSubject } from "./audio-analysis";
27
32
  import { CliError } from "./errors";
28
33
  import type { ProcessRunner } from "./io";
34
+ import { SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS } from "./media-ingest-model";
29
35
  import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
36
+ import { resolveVerifiedProjectMedia, type PhysicalProjectMediaFingerprint } from "./project-media-integrity";
30
37
  import { projectAnalysisPath, type OpenProject } from "./project-service";
31
38
 
32
39
  export const MUSIC_ANALYSIS_SAMPLE_RATE_HZ = 8_000;
@@ -174,6 +181,60 @@ export async function readMonoFloat32PcmBounded(
174
181
  }
175
182
  }
176
183
 
184
+ const MAX_AUDIO_REACTIVITY_PCM_BYTES = 600 * AUDIO_REACTIVITY_SAMPLE_RATE_HZ * 4;
185
+
186
+ /** Decode one verified retained soundtrack prefix for the optional bands-v1 sidecar. */
187
+ export async function analyzeRetainedAudioReactivity(options: {
188
+ readonly repositoryRoot: string;
189
+ readonly jobDirectory: string;
190
+ readonly audio: PhysicalProjectMediaFingerprint;
191
+ readonly durationUs: number;
192
+ readonly streamIndex: number;
193
+ readonly ffmpeg: string;
194
+ readonly run: (argv: readonly [string, ...string[]]) => Promise<unknown>;
195
+ readonly fence: () => Promise<void>;
196
+ }) {
197
+ if (!Number.isSafeInteger(options.durationUs) || options.durationUs < 1
198
+ || options.durationUs > AUDIO_REACTIVITY_MAX_DURATION_US
199
+ || !Number.isSafeInteger(options.streamIndex) || options.streamIndex < 0) {
200
+ throw new CliError("invalid-data", "Audio reactivity duration or stream index is outside its bound.");
201
+ }
202
+ const verify = async () => {
203
+ await options.fence();
204
+ return await resolveVerifiedProjectMedia({ repositoryRoot: options.repositoryRoot,
205
+ path: options.audio.path, expected: options.audio, label: "Audio reactivity soundtrack" });
206
+ };
207
+ const source = await verify();
208
+ const pcmPath = join(options.jobDirectory, "audio-reactivity.f32le");
209
+ const fs = createNodeBundleFileSystem(options.jobDirectory);
210
+ try {
211
+ await options.run([options.ffmpeg, "-nostdin", "-v", "error", "-n", "-threads", "2",
212
+ ...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS, "-i", source, "-map", `0:${options.streamIndex}`,
213
+ "-vn", "-sn", "-dn", "-t", (options.durationUs / 1_000_000).toFixed(6),
214
+ "-ac", "1", "-ar", String(AUDIO_REACTIVITY_SAMPLE_RATE_HZ), "-c:a", "pcm_f32le", "-f", "f32le",
215
+ "-fs", String(MAX_AUDIO_REACTIVITY_PCM_BYTES + 4), pcmPath]);
216
+ await verify();
217
+ const before = await fs.inspectFile!("audio-reactivity.f32le", MAX_AUDIO_REACTIVITY_PCM_BYTES);
218
+ const pcm = await readMonoFloat32PcmBounded(pcmPath, { maxBytes: MAX_AUDIO_REACTIVITY_PCM_BYTES, sampleRateHz: AUDIO_REACTIVITY_SAMPLE_RATE_HZ });
219
+ const expectedSamples = Math.round(options.durationUs * AUDIO_REACTIVITY_SAMPLE_RATE_HZ / 1_000_000);
220
+ if (Math.abs(pcm.samples.length - expectedSamples) > 2) {
221
+ throw new CliError("invalid-data", "Audio reactivity decode did not cover the requested soundtrack prefix.");
222
+ }
223
+ const analysis = analyzeAudioReactivity(pcm);
224
+ await options.fence();
225
+ const after = await fs.inspectFile!("audio-reactivity.f32le", MAX_AUDIO_REACTIVITY_PCM_BYTES);
226
+ if (before.bytes !== after.bytes || before.sha256 !== after.sha256) {
227
+ throw new CliError("conflict", "Audio reactivity PCM changed during analysis.");
228
+ }
229
+ return HtmlOverlayAudioReactivitySchema.parse({ kind: "slopcamera.audio-reactivity", schemaVersion: 1,
230
+ profile: "bands-v1", rateHz: 60, ...analysis,
231
+ source: { bytes: options.audio.bytes, sha256: options.audio.sha256 },
232
+ pcm: { sampleCount: pcm.samples.length, sha256: before.sha256 } });
233
+ } finally {
234
+ await rm(pcmPath, { force: true });
235
+ }
236
+ }
237
+
177
238
  export interface WithDecodedMusicPcmOptions {
178
239
  readonly ffmpeg: string;
179
240
  readonly maxDecodedPcmBytes?: number;
@@ -1,3 +1,5 @@
1
+ import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
2
+
1
3
  import { constants } from "node:fs";
2
4
  import { mkdir, open } from "node:fs/promises";
3
5
  import { dirname, resolve } from "node:path";
@@ -16,7 +18,10 @@ import { CliError } from "./errors";
16
18
 
17
19
  const HEADLESS_SLOPCAMERA_CLI_MODULE = "@hraness/slopcamera/cli";
18
20
 
19
- async function runHeadlessSlopcameraCli(argv: readonly string[]): Promise<void> {
21
+ async function runHeadlessSlopcameraCli(
22
+ argv: readonly string[],
23
+ options?: Readonly<{ onUsefulResult?: UsefulResultObserver }>,
24
+ ): Promise<void> {
20
25
  // Keep this as a runtime package import so the headless CLI retains its own
21
26
  // package-relative skill and asset resolution inside an installed bundle.
22
27
  const module: unknown = await import(HEADLESS_SLOPCAMERA_CLI_MODULE);
@@ -28,13 +33,14 @@ async function runHeadlessSlopcameraCli(argv: readonly string[]): Promise<void>
28
33
  ) {
29
34
  throw new CliError("unavailable", "The portable Slopcamera CLI is unavailable.");
30
35
  }
31
- await module.main(argv);
36
+ await module.main(argv, options);
32
37
  }
33
38
 
34
39
  export interface PortableSurfaceDependencies {
40
+ readonly onUsefulResult?: UsefulResultObserver;
35
41
  readonly cwd?: () => string;
36
42
  readonly log?: (value: string) => void;
37
- readonly runHeadless?: (argv: readonly string[]) => Promise<void>;
43
+ readonly runHeadless?: (argv: readonly string[], options?: Readonly<{ onUsefulResult?: UsefulResultObserver }>) => Promise<void>;
38
44
  readonly writeScaffold?: (path: string, html: string) => Promise<void>;
39
45
  }
40
46
  function optionValue(argv: readonly string[], name: string): string | undefined {
@@ -187,6 +193,7 @@ async function runHtmlScaffold(
187
193
  createHtmlOverlayScaffold(kind),
188
194
  );
189
195
  (dependencies.log ?? console.log)(`Created ${outputPath}`);
196
+ reportUsefulResult(dependencies.onUsefulResult);
190
197
  return 0;
191
198
  }
192
199
 
@@ -214,6 +221,7 @@ export async function runPortableSurface(
214
221
  ): Promise<number | undefined> {
215
222
  const argv = canonicalizeUnifiedCliArgs(argvInput);
216
223
  if (argv[0] === "html") {
224
+ if (argv[1] === "render" || argv.includes("--help") || argv.includes("-h")) return undefined;
217
225
  if (argv[1] === "catalog") return runHtmlCatalog(argv, dependencies);
218
226
  return await runHtmlScaffold(argv, dependencies);
219
227
  }
@@ -234,7 +242,8 @@ export async function runPortableSurface(
234
242
  const previousExitCode = process.exitCode;
235
243
  process.exitCode = undefined;
236
244
  try {
237
- await (dependencies.runHeadless ?? runHeadlessSlopcameraCli)(argv);
245
+ await (dependencies.runHeadless ?? runHeadlessSlopcameraCli)(argv,
246
+ dependencies.onUsefulResult === undefined ? {} : { onUsefulResult: dependencies.onUsefulResult });
238
247
  return process.exitCode ?? 0;
239
248
  } finally {
240
249
  process.exitCode = previousExitCode;
@@ -0,0 +1,14 @@
1
+ const camera = " __\n _|__|_\n| (o) |\n|______|\n\n";
2
+
3
+ /** Decoration is confined to readable, interactive root help. */
4
+ export function rootHelpIntro(
5
+ argv: readonly string[],
6
+ terminal: Readonly<{ isTTY: boolean; term: string | undefined; columns: number | undefined }>,
7
+ ): string {
8
+ return terminal.isTTY && terminal.term !== "dumb"
9
+ && terminal.columns !== undefined && Number.isFinite(terminal.columns) && terminal.columns >= 48
10
+ && (argv.length === 0
11
+ || argv.length === 1 && (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h"))
12
+ ? camera
13
+ : "";
14
+ }
@@ -51,7 +51,6 @@ import {
51
51
  bindProjectRenderInputV2,
52
52
  bindProjectRenderInputV3,
53
53
  bindProjectRenderToolchain,
54
- parseRecordingOperationOutput,
55
54
  projectEditCommitReceipt,
56
55
  projectEditTransactionId,
57
56
  reconcileVariantSelectionPromotion,
@@ -105,7 +104,6 @@ import type {
105
104
  import { workflowBoundary, workflowValidation, type WorkflowFailure } from "./workflow-effects";
106
105
 
107
106
  const ANALYSIS_ID_DOMAIN = "studio.workflow.analysis-id/v1";
108
- type RecordingOperationKind = Extract<OperationKind, `recording.${string}`>;
109
107
  type GatewayOperationKind = Extract<OperationKind, `gateway.${string}`>;
110
108
 
111
109
  export const APPLICATION_VERIFIED_RECEIPT_RECONCILER_KINDS =
@@ -114,15 +112,6 @@ export const APPLICATION_VERIFIED_RECEIPT_RECONCILER_KINDS =
114
112
  "render.project",
115
113
  ] as const satisfies readonly OperationKind[]);
116
114
 
117
- function isRecordingOperationKind(
118
- kind: OperationKind,
119
- ): kind is RecordingOperationKind {
120
- return kind === "recording.start"
121
- || kind === "recording.pause"
122
- || kind === "recording.resume"
123
- || kind === "recording.stop";
124
- }
125
-
126
115
  function isGatewayOperationKind(
127
116
  kind: OperationKind,
128
117
  ): kind is GatewayOperationKind {
@@ -934,36 +923,6 @@ function renderNodeReconciliation(result: ProjectRenderReconciliation | Awaited<
934
923
  };
935
924
  }
936
925
 
937
- async function reconcileRecordingNode(
938
- application: ApplicationContext,
939
- operationKind: RecordingOperationKind,
940
- ): Promise<NodeReconciliation> {
941
- const controller = application.recordingController;
942
- if (controller === undefined) {
943
- return {
944
- kind: "ambiguous",
945
- message: "Recording controller is unavailable for live-state reconciliation.",
946
- };
947
- }
948
- let output;
949
- try {
950
- output = parseRecordingOperationOutput(await controller.status());
951
- } catch (error) {
952
- return {
953
- kind: "ambiguous",
954
- message: error instanceof Error
955
- ? `Recording state could not be reconciled: ${error.message}`
956
- : "Recording state could not be reconciled.",
957
- };
958
- }
959
- return {
960
- kind: "ambiguous",
961
- message: `Current controller state ${output.state} for ${
962
- output.recordingId ?? "no active recording"
963
- } is not an exact receipt proving ${operationKind} completed; issue a new explicit action.`,
964
- };
965
- }
966
-
967
926
  async function reconcileGatewayNode(
968
927
  application: ApplicationContext,
969
928
  request: NodeReconciliationRequest,
@@ -1223,12 +1182,6 @@ async function reconcileApplicationNode(
1223
1182
  request.operation.kind,
1224
1183
  );
1225
1184
  }
1226
- if (isRecordingOperationKind(request.operation.kind)) {
1227
- return await reconcileRecordingNode(
1228
- application,
1229
- request.operation.kind,
1230
- );
1231
- }
1232
1185
  if (request.operation.kind === "project.commit-edits") {
1233
1186
  return await reconcileProjectCommit(application, request);
1234
1187
  }
@@ -0,0 +1,46 @@
1
+ import { basename, dirname, join, resolve } from "node:path";
2
+
3
+ export interface HostSourceRoots {
4
+ /**
5
+ * True when the running module is inside the committed bundle at
6
+ * apps/desktop/dist/cli. A bundled host still spawns the checked TypeScript
7
+ * sources below apps/desktop and src/.
8
+ */
9
+ readonly bundled: boolean;
10
+ /** The installed apps/desktop source directory. */
11
+ readonly desktopRoot: string;
12
+ /** The checkout or package root containing src/, packages/, and package.json. */
13
+ readonly repositoryRoot: string;
14
+ }
15
+
16
+ /**
17
+ * Walks to the apps/desktop anchor instead of assuming a fixed module depth.
18
+ * From a checkout, caller modules sit below apps/desktop/<area>/; inside the
19
+ * committed bundle they all share apps/desktop/dist/cli.
20
+ */
21
+ export function hostSourceRoots(moduleDirectory: string): HostSourceRoots {
22
+ const requested = resolve(moduleDirectory);
23
+ let candidate = requested;
24
+ while (true) {
25
+ if (basename(candidate) === "desktop" && basename(dirname(candidate)) === "apps") {
26
+ return {
27
+ bundled: requested === join(candidate, "dist", "cli"),
28
+ desktopRoot: candidate,
29
+ repositoryRoot: dirname(dirname(candidate)),
30
+ };
31
+ }
32
+ const parent = dirname(candidate);
33
+ if (parent === candidate) {
34
+ // Embedded compiled-binary modules and foreign callers keep the
35
+ // historical assumption that the module sits one level below
36
+ // apps/desktop.
37
+ const desktopRoot = resolve(requested, "..");
38
+ return {
39
+ bundled: false,
40
+ desktopRoot,
41
+ repositoryRoot: resolve(desktopRoot, "../.."),
42
+ };
43
+ }
44
+ candidate = parent;
45
+ }
46
+ }
@@ -100,7 +100,6 @@ export {
100
100
  type ProducedVariantMatrixHandle,
101
101
  type ProducedVariantSelectionHandle,
102
102
  type ProjectSnapshotHandle,
103
- type RecordingStartOptions,
104
103
  type SelectCreativeVariantOptions,
105
104
  type VariantMatrixHandle,
106
105
  type VariantSelectionHandle,