@hraness/slopcamera 3.3.6 → 3.5.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.
Files changed (66) hide show
  1. package/README.md +30 -16
  2. package/apps/desktop/application/operation.ts +1 -1
  3. package/apps/desktop/cli/args.ts +1 -1
  4. package/apps/desktop/cli/capability-manifest.ts +13 -0
  5. package/apps/desktop/cli/commands.ts +8 -1
  6. package/apps/desktop/cli/help.ts +45 -10
  7. package/apps/desktop/cli/html-overlay-renderer.ts +16 -0
  8. package/apps/desktop/cli/menubar.ts +91 -15
  9. package/apps/desktop/cli/portable-surface.ts +2 -0
  10. package/apps/desktop/code/public.ts +1 -0
  11. package/apps/desktop/core/visual-style-look.ts +34 -0
  12. package/apps/desktop/dist/cli/main.js +338 -282
  13. package/apps/desktop/html-overlay/catalog.ts +1 -0
  14. package/apps/desktop/html-overlay/runtime.ts +15 -0
  15. package/apps/desktop/html-overlay/spatial.ts +10 -6
  16. package/apps/desktop/workflows/index.ts +1 -0
  17. package/dist/cli.js +6 -2
  18. package/dist/{index-sy1n4zfy.js → index-24w75s6a.js} +1 -1
  19. package/dist/{index-qtn3s421.js → index-8bhp7cj2.js} +1 -1
  20. package/dist/{index-10bz03js.js → index-jab2mg0s.js} +4 -3
  21. package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/operations.js +1 -1
  24. package/dist/vectorize/worker.js +1 -1
  25. package/dist/workflow.js +1 -1
  26. package/docs/README.md +4 -3
  27. package/examples/studio/vgpu/README.md +1 -1
  28. package/examples/style-portfolio/ANIMATION-STUDIES.md +34 -0
  29. package/examples/style-portfolio/FILM-PLATE.md +19 -0
  30. package/examples/style-portfolio/README.md +137 -0
  31. package/examples/style-portfolio/animation-studies.html +597 -0
  32. package/examples/style-portfolio/assets/instrument-serif-OFL.txt +93 -0
  33. package/examples/style-portfolio/assets/instrument-serif-latin-400.woff2 +0 -0
  34. package/examples/style-portfolio/build-gallery.ts +403 -0
  35. package/examples/style-portfolio/film-plate.html +76 -0
  36. package/examples/style-portfolio/finish-film.ts +152 -0
  37. package/examples/style-portfolio/market-street/README.md +43 -0
  38. package/examples/style-portfolio/market-street/mesh.py +145 -0
  39. package/examples/style-portfolio/market-street/scene.py +760 -0
  40. package/examples/style-portfolio/market-street/source.json +1 -0
  41. package/examples/style-portfolio/market-street/street_data.py +72 -0
  42. package/examples/style-portfolio/render-film-plate.ts +128 -0
  43. package/examples/style-portfolio/render.ts +89 -0
  44. package/examples/style-portfolio/verify-animation-source.mjs +94 -0
  45. package/package.json +14 -5
  46. package/skills/slopcamera/SKILL.md +13 -3
  47. package/skills/slopcamera/references/brand-illustrations.md +237 -0
  48. package/skills/slopcamera/references/gateway-media.md +6 -1
  49. package/skills/slopcamera/references/install.md +10 -1
  50. package/skills/slopcamera/references/music-video.md +5 -0
  51. package/skills/slopcamera/references/native-studio.md +4 -0
  52. package/skills/slopcamera/references/patent-drawings.md +4 -4
  53. package/skills/slopcamera/references/support.md +1 -1
  54. package/skills/slopcamera/references/vectorization.md +3 -1
  55. package/skills/slopcamera/references/visual-style-direction.md +143 -0
  56. package/src/cli.ts +28 -0
  57. package/src/code/public-operations.ts +9 -0
  58. package/src/index.ts +1 -0
  59. package/src/mcp/tools.ts +4 -0
  60. package/src/operations.ts +4 -0
  61. package/src/support.ts +1 -1
  62. package/src/vectorize/command.ts +6 -1
  63. package/src/vectorize/supervisor.ts +23 -3
  64. package/src/vectorize/tool.ts +8 -4
  65. package/src/version.ts +1 -1
  66. package/src/visual-style.ts +378 -0
package/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # Slopcamera
2
2
 
3
- [![Slopcamera: a visual studio for coding agents](https://slopcamera.com/og.png)](https://slopcamera.com)
3
+ [![Slopcamera: Visual work your agent can keep revising.](https://slopcamera.com/og.png)](https://slopcamera.com)
4
4
 
5
- **Slopcamera is a local visual studio for coding agents. Author scenes, combine
6
- generated and recorded media, and export images, diagrams, animation, and video
7
- from retained sources.**
5
+ **Slopcamera is a media studio for coding agents. Codex, Claude Code, and other
6
+ agents make images, diagrams, animation, 3D scenes, and edited video from source
7
+ files they can keep revising.**
8
8
 
9
- Describe a finished result to Codex, Claude, or another coding agent. Slopcamera gives
10
- it a Bun CLI, TypeScript SDK, and version-matched Agent Skill to inspect sources,
11
- direct cameras, edit a composition, and render the result. A separate MCP server
12
- exposes fixed tools for diagrams, images, and scene inspection and planning. There is no Slopcamera account or hosted
13
- project database.
9
+ Describe the result you want to Codex, Claude Code, or another coding agent.
10
+ Slopcamera gives the agent a Bun CLI, a TypeScript SDK, and an Agent Skill matched
11
+ to the CLI version, so it can inspect sources, set up cameras, edit a composition,
12
+ and render. `slopcamera mcp` runs an MCP server with a smaller, fixed set of diagram,
13
+ image, and scene tools. Projects stay in local files, and there is no Slopcamera
14
+ account. Generation uses your own Vercel AI Gateway account or, for prompt-only
15
+ images, prepaid Hraness Credits.
14
16
 
15
17
  [Install](#install-slopcamera) · [Make a first diagram](#make-your-first-diagram) · [Capabilities](#what-slopcamera-does) · [Documentation](https://slopcamera.com/docs) · [slopcamera.com](https://slopcamera.com)
16
18
 
@@ -38,20 +40,21 @@ the rendered output with source, runtime requirements and reproduction steps.
38
40
  timing, framing, captions, and effects as project decisions. Preview and final
39
41
  renders use the same timeline and composition.
40
42
  - **Choose where computation happens.** Local rendering and editing use your
41
- machine. Optional model-backed work uses your Vercel AI Gateway access, with
42
- explicit acknowledgement before named local media is uploaded. Native Python
43
+ machine. Optional model-backed work uses your Vercel AI Gateway access and
44
+ uploads named local media only after you acknowledge it. Prompt-only images can
45
+ instead run on the hosted API with prepaid Hraness Credits. Native Python
43
46
  authoring requires separate trust because it runs as your current user.
44
47
 
45
48
  ## Install Slopcamera
46
49
 
47
- Slopcamera v3.3.5 adds paid hosted image generation through Hraness Credits top-up (`slopcamera credits`, `ai image --hosted`), public hosted model discovery, and checked patent-style vector drawing sheets.
50
+ Slopcamera v3.4.0 includes 17 reusable film and animation direction profiles, deterministic exposure helpers, and a sample renderer that defaults to native 4K. Use the [style direction guide](docs/how-to/direct-visual-styles.md) to author cel animation, pixel art, mathematical explanations, and historical film treatments from retained sources.
48
51
 
49
52
  Install [Bun 1.3.14 or newer](https://bun.sh), then install the verified
50
- [Slopcamera v3.3.5 release](https://github.com/hraness/slopcamera/releases/tag/v3.3.5)
53
+ [Slopcamera v3.4.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.4.0)
51
54
  from its canonical archive:
52
55
 
53
56
  ```sh
54
- bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.5/hraness-slopcamera-3.3.5.tgz
57
+ bun add --global https://github.com/hraness/slopcamera/releases/download/v3.4.0/hraness-slopcamera-3.4.0.tgz
55
58
  slopcamera skill install --target agents
56
59
  ```
57
60
 
@@ -134,6 +137,9 @@ architecture from dimensions, [build a parametric design](https://slopcamera.com
134
137
 
135
138
  ## What Slopcamera does
136
139
 
140
+ Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes,
141
+ and video from source files it can keep revising.
142
+
137
143
  ### Author scenes and direct cameras
138
144
 
139
145
  Place geometry, images, video, diagrams, and text in a portable scene. Inspect
@@ -151,7 +157,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
151
157
  capture appearance; they do not establish collision geometry or editable native
152
158
  meshes. See [Directed scenes](docs/spatial-scenes.md).
153
159
 
154
- Slopcamera v3.3.1 includes the cinematic planning loop: a `slopcamera.spatial-direction`
160
+ Slopcamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
155
161
  document describes beats, actions, camera coverage, and look intents, compiles
156
162
  into proposed performance/camera/cinema/material-lighting/shot documents, and
157
163
  fans out into bounded galleries per axis. Declared effects bind into renders,
@@ -306,7 +312,7 @@ declarative graphs, approvals, and resuming work.
306
312
  workflows run with the current user's access. Hashes and receipts identify
307
313
  observed inputs and outputs; they do not make arbitrary code hermetic.
308
314
  - **MCP is a subset.** Its 17 fixed tools check and render diagrams, plan and audit
309
- scenes, and run six bounded portable operation codes in v3.3.1. It does not
315
+ scenes, and run six bounded operation codes. It does not
310
316
  expose every local CLI operation and never mutates project state.
311
317
 
312
318
  ## Design and trust
@@ -331,6 +337,14 @@ source identity alone does not promise identical pixels on another machine.
331
337
  See [Architecture](docs/architecture.md), [`SECURITY.md`](SECURITY.md),
332
338
  [`PRIVACY.md`](PRIVACY.md), and [`NOTICE.md`](NOTICE.md) for the detailed boundaries.
333
339
 
340
+ Your agent renders images, diagrams, scenes, animation, and video from source
341
+ files you keep, and important operations record their inputs and outputs in
342
+ receipts, so a rendered result arrives with the record of how it was made: the
343
+ design every Hraness project shares. [The thread through
344
+ hraness](https://hraness.com/writing/the-thread-through-hraness) follows that
345
+ design across the projects, and the [ALGAL
346
+ vision](https://algal.computer/docs/vision/) states the bet behind it.
347
+
334
348
  ## Documentation
335
349
 
336
350
  - **Learn:** [Your first diagram](docs/tutorials/first-diagram.md) · [Your first native film](docs/tutorials/first-native-film.md).
@@ -4,7 +4,7 @@ import type { z } from "zod";
4
4
 
5
5
  import type { ApplicationContext } from "./context";
6
6
 
7
- export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.3.6" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.5.0" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -3659,7 +3659,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3659
3659
  case "menubar": {
3660
3660
  const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--foreground": "flag", "--background": "flag" });
3661
3661
  const positionals = parsed.positionals;
3662
- if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] | install|uninstall|status");
3662
+ if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] or slopcamera menubar install|uninstall|status. See slopcamera help menubar.");
3663
3663
  if (positionals[0] !== undefined && (optionFlag(parsed, "--foreground") || optionFlag(parsed, "--background"))) throw new CliError("usage", "Choose a menu-bar action or a run mode, not both.");
3664
3664
  return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
3665
3665
  }
@@ -104,6 +104,19 @@ const gatewayOperations = [
104
104
  ] as const;
105
105
 
106
106
  const capabilityModules = Object.freeze([
107
+ defineSlopcameraCapabilityModule({
108
+ commands: [{ effect: "pure", name: "style list" }, { effect: "pure", name: "style show" }],
109
+ description: "Renderer-independent visual direction, delivery recommendations, and deterministic exposure timing for film and animation.",
110
+ moduleId: "slopcamera.capability.visual-style",
111
+ operationKeys: [],
112
+ profiles: [{
113
+ id: "visual-style-direction-v1", kind: "authoring",
114
+ qualification: { evidence: ["src/visual-style.test.ts", "src/visual-style.property.test.ts"], level: "unit", status: "qualified" },
115
+ requirements: [],
116
+ }],
117
+ stability: "stable", title: "Visual style direction", toolNames: [],
118
+ trust: "portable", version: 1, workflowKeys: [],
119
+ }),
107
120
  defineSlopcameraCapabilityModule({
108
121
  commands: [
109
122
  { effect: "pure", name: "operations list" },
@@ -362,7 +362,7 @@ import {
362
362
  workflowRunStore,
363
363
  } from "./workflow-runs";
364
364
 
365
- export const SLOPCAMERA_VERSION = "3.3.6";
365
+ export const SLOPCAMERA_VERSION = "3.5.0";
366
366
 
367
367
  // Legacy direct renders predate per-target output contracts. Keep them
368
368
  // bounded generously enough for long-form production while preventing one
@@ -739,6 +739,13 @@ function humanTime(microseconds: number): string {
739
739
 
740
740
  function requireCapability(capabilities: readonly Capability[], name: Capability["name"]): string {
741
741
  const capability = capabilityByName(capabilities, name);
742
+ if ((!capability.available || capability.command === undefined) && name === "face-analyzer") {
743
+ throw new CliError(
744
+ "unavailable",
745
+ "Face analysis needs a small helper that isn't built on this Mac yet. In a Slopcamera checkout, run bun run build:desktop:analysis:macos; it uses Apple's command line tools and asks before installing them.",
746
+ { capability: name, ...(capability.reason === undefined ? {} : { reason: capability.reason }) },
747
+ );
748
+ }
742
749
  if (!capability.available || capability.command === undefined) {
743
750
  throw new CliError(
744
751
  "unavailable",
@@ -3,13 +3,13 @@ import { HTML_OVERLAY_SCAFFOLD_KINDS } from "../html-overlay";
3
3
  const HTML_OVERLAY_SCAFFOLD_KIND_HELP =
4
4
  HTML_OVERLAY_SCAFFOLD_KINDS.join("|");
5
5
 
6
- const GLOBAL_HELP = `slopcamera — creative scenes and non-destructive media editing for coding agents
6
+ const GLOBAL_HELP = `slopcamera: a media studio for coding agents
7
7
 
8
8
  Usage: slopcamera <command> [options]
9
9
 
10
10
  Commands:
11
- capabilities Discover the exact static capability modules and qualification status
12
- operations list|show Discover host-owned typed operations and policies
11
+ capabilities List what this build can do and how well each part is tested
12
+ operations list|show List the operations agents can run and their limits
13
13
  diagram init|check|render Create, validate, or render portable diagram sources
14
14
  diagram sheets init|check|render
15
15
  Create and render black-and-white drawing sheets
@@ -24,6 +24,7 @@ Commands:
24
24
  image vectorize|generate|gallery|icon
25
25
  Create local SVGs, generated images, or review galleries
26
26
  html catalog|scaffold|render Author HTML scenes and export video with local audio
27
+ style list|show Discover reusable art direction and review criteria
27
28
  workflows list|show|plan|run Plan or run a reviewed reusable workflow
28
29
  code init|check|plan|run Author, preflight, and run trusted TypeScript workflows
29
30
  runs list|show|resume|approve|cancel
@@ -34,8 +35,9 @@ Commands:
34
35
  credits status|topup|wait|forget
35
36
  Buy and inspect hosted generation credits
36
37
  media audio|color Apply local non-destructive audio and video effects
37
- menubar [--background] Run the prebuilt menu-bar companion
38
- menubar install|uninstall|status Manage its per-user LaunchAgent
38
+ menubar [--foreground|--background]
39
+ Show your outputs in the menu bar
40
+ menubar install|uninstall|status Open it at login, stop that, or check it
39
41
  support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
40
42
  Optional support; no feature requires payment
41
43
  outputs Print the agent outputs directory
@@ -54,13 +56,36 @@ Commands:
54
56
  render plan|run <recording> Resolve or execute a render plan
55
57
  assets emoji search|resolve Find checked local emoji overlays
56
58
 
57
- Agents: after useful work, read slopcamera support protocol --json for the optional
58
- closeout protocol. Discovery uses stderr; it does not claim an invitation.
59
- HRANESS_SUPPORT_AUDIENCE=off suppresses offers and incidental discovery.
59
+ Examples:
60
+ slopcamera doctor Check what this Mac can render
61
+ slopcamera html scaffold plain --output intro.html
62
+ Start an HTML scene
63
+ slopcamera render run REC --output out.mp4 Render a recording
60
64
 
61
- Run slopcamera help <command> for command-specific help.`;
65
+ Run slopcamera help <command> for command-specific help.
66
+ Optional support: slopcamera support · Turn off: HRANESS_SUPPORT_AUDIENCE=off`;
62
67
 
63
68
  const HELP: Readonly<Record<string, string>> = {
69
+ menubar: `Usage:
70
+ slopcamera menubar [--foreground|--background] [--json]
71
+ slopcamera menubar install|uninstall|status [--json]
72
+
73
+ Shows 📷 in the menu bar with your recent outputs. It only lists and opens
74
+ files in the outputs folder (slopcamera outputs).
75
+
76
+ menubar Open it now; it closes when this command ends
77
+ menubar --background Open it now and return
78
+ menubar install Open it now and every time you log in
79
+ menubar uninstall Stop opening it at login and remove the installed copy
80
+ menubar status Check whether it opens at login and is running
81
+
82
+ If it's already in your menu bar, starting it again says so and succeeds.
83
+ macOS shows a notice that slopcamera-menubar can open at login. Turn it off any
84
+ time in System Settings › General › Login Items & Extensions.
85
+
86
+ The menu bar isn't in released packages yet. In a Slopcamera checkout, build it
87
+ with cargo build --release --manifest-path desktop/Cargo.toml and slopcamera
88
+ finds it; elsewhere, set SLOPCAMERA_MENUBAR to the built file.`,
64
89
  capabilities: `Usage: slopcamera capabilities [--json]
65
90
 
66
91
  Print the exact statically assembled capability manifest for this CLI build. Each module owns named operation, workflow, MCP-tool, command, and runtime-profile identities. The manifest includes trust classes, resource/effect policies, runtime requirements, qualification status, and a canonical SHA-256. It reads no workspace source, loads no plugin, probes no runtime, and grants no authority. Use doctor separately to inspect this machine's currently available executables.`,
@@ -293,6 +318,15 @@ composes a labelled contact sheet plus receipt for agent review. Texture cells r
293
318
  candidate 2×2 so seams are inspectable (--tile/--no-tile overrides). It never replaces existing
294
319
  outputs and never promotes a candidate into authored source; review the sheet and select
295
320
  explicitly.`,
321
+ style: `Usage:
322
+ slopcamera style list [--json]
323
+ slopcamera style show <id> [--json]
324
+
325
+ Styles describe palette, shape, materials, camera, exposure cadence, finishing targets,
326
+ recommended delivery, and visual review criteria. They work across authored scenes and
327
+ generation prompts, independently of the seven HTML library profiles. Discovery is local
328
+ and read-only: it does not render, select a model, or apply an effect. A profile is art
329
+ direction, not proof of visual quality or historically authentic footage.`,
296
330
  html: `Usage:
297
331
  slopcamera html catalog [--json]
298
332
  slopcamera html scaffold <${HTML_OVERLAY_SCAFFOLD_KIND_HELP}> --output <file.html>
@@ -673,12 +707,13 @@ export function commandHelp(topic: readonly string[]): string {
673
707
 
674
708
  export function completions(words: readonly string[]): readonly string[] {
675
709
  const topLevel = [
676
- "capabilities", "operations", "diagram", "direct", "studio", "image", "html", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
710
+ "capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
677
711
  ];
678
712
  if (words.length <= 1) return topLevel;
679
713
  const command = words[0];
680
714
  if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
681
715
  if (command === "html") return ["catalog", "scaffold", "render"];
716
+ if (command === "style") return ["list", "show"];
682
717
  if (command === "direct") return ["init", "anchor", "plan", "start", "inspect", "revise", "generate", "resume", "review", "assemble", "cleanup"];
683
718
  if (command === "studio") return words[2] === "assets" || words[1] === "assets" ? ["search", "describe", "plan", "import"] : ["init", "bundle", "plan", "probe", "run", "encode", "asset", "assemble", "inspect", "reconcile", "assets"];
684
719
  if (command === "operations") return ["list", "show"];
@@ -135,6 +135,7 @@ type BrowserRuntimeSnapshotLease = Readonly<
135
135
  interface HtmlOverlayHostController {
136
136
  renderFrame(frame: HtmlOverlayRuntimeFrame): Promise<void>;
137
137
  securityViolationCount(): number;
138
+ settlePresentation(): Promise<void>;
138
139
  }
139
140
 
140
141
  interface PreparedRoute {
@@ -2332,6 +2333,21 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2332
2333
  );
2333
2334
  }
2334
2335
  await inspectGpu();
2336
+ // Wait for the compositor to present the frame the callbacks
2337
+ // just produced. renderFrame resolves when authored work returns,
2338
+ // not when the new surface reaches the screen; under heavy GPU
2339
+ // load a screenshot taken immediately can capture the previous
2340
+ // presented frame. Two animation frames bound the wait: the draw
2341
+ // is committed before the first callback fires and presented
2342
+ // before the second.
2343
+ await boundedBrowserStep(
2344
+ async () => await host.evaluate(
2345
+ async (controller) => await controller.settlePresentation(),
2346
+ ),
2347
+ signal,
2348
+ this.#browserStepTimeoutMs,
2349
+ `frame ${String(frameIndex)} presentation settle`,
2350
+ );
2335
2351
  await boundedBrowserStep(
2336
2352
  async () => await page.screenshot({
2337
2353
  omitBackground: HTML_OVERLAY_RENDERER_CONTRACT.screenshot.omitBackground,
@@ -208,33 +208,109 @@ export function uninstallLaunchAgent(_binary: string, environment: Environment =
208
208
  if (infoAt(stable) !== null) rmSync(stable);
209
209
  return "absent";
210
210
  }
211
- async function runBinary(binary: string, foreground: boolean): Promise<number> {
211
+ /** `slopcamera-menubar` exits with this code when another copy already holds the menu bar. */
212
+ export const ALREADY_RUNNING_EXIT = 3;
213
+ const NOT_BUILT = "The Slopcamera menu bar isn't in this release yet. Build it with cargo build --release --manifest-path desktop/Cargo.toml in a Slopcamera checkout; slopcamera finds it there, or set SLOPCAMERA_MENUBAR to the built file.";
214
+ const LOGIN_ITEMS = "System Settings › General › Login Items & Extensions";
215
+
216
+ export type Launched = "running" | "already-running" | "exited";
217
+ export type BinaryRunner = (binary: string, foreground: boolean) => Promise<number | null>;
218
+
219
+ /** Resolves the exit code, or null when a background copy is still running after startup. */
220
+ async function spawnBinary(binary: string, foreground: boolean): Promise<number | null> {
212
221
  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; }
222
+ 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 couldn't start."); }
223
+ if (!foreground) { child.unref(); return await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); }
215
224
  return await child.exited;
216
225
  }
217
- export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground"): Promise<void> {
226
+
227
+ /** Interprets the companion's exit: another running copy is success, not a failure. */
228
+ export function launchOutcome(code: number | null, foreground: boolean): Launched {
229
+ if (code === ALREADY_RUNNING_EXIT) return "already-running";
230
+ if (code === null && !foreground) return "running";
231
+ if (code === 0 && foreground) return "exited";
232
+ throw new CliError("unavailable", "The Slopcamera menu bar stopped while starting. Run it in the foreground to see why: slopcamera menubar --foreground");
233
+ }
234
+
235
+ interface Marks { readonly ok: string; readonly warn: string; readonly note: string; readonly next: string }
236
+ /** ✓ ⚠ 🔐 → with ASCII fallbacks for dumb terminals and non-UTF-8 locales. */
237
+ export function marks(env: Readonly<Record<string, string | undefined>>): Marks {
238
+ const utf8 = [env.LC_ALL, env.LC_CTYPE, env.LANG].some((value) => value !== undefined && /utf-?8/i.test(value));
239
+ const ascii = env.TERM === "dumb" || !utf8 || env.HRANESS_ASCII === "1";
240
+ return ascii ? { ok: "OK", warn: "WARN", note: "NOTE", next: "->" } : { ok: "✓", warn: "⚠", note: "🔐", next: "→" };
241
+ }
242
+
243
+ /** The LOGIN_ITEM notice (notifies, so there is no confirm line). The requester is the bare companion until Slopcamera ships as an app. */
244
+ export function loginItemNotice(env: Readonly<Record<string, string | undefined>>): string {
245
+ return `${marks(env).note} macOS will show a notice that slopcamera-menubar can open at login. That's Slopcamera's menu bar.\n It lists your outputs and opens when you log in. Nothing else runs in the background. Turn it off any time in ${LOGIN_ITEMS}.\n`;
246
+ }
247
+
248
+ export function launchMessage(outcome: Launched, env: Readonly<Record<string, string | undefined>>): string {
249
+ const mark = marks(env);
250
+ if (outcome === "already-running") return `${mark.ok} Slopcamera is already in your menu bar. Look for 📷.`;
251
+ if (outcome === "running") return `${mark.ok} Slopcamera is in your menu bar. Look for 📷.`;
252
+ return "The Slopcamera menu bar closed.";
253
+ }
254
+
255
+ export function stateMessage(action: "install" | "uninstall" | "status", state: LaunchAgentState, running: boolean, env: Readonly<Record<string, string | undefined>>): string {
256
+ const mark = marks(env);
257
+ if (state === "conflict") return `${mark.warn} Another menu-bar setup that this command didn't create is in the way. It was left untouched.\n Remove ~/Library/LaunchAgents/${LABEL}.plist yourself if you don't need it.`;
258
+ if (state === "absent") return action === "uninstall" ? `${mark.ok} Slopcamera no longer opens in your menu bar at login.` : `Slopcamera doesn't open at login.\n${mark.next} slopcamera menubar install`;
259
+ if (action === "install") return `${mark.ok} Slopcamera opens in your menu bar now and at every login. Look for 📷.\n Remove it any time: slopcamera menubar uninstall`;
260
+ return running
261
+ ? `${mark.ok} Slopcamera opens at login and is in your menu bar now.`
262
+ : `${mark.warn} Slopcamera is set to open at login but isn't running. It may be turned off in ${LOGIN_ITEMS}.\n${mark.next} slopcamera menubar install`;
263
+ }
264
+
265
+ /** Whether launchd reports the loaded companion as running. Reads state only. */
266
+ export function serviceRunning(runLaunchctl: LaunchctlRunner): boolean {
267
+ const output = runLaunchctl(["print", `gui/{uid}/${LABEL}`], true);
268
+ return output !== null && output.split("\n").some((line) => line.trim() === "state = running");
269
+ }
270
+
271
+ export type ProcessProbe = () => boolean;
272
+ /** Whether any copy of the companion runs for this user, including one started by hand. */
273
+ function companionProcessRunning(): boolean {
274
+ const uid = process.getuid?.();
275
+ if (uid === undefined) return false;
276
+ try {
277
+ return Bun.spawnSync(["/usr/bin/pgrep", "-x", "-U", String(uid), "slopcamera-menubar"], { stdin: "ignore", stdout: "ignore", stderr: "ignore", timeout: 5_000 }).exitCode === 0;
278
+ } catch { return false; }
279
+ }
280
+
281
+ /** The menu bar is showing when launchd runs the login item or another copy holds it. */
282
+ async function menubarShowing(runLaunchctl: LaunchctlRunner, probe: ProcessProbe, waitMs: number): Promise<boolean> {
283
+ const deadline = Date.now() + waitMs;
284
+ for (;;) {
285
+ if (serviceRunning(runLaunchctl) || probe()) return true;
286
+ if (Date.now() >= deadline) return false;
287
+ await Bun.sleep(200);
288
+ }
289
+ }
290
+
291
+ export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground", runBinary: BinaryRunner = spawnBinary): Promise<void> {
218
292
  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.");
293
+ if (binary === null) throw new CliError("unavailable", NOT_BUILT);
294
+ const outcome = launchOutcome(await runBinary(binary, mode === "foreground"), mode === "foreground");
295
+ if (asJson) writeJson(io, { running: outcome !== "exited", foreground: mode === "foreground", alreadyRunning: outcome === "already-running" });
296
+ else writeLine(io, launchMessage(outcome, io.env));
224
297
  }
225
- export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean): Promise<void> {
298
+ export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean, runLaunchctl: LaunchctlRunner = launchctl, probe: ProcessProbe = companionProcessRunning): Promise<void> {
226
299
  assertMac(io.platform);
227
300
  const stable = installedBinaryPath(io.env);
228
301
  let state: LaunchAgentState;
229
302
  if (action === "install") {
230
303
  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);
304
+ if (binary === null) throw new CliError("unavailable", NOT_BUILT);
305
+ if (!asJson) io.stderr(loginItemNotice(io.env));
306
+ state = installLaunchAgent(binary, io.env, io.platform, runLaunchctl);
233
307
  } else {
234
- state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform) : launchAgentState(stable, io.env);
308
+ state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform, runLaunchctl) : launchAgentState(stable, io.env);
235
309
  }
236
- if (asJson) writeJson(io, { launchAgent: state });
237
- else writeLine(io, `Slopcamera menu-bar LaunchAgent: ${state}.`);
310
+ // A fresh login item may take a moment to start, so install waits briefly.
311
+ const running = state === "installed" && await menubarShowing(runLaunchctl, probe, action === "install" ? 1_000 : 0);
312
+ if (asJson) writeJson(io, { launchAgent: state, running });
313
+ else writeLine(io, stateMessage(action, state, running, io.env));
238
314
  }
239
315
  export async function reportOutputsRoot(io: CliIo, stateRoot: string, asJson: boolean): Promise<void> {
240
316
  const directory = outputsRoot(stateRoot);
@@ -220,12 +220,14 @@ export async function runPortableSurface(
220
220
  dependencies: PortableSurfaceDependencies = {},
221
221
  ): Promise<number | undefined> {
222
222
  const argv = canonicalizeUnifiedCliArgs(argvInput);
223
+ if (argv[0] === "style" && (argv.includes("--help") || argv.includes("-h"))) return undefined;
223
224
  if (argv[0] === "html") {
224
225
  if (argv[1] === "render" || argv.includes("--help") || argv.includes("-h")) return undefined;
225
226
  if (argv[1] === "catalog") return runHtmlCatalog(argv, dependencies);
226
227
  return await runHtmlScaffold(argv, dependencies);
227
228
  }
228
229
  const delegatesToHeadless = argv[0] === "diagram"
230
+ || argv[0] === "style"
229
231
  || argv[0] === "mcp"
230
232
  || argv[0] === "canvas"
231
233
  || argv[0] === "skill"
@@ -10,6 +10,7 @@ export {
10
10
  } from "./define-workflow";
11
11
  export * from "../../../src/spatial-scene/index";
12
12
  export * from "../../../src/studio/index";
13
+ export { createVisualStyleVideoLook, VisualStyleVideoLookOptionsSchema } from "../core/visual-style-look";
13
14
  export { StudioRunInputSchema, BoundStudioRunInputSchema, StudioRunOutputSchema, type StudioRunInput, type BoundStudioRunInput, type StudioRunOutput } from "../application/studio-port";
14
15
  export type { SpatialRenderInput, BoundSpatialRenderInput } from "../application/operations/spatial-render";
15
16
  export type { SpatialRenderRequest, SpatialRenderResult } from "../application/spatial-render";
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { getVisualStyleProfile } from "../../../src/visual-style";
3
+ import type { VideoEffectInput, VideoLookV1 } from "../contracts/video-effects";
4
+ import { createVideoLook } from "./video-effects";
5
+
6
+ export const VisualStyleVideoLookOptionsSchema = z.strictObject({
7
+ height: z.number().int().min(64).max(4320).default(1080),
8
+ seed: z.number().int().safe().min(0).max(2_147_483_647).default(20260923),
9
+ });
10
+
11
+ /**
12
+ * Translate only supported finishing controls into the existing ordered look
13
+ * graph. Scene construction, motion, gate weave, grain size, and true halation
14
+ * remain authoring decisions; screen diffusion is not a film-stock simulation.
15
+ */
16
+ export function createVisualStyleVideoLook(style: unknown, options: unknown = {}): VideoLookV1 {
17
+ const profile = getVisualStyleProfile(style);
18
+ const settings = VisualStyleVideoLookOptionsSchema.parse(options);
19
+ const finish = profile.finishing;
20
+ const effects: VideoEffectInput[] = [{
21
+ kind: "color-grade", amount: 1,
22
+ grade: { kind: "custom", controls: { saturation: finish.saturation, contrast: finish.contrast } },
23
+ }];
24
+ if (finish.halation > 0) effects.push({
25
+ kind: "diffusion", amount: finish.halation,
26
+ radiusPx: Math.max(0.25, 1.8 * settings.height / 1080), blendMode: "screen",
27
+ });
28
+ if (finish.grain > 0) effects.push({
29
+ kind: "film-grain", amount: finish.grain, seed: settings.seed,
30
+ cadence: "frame-varying", chroma: finish.saturation === 0 ? 0 : 0.12,
31
+ });
32
+ if (finish.vignette > 0) effects.push({ kind: "vignette", amount: finish.vignette });
33
+ return createVideoLook(effects);
34
+ }