@hraness/slopcamera 3.5.0 → 3.7.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 (52) hide show
  1. package/README.md +42 -26
  2. package/apps/desktop/README.md +1 -1
  3. package/apps/desktop/application/html-overlay-renderer.ts +6 -0
  4. package/apps/desktop/application/operation.ts +1 -1
  5. package/apps/desktop/cli/args.ts +63 -1
  6. package/apps/desktop/cli/command-host-resources.ts +5 -0
  7. package/apps/desktop/cli/commands.ts +27 -1
  8. package/apps/desktop/cli/help.ts +43 -21
  9. package/apps/desktop/cli/html-film-init.ts +158 -0
  10. package/apps/desktop/cli/html-film-names.ts +17 -0
  11. package/apps/desktop/cli/html-film.ts +471 -0
  12. package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
  13. package/apps/desktop/cli/html-scene.ts +3 -3
  14. package/apps/desktop/cli/main.ts +1 -0
  15. package/apps/desktop/cli/menubar-status.ts +156 -0
  16. package/apps/desktop/cli/menubar.ts +88 -136
  17. package/apps/desktop/cli/portable-surface.ts +55 -1
  18. package/apps/desktop/dist/cli/main.js +353 -328
  19. package/apps/desktop/html-overlay/film.ts +418 -0
  20. package/dist/cli.js +19 -9
  21. package/dist/generate.js +1 -1
  22. package/dist/{index-8bhp7cj2.js → index-842t02pp.js} +1 -1
  23. package/dist/{index-jab2mg0s.js → index-nv3byqa1.js} +20 -4
  24. package/dist/index-p74xy76d.js +3 -0
  25. package/dist/{index-24w75s6a.js → index-y8spxs77.js} +10 -7
  26. package/dist/index.js +1 -1
  27. package/dist/operations.js +1 -1
  28. package/dist/workflow.js +1 -1
  29. package/docs/README.md +5 -5
  30. package/examples/html/launch-film/README.md +51 -0
  31. package/examples/html/launch-film/build.ts +158 -0
  32. package/examples/html/launch-film/film.css +593 -0
  33. package/examples/html/launch-film/film.html +85 -0
  34. package/examples/html/launch-film/film.js +410 -0
  35. package/examples/html/launch-film/film.json +63 -0
  36. package/examples/html/launch-film/mockups.tsx +74 -0
  37. package/examples/html/launch-film/timeline.ts +57 -0
  38. package/package.json +14 -8
  39. package/skills/slopcamera/SKILL.md +3 -2
  40. package/skills/slopcamera/references/install.md +3 -3
  41. package/skills/slopcamera/references/launch-films.md +154 -0
  42. package/skills/slopcamera/references/visual-communication.md +1 -1
  43. package/src/cli.ts +138 -19
  44. package/src/generate.ts +44 -1
  45. package/src/generation-pricing.ts +237 -0
  46. package/src/icon-set.ts +1086 -0
  47. package/src/icon.ts +396 -59
  48. package/src/index.ts +1 -0
  49. package/src/operations.ts +52 -0
  50. package/src/support.ts +6 -0
  51. package/src/version.ts +1 -1
  52. package/dist/index-77fjfg9f.js +0 -3
package/README.md CHANGED
@@ -1,22 +1,22 @@
1
- # Slopcamera
1
+ # SlopCamera
2
2
 
3
- [![Slopcamera: Visual work your agent can keep revising.](https://slopcamera.com/og.png)](https://slopcamera.com)
3
+ [![SlopCamera: Images, diagrams, animation, 3D and video your coding agent can keep revising.](https://slopcamera.com/og.png)](https://slopcamera.com)
4
4
 
5
- **Slopcamera is a media studio for coding agents. Codex, Claude Code, and other
5
+ **SlopCamera is a media studio for coding agents. Codex, Claude Code, and other
6
6
  agents make images, diagrams, animation, 3D scenes, and edited video from source
7
7
  files they can keep revising.**
8
8
 
9
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
10
+ SlopCamera gives the agent a Bun CLI, a TypeScript SDK, and an Agent Skill matched
11
11
  to the CLI version, so it can inspect sources, set up cameras, edit a composition,
12
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
13
+ image, and scene tools. Projects stay in local files, and there is no SlopCamera
14
14
  account. Generation uses your own Vercel AI Gateway account or, for prompt-only
15
15
  images, prepaid Hraness Credits.
16
16
 
17
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)
18
18
 
19
- ## Made with Slopcamera
19
+ ## Made with SlopCamera
20
20
 
21
21
  [![An original brass optical instrument rendered in Blender](apps/web/media/native-product-poster-51b1658c46d3.webp)](https://slopcamera.com/docs/tutorials/first-native-film#inspect-the-finished-example)
22
22
 
@@ -27,7 +27,7 @@ The [portrait geometry lesson](https://slopcamera.com/docs/how-to/educational-vi
27
27
  adds mathematical typesetting, a presenter and authored captions. Each guide pairs
28
28
  the rendered output with source, runtime requirements and reproduction steps.
29
29
 
30
- ## Why Slopcamera
30
+ ## Why SlopCamera
31
31
 
32
32
  - **Keep creative work editable.** Native scenes, portable scenes, diagrams,
33
33
  and video projects retain their own sources and settings. Rendered frames
@@ -45,16 +45,29 @@ the rendered output with source, runtime requirements and reproduction steps.
45
45
  instead run on the hosted API with prepaid Hraness Credits. Native Python
46
46
  authoring requires separate trust because it runs as your current user.
47
47
 
48
- ## Install Slopcamera
48
+ ## Compared with other tools
49
49
 
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.
50
+ - [Remotion](https://www.remotion.dev/) renders React components to video and can
51
+ render on AWS Lambda. It is source-available and needs a paid license for
52
+ companies of four or more people.
53
+ - [HyperFrames](https://github.com/heygen-com/hyperframes) renders HTML to MP4
54
+ under Apache 2.0, with optional HeyGen-hosted rendering.
55
+ - [MCP for Blender](https://github.com/ahujasid/mcp-for-blender), formerly Blender
56
+ MCP, lets an agent drive a running Blender interactively.
57
+
58
+ SlopCamera renders HTML motion too, and adds diagrams, Three.js scenes, headless
59
+ Blender, CadQuery, and Manim renders, and edits of your own footage in one local
60
+ project. It has no cloud rendering and a much smaller community. See
61
+ [the full comparison](https://slopcamera.com/docs/explanation/why-slopcamera#compared-with-other-tools).
62
+
63
+ ## Install SlopCamera
51
64
 
52
65
  Install [Bun 1.3.14 or newer](https://bun.sh), then install the verified
53
- [Slopcamera v3.4.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.4.0)
66
+ [SlopCamera v3.6.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.6.0)
54
67
  from its canonical archive:
55
68
 
56
69
  ```sh
57
- bun add --global https://github.com/hraness/slopcamera/releases/download/v3.4.0/hraness-slopcamera-3.4.0.tgz
70
+ bun add --global https://github.com/hraness/slopcamera/releases/download/v3.6.0/hraness-slopcamera-3.6.0.tgz
58
71
  slopcamera skill install --target agents
59
72
  ```
60
73
 
@@ -90,8 +103,9 @@ Native engines install separately. The [source-install guide](docs/how-to/use-cu
90
103
  explains workspace placement and durable-run identity; the
91
104
  [capability reference](docs/reference/capabilities.md) names runtime requirements.
92
105
 
93
- Slopcamera is a CLI. On macOS, an optional unbundled menu-bar companion can show
94
- the files in the agent outputs directory; `slopcamera menubar` runs a prebuilt
106
+ SlopCamera is a CLI. On macOS, an optional unbundled menu-bar companion shows
107
+ what SlopCamera is rendering, how the last job ended, your last known credits
108
+ balance and the newest files in the agent outputs directory; `slopcamera menubar` runs a prebuilt
95
109
  companion directly and does not install or launch an application bundle. The
96
110
  companion is a thin product binary over the shared
97
111
  [desktop-foundation](https://github.com/hraness/desktop-foundation) crate, pinned
@@ -105,14 +119,14 @@ when working from source.
105
119
  [Atet v3.2.3](https://github.com/hraness/atet/releases/tag/v3.2.3) and its
106
120
  [original archive](https://github.com/hraness/atet/releases/download/v3.2.3/hraness-atet-3.2.3.tgz)
107
121
  remain historical publication evidence. Those immutable bytes install Atet,
108
- not Slopcamera. Renaming the repository does not create a renamed package or
122
+ not SlopCamera. Renaming the repository does not create a renamed package or
109
123
  change an existing release.
110
124
 
111
125
  </details>
112
126
 
113
127
  ## Make your first diagram
114
128
 
115
- After installing Slopcamera, this local task needs no model account. In a new directory,
129
+ After installing SlopCamera, this local task needs no model account. In a new directory,
116
130
  create the included diagram, check it, and render it:
117
131
 
118
132
  ```sh
@@ -135,10 +149,12 @@ For detailed native 3D, follow
135
149
  [Your first native film](docs/tutorials/first-native-film.md). To generate editable
136
150
  architecture from dimensions, [build a parametric design](https://slopcamera.com/docs/how-to/parametric-design).
137
151
 
138
- ## What Slopcamera does
152
+ ## What SlopCamera does
139
153
 
140
- Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes,
154
+ SlopCamera lets your coding agent make images, diagrams, animation, 3D scenes,
141
155
  and video from source files it can keep revising.
156
+ Direct cel animation, pixel art, math explainers, and period film looks with the
157
+ [style direction guide](docs/how-to/direct-visual-styles.md).
142
158
 
143
159
  ### Author scenes and direct cameras
144
160
 
@@ -157,7 +173,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
157
173
  capture appearance; they do not establish collision geometry or editable native
158
174
  meshes. See [Directed scenes](docs/spatial-scenes.md).
159
175
 
160
- Slopcamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
176
+ SlopCamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
161
177
  document describes beats, actions, camera coverage, and look intents, compiles
162
178
  into proposed performance/camera/cinema/material-lighting/shot documents, and
163
179
  fans out into bounded galleries per axis. Declared effects bind into renders,
@@ -173,12 +189,12 @@ slopcamera workflows plan cinematic-world --input pack-input.json --json
173
189
  The `cinematic-world` workflow consumes one inert recipe pack — a bounded,
174
190
  content-addressed JSON document an agent can author — and stops before
175
191
  selection. Registration and selection stay explicit `scene project` operations.
176
- See [Direct a cinematic world](docs/how-to/direct-cinematic-worlds.md) and the
192
+ See [Plan camera moves, lighting, and effects for a 3D scene](docs/how-to/direct-cinematic-worlds.md) and the
177
193
  [extension architecture](docs/extension-architecture.md).
178
194
 
179
195
  ### Film native worlds and educational animation
180
196
 
181
- Slopcamera can direct Blender for detailed sets, materials,
197
+ SlopCamera can direct Blender for detailed sets, materials,
182
198
  lighting, skinned characters, cloth and liquid caches; CadQuery for parametric
183
199
  solids and STEP; and Manim Community for mathematical animation. Seven editable
184
200
  starters include a product, character, shaded street, cloth, liquid, CAD bracket,
@@ -194,7 +210,7 @@ camera clock for reuse. Supported GLB derivatives and calibrated cameras can
194
210
  cross between native and portable scenes; rigs, solvers, and procedural materials
195
211
  remain native. Blender and Python environments are installed separately, and
196
212
  source execution requires explicit current-user trust. See [Native film studio](docs/studio.md)
197
- and [Make an educational video](docs/how-to/educational-video.md).
213
+ and [Make a math explainer video with Manim](docs/how-to/educational-video.md).
198
214
 
199
215
  ### Build diagrams and motion graphics
200
216
 
@@ -268,12 +284,12 @@ slopcamera workflows list --json
268
284
  slopcamera workflows show social-variants --json
269
285
  ```
270
286
 
271
- ## How Slopcamera works
287
+ ## How SlopCamera works
272
288
 
273
289
  Keep the source that owns each creative decision. A native scene owns a rig or
274
290
  simulation; a portable scene owns supported geometry, cameras, and media surfaces;
275
291
  a diagram owns its objects and labels; a video project owns cuts and delivery.
276
- Slopcamera connects these through explicit assets and rendered derivatives.
292
+ SlopCamera connects these through explicit assets and rendered derivatives.
277
293
 
278
294
  1. **Prepare the sources.** Import footage and assets or author a scene, diagram,
279
295
  or native program. Inspect available tools before choosing an engine.
@@ -317,7 +333,7 @@ declarative graphs, approvals, and resuming work.
317
333
 
318
334
  ## Design and trust
319
335
 
320
- There is no Slopcamera account, hosted project database, or browser generation service.
336
+ There is no SlopCamera account, hosted project database, or browser generation service.
321
337
  Ordinary editing and rendering remain local. Gateway generation and selected
322
338
  cloud analysis use credentials from the local process and request explicit
323
339
  acknowledgement before uploading named media. The optional hosted API runs each
@@ -348,7 +364,7 @@ vision](https://algal.computer/docs/vision/) states the bet behind it.
348
364
  ## Documentation
349
365
 
350
366
  - **Learn:** [Your first diagram](docs/tutorials/first-diagram.md) · [Your first native film](docs/tutorials/first-native-film.md).
351
- - **Make a result:** [Edit video](docs/how-to/edit-video.md) · [Generate media](docs/how-to/generate-media.md) · [Run workflows](docs/how-to/run-workflows.md) · [Educational video](docs/how-to/educational-video.md) · [Direct a cinematic world](docs/how-to/direct-cinematic-worlds.md).
367
+ - **Make a result:** [Edit video](docs/how-to/edit-video.md) · [Generate media](docs/how-to/generate-media.md) · [Run workflows](docs/how-to/run-workflows.md) · [Educational video](docs/how-to/educational-video.md) · [Plan camera moves, lighting, and effects for a 3D scene](docs/how-to/direct-cinematic-worlds.md).
352
368
  - **Look up support:** [Capabilities and release availability](docs/reference/capabilities.md) · [SDK entrypoints](docs/reference/sdk.md) · [Creative tools](docs/html-overlay-creative-toolkit.md).
353
369
  - **Understand the system:** [Architecture](docs/architecture.md) · [Native studio](docs/studio.md) · [Directed scenes](docs/spatial-scenes.md) · [Extension architecture](docs/extension-architecture.md).
354
370
 
@@ -412,4 +428,4 @@ an acknowledgment; the unacknowledged offer expires after ten minutes and does n
412
428
  consume the weekly shown cooldown. `support dismiss` opts out across the local suite;
413
429
  `support snooze` pauses for 30 days. `HRANESS_SUPPORT_AUDIENCE=off` disables ambient
414
430
  notices and claimed offers. Payment is an explicit human choice on the shared support
415
- page; Slopcamera has no product-updates mailing list or account-status lookup.
431
+ page; SlopCamera has no product-updates mailing list or account-status lookup.
@@ -1,4 +1,4 @@
1
- # Slopcamera local host and CLI
1
+ # SlopCamera local host and CLI
2
2
 
3
3
  Slopcamera reads finished recording bundles from repository-local directories, combines independent camera and audio takes on one typed project clock, then applies non-destructive edits through the `slopcamera` CLI. The CLI is the product surface for agents. An optional unbundled `slopcamera menubar` companion surfaces the agent outputs directory in the macOS menu bar; it carries no capture or editing authority.
4
4
 
@@ -19,6 +19,12 @@ export interface HtmlOverlayFrameRenderRequest {
19
19
  readonly browserRuntime: HtmlOverlayBrowserRuntimeBinding;
20
20
  readonly outputDirectory: string;
21
21
  readonly resources: readonly BoundHtmlOverlayResource[];
22
+ /**
23
+ * Optional ascending subset of frame indexes to capture, for stills and
24
+ * previews. Omitted means every frame. Captured files keep their absolute
25
+ * frame index in the name, so `frame-%08d.png` stays addressable.
26
+ */
27
+ readonly frames?: readonly number[];
22
28
  }
23
29
 
24
30
  export interface HtmlOverlayFrameRenderResult {
@@ -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.5.0" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.7.0" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "./errors";
2
2
  import { MAX_EVENT_QUERY_LIMIT } from "./query-limits";
3
+ import { HTML_FILM_CUTS, type HtmlFilmCommand, type HtmlFilmCut } from "./html-film-names";
3
4
  import { STUDIO_TEMPLATES, type StudioTemplate } from "./studio-template-names";
4
5
  import {
5
6
  slopcameraGalleryKinds,
@@ -121,6 +122,7 @@ export type SpatialProjectCommand = JsonOption & {
121
122
 
122
123
  export type CliCommand =
123
124
  | { readonly kind: "html-render"; readonly input: string; readonly dryRun: boolean; readonly json: boolean }
125
+ | HtmlFilmCommand
124
126
  | StudioCommand
125
127
  | DirectingCommand
126
128
  | SpatialWorldCommand
@@ -731,6 +733,63 @@ function fail(message: string): never {
731
733
  throw new CliError("usage", message);
732
734
  }
733
735
 
736
+ const HTML_STILL_USAGE = "Use html still --input <scene.json> --at <seconds>[,<seconds>...] --output <dir> [--json].";
737
+ const HTML_PREVIEW_USAGE = "Use html preview --input <scene.json> --output <dir> [--every <seconds>] [--json].";
738
+ const HTML_DELIVER_USAGE = "Use html deliver <export.json> --basename <name> --poster-at <seconds> --social-at <seconds> [--cuts 1:1,9:16] [--per-beat-clips] [--beats <beats.json>] [--output <dir>] [--json].";
739
+
740
+ function secondsList(value: string | undefined, usage: string): readonly number[] {
741
+ if (value === undefined) fail(usage);
742
+ return value.split(",").map(part => {
743
+ if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/u.test(part.trim())) fail(`Times must be seconds such as 3 or 12.5; got ${part}.`);
744
+ return Number(part.trim());
745
+ });
746
+ }
747
+
748
+ function parseHtmlFilmArgs(action: "still" | "preview" | "deliver", argv: readonly string[]): HtmlFilmCommand {
749
+ if (action === "still") {
750
+ const parsed = parseOptions(argv, { "--input": "value", "--at": "value", "--output": "value", "--json": "flag" });
751
+ const input = optionString(parsed, "--input");
752
+ const output = optionString(parsed, "--output");
753
+ if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_STILL_USAGE);
754
+ return { kind: "html-film", action, input, output, at: secondsList(optionString(parsed, "--at"), HTML_STILL_USAGE), json: optionFlag(parsed, "--json") };
755
+ }
756
+ if (action === "preview") {
757
+ const parsed = parseOptions(argv, { "--input": "value", "--every": "value", "--output": "value", "--json": "flag" });
758
+ const input = optionString(parsed, "--input");
759
+ const output = optionString(parsed, "--output");
760
+ if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_PREVIEW_USAGE);
761
+ const every = optionString(parsed, "--every");
762
+ const [seconds] = every === undefined ? [2] : secondsList(every, HTML_PREVIEW_USAGE);
763
+ if (seconds === undefined || seconds <= 0) fail("--every must be more than 0 seconds.");
764
+ return { kind: "html-film", action, input, output, every: seconds, json: optionFlag(parsed, "--json") };
765
+ }
766
+ const parsed = parseOptions(argv, {
767
+ "--basename": "value", "--poster-at": "value", "--social-at": "value", "--cuts": "value",
768
+ "--per-beat-clips": "flag", "--beats": "value", "--output": "value", "--json": "flag",
769
+ });
770
+ const basename = optionString(parsed, "--basename");
771
+ if (parsed.positionals.length !== 1 || basename === undefined) fail(HTML_DELIVER_USAGE);
772
+ const single = (name: string) => {
773
+ const values = secondsList(optionString(parsed, name), HTML_DELIVER_USAGE);
774
+ if (values.length !== 1) fail(`${name} takes one time in seconds.`);
775
+ return values[0]!;
776
+ };
777
+ const cutsInput = optionString(parsed, "--cuts");
778
+ const cuts = cutsInput === undefined ? [] : cutsInput.split(",").map(cut => {
779
+ if (!(HTML_FILM_CUTS as readonly string[]).includes(cut)) fail(`--cuts takes ${HTML_FILM_CUTS.join(", ")}; got ${cut}.`);
780
+ return cut as HtmlFilmCut;
781
+ });
782
+ const beats = optionString(parsed, "--beats");
783
+ const output = optionString(parsed, "--output");
784
+ return {
785
+ kind: "html-film", action, exportPath: parsed.positionals[0]!, basename,
786
+ posterAt: single("--poster-at"), socialAt: single("--social-at"), cuts,
787
+ perBeatClips: optionFlag(parsed, "--per-beat-clips"),
788
+ ...(beats === undefined ? {} : { beats }), ...(output === undefined ? {} : { output }),
789
+ json: optionFlag(parsed, "--json"),
790
+ };
791
+ }
792
+
734
793
  function parseOptions(argv: readonly string[], spec: OptionSpec): ParsedOptions {
735
794
  const options: Record<string, OptionValue> = {};
736
795
  const positionals: string[] = [];
@@ -3631,6 +3690,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3631
3690
  const command = argv[0]!;
3632
3691
  switch (command) {
3633
3692
  case "html": {
3693
+ if (argv[1] === "still" || argv[1] === "preview" || argv[1] === "deliver") return parseHtmlFilmArgs(argv[1], argv.slice(2));
3634
3694
  if (argv[1] !== "render") fail("Use html render --input <scene.json> [--dry-run] [--json].");
3635
3695
  const parsed = parseOptions(argv.slice(2), { "--input": "value", "--dry-run": "flag", "--json": "flag" });
3636
3696
  const input = optionString(parsed, "--input");
@@ -3659,8 +3719,10 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3659
3719
  case "menubar": {
3660
3720
  const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--foreground": "flag", "--background": "flag" });
3661
3721
  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] or slopcamera menubar install|uninstall|status. See slopcamera help menubar.");
3722
+ if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status", "start"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] or slopcamera menubar install|uninstall|status|start. See slopcamera help menubar.");
3663
3723
  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.");
3724
+ // `start` opens it now and returns, like --background; the login item's hints use it.
3725
+ if (positionals[0] === "start") return { kind: "menubar", action: "run", mode: "background", json: optionFlag(parsed, "--json") };
3664
3726
  return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
3665
3727
  }
3666
3728
  case "outputs": {
@@ -125,6 +125,11 @@ export function commandHostResourceClaims(
125
125
  coordinator: HostResourceCoordinator,
126
126
  ): readonly HostResourceClaim[] {
127
127
  switch (command.kind) {
128
+ case "html-film": return command.action === "deliver"
129
+ ? claims(coordinator, ["cpu", "ffmpeg", "local-io"])
130
+ : claims(coordinator, command.action === "preview"
131
+ ? ["cpu", "local-io", "browser", "ffmpeg"]
132
+ : ["cpu", "local-io", "browser"]);
128
133
  case "html-render": return command.dryRun
129
134
  ? claims(coordinator, ["cpu", "local-io"])
130
135
  : claims(coordinator, ["cpu", "local-io", "browser", "ffmpeg", "output-publication", "project-render"]);
@@ -1,3 +1,4 @@
1
+ import { activityLabel, recordActivity, recordCredits } from "./menubar-status";
1
2
  import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
2
3
  import { SlopcameraCloudError } from "../../../src/cloud-errors";
3
4
  import {
@@ -187,6 +188,7 @@ import { commandHelp, completions } from "./help";
187
188
  import { createLocalSlopcameraCapabilityManifest } from "./capability-manifest";
188
189
  import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
189
190
  import { executeHtmlSceneCommand } from "./html-scene";
191
+ import { executeHtmlFilmCommand, formatHtmlFilmResult } from "./html-film";
190
192
  import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
191
193
  import { launchMenubar, manageMenubar, reportOutputsRoot } from "./menubar";
192
194
  import {
@@ -362,7 +364,7 @@ import {
362
364
  workflowRunStore,
363
365
  } from "./workflow-runs";
364
366
 
365
- export const SLOPCAMERA_VERSION = "3.5.0";
367
+ export const SLOPCAMERA_VERSION = "3.7.0";
366
368
 
367
369
  // Legacy direct renders predate per-target output contracts. Keep them
368
370
  // bounded generously enough for long-form production while preventing one
@@ -5548,6 +5550,7 @@ async function handleCreditsStatus(
5548
5550
  } catch (error) {
5549
5551
  throw creditsErrorToCli(error);
5550
5552
  }
5553
+ recordCredits(context.stateRoot, { usd: balance.balance.usd, low: balance.lowBalance, checkedAt: context.clock() });
5551
5554
  const view = {
5552
5555
  configured: true,
5553
5556
  tokenSource: loaded.source,
@@ -7448,6 +7451,16 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
7448
7451
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
7449
7452
  return;
7450
7453
  }
7454
+ case "html-film": {
7455
+ const output = await executeHtmlFilmCommand(applicationContext(context), command,
7456
+ context.abortSignal ?? new AbortController().signal,
7457
+ { progress: stage => context.io.stderr(`HTML film: ${stage}\n`) });
7458
+ writeValue(context.io, command.json, output, () => formatHtmlFilmResult(output));
7459
+ if (output.kind === "slopcamera.html-film-delivery" && !output.withinBudget) {
7460
+ throw new CliError("invalid-data", "One or more delivered files are over budget.");
7461
+ }
7462
+ return;
7463
+ }
7451
7464
  case "studio": {
7452
7465
  const output = await executeStudioCommand(applicationContext(context), command, context.abortSignal ?? new AbortController().signal);
7453
7466
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
@@ -8267,6 +8280,7 @@ type MutationReference =
8267
8280
  function commandMutationReference(command: CliCommand): MutationReference | undefined {
8268
8281
  switch (command.kind) {
8269
8282
  case "html-render": return undefined; // A fresh retained attempt owns its publication lease.
8283
+ case "html-film": return undefined; // Fresh private job directories; outputs are plain caller files.
8270
8284
  case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
8271
8285
  case "directing": return undefined; // The directing store owns its explicit lease.
8272
8286
  case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
@@ -8419,6 +8433,7 @@ async function resolveMutationTarget(
8419
8433
  export async function runCli(argv: readonly string[], dependencies: CliDependencies = {}): Promise<number> {
8420
8434
  const io = dependencies.io ?? processIo;
8421
8435
  let jsonRequested = argv.includes("--json") || argv.includes("--jsonl");
8436
+ let activity: { readonly stateRoot: string; readonly label: string; readonly startedAt: number } | undefined;
8422
8437
  try {
8423
8438
  const command = parseCliArgs(argv);
8424
8439
  jsonRequested = "json" in command ? command.json : command.kind === "events" && command.format !== "human";
@@ -8513,6 +8528,11 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
8513
8528
  ? {}
8514
8529
  : { renderGalleryScene: dependencies.renderGalleryScene }),
8515
8530
  };
8531
+ const label = activityLabel(command);
8532
+ if (label !== undefined) {
8533
+ activity = { stateRoot, label, startedAt: context.clock() };
8534
+ recordActivity(stateRoot, { state: "running", label, startedAt: activity.startedAt });
8535
+ }
8516
8536
  const mutationTarget = await resolveMutationTarget(paths, stateRoot, command);
8517
8537
  await withCommandHostResources(context, command, async admittedContext => {
8518
8538
  if (mutationTarget === undefined) {
@@ -8535,9 +8555,15 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
8535
8555
  || command.kind === "project-render" && command.action === "run" && !command.dryRun) {
8536
8556
  reportUsefulResult(dependencies.onUsefulResult);
8537
8557
  }
8558
+ if (activity !== undefined) {
8559
+ recordActivity(activity.stateRoot, { state: "done", label: activity.label, startedAt: activity.startedAt, finishedAt: io.now().getTime() });
8560
+ }
8538
8561
  return 0;
8539
8562
  } catch (error) {
8540
8563
  const failure = asCliError(error);
8564
+ if (activity !== undefined) {
8565
+ recordActivity(activity.stateRoot, { state: "failed", label: activity.label, startedAt: activity.startedAt, finishedAt: io.now().getTime(), error: failure.code });
8566
+ }
8541
8567
  const payload = {
8542
8568
  error: {
8543
8569
  code: failure.code,
@@ -1,9 +1,11 @@
1
+ import { slopcameraSupportAdvancedHelp, slopcameraSupportHelpLine } from "../../../src/support";
2
+
1
3
  import { HTML_OVERLAY_SCAFFOLD_KINDS } from "../html-overlay";
2
4
 
3
5
  const HTML_OVERLAY_SCAFFOLD_KIND_HELP =
4
6
  HTML_OVERLAY_SCAFFOLD_KINDS.join("|");
5
7
 
6
- const GLOBAL_HELP = `slopcamera: a media studio for coding agents
8
+ const globalHelp = () => `slopcamera: a media studio for coding agents
7
9
 
8
10
  Usage: slopcamera <command> [options]
9
11
 
@@ -23,7 +25,8 @@ Commands:
23
25
  Author and render retained Blender, CAD and Manim productions
24
26
  image vectorize|generate|gallery|icon
25
27
  Create local SVGs, generated images, or review galleries
26
- html catalog|scaffold|render Author HTML scenes and export video with local audio
28
+ html catalog|scaffold|init|render|still|preview|deliver
29
+ Author HTML scenes and launch films, render, and deliver web video
27
30
  style list|show Discover reusable art direction and review criteria
28
31
  workflows list|show|plan|run Plan or run a reviewed reusable workflow
29
32
  code init|check|plan|run Author, preflight, and run trusted TypeScript workflows
@@ -35,11 +38,6 @@ Commands:
35
38
  credits status|topup|wait|forget
36
39
  Buy and inspect hosted generation credits
37
40
  media audio|color Apply local non-destructive audio and video effects
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
41
- support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
42
- Optional support; no feature requires payment
43
41
  outputs Print the agent outputs directory
44
42
  recordings list List recording bundles
45
43
  projects list|create List projects or create one from a recording
@@ -63,19 +61,21 @@ Examples:
63
61
  slopcamera render run REC --output out.mp4 Render a recording
64
62
 
65
63
  Run slopcamera help <command> for command-specific help.
66
- Optional support: slopcamera support · Turn off: HRANESS_SUPPORT_AUDIENCE=off`;
64
+ ${slopcameraSupportHelpLine()}`;
67
65
 
68
66
  const HELP: Readonly<Record<string, string>> = {
67
+ advanced: `${slopcameraSupportAdvancedHelp()}`,
69
68
  menubar: `Usage:
70
69
  slopcamera menubar [--foreground|--background] [--json]
71
- slopcamera menubar install|uninstall|status [--json]
70
+ slopcamera menubar install|uninstall|status|start [--json]
72
71
 
73
- Shows 📷 in the menu bar with your recent outputs. It only lists and opens
74
- files in the outputs folder (slopcamera outputs).
72
+ Shows what Slopcamera is doing in the menu bar: a render in progress, how the
73
+ last job ended and your last known credits balance, plus your newest outputs.
74
+ It only reads that status and the outputs folder (slopcamera outputs).
75
75
 
76
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
77
+ menubar start Open it now and return (same as --background)
78
+ menubar install Open it every time you log in
79
79
  menubar uninstall Stop opening it at login and remove the installed copy
80
80
  menubar status Check whether it opens at login and is running
81
81
 
@@ -83,9 +83,10 @@ If it's already in your menu bar, starting it again says so and succeeds.
83
83
  macOS shows a notice that slopcamera-menubar can open at login. Turn it off any
84
84
  time in System Settings › General › Login Items & Extensions.
85
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.`,
86
+ The menu bar isn't in released packages yet, so it isn't listed in
87
+ slopcamera help. In a Slopcamera checkout, build it with
88
+ cargo build --release --manifest-path desktop/Cargo.toml and slopcamera finds
89
+ it; elsewhere, set SLOPCAMERA_MENUBAR to the built file.`,
89
90
  capabilities: `Usage: slopcamera capabilities [--json]
90
91
 
91
92
  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.`,
@@ -304,7 +305,11 @@ compliance. Drawing sheets are exposed through this CLI and the root SDK, not co
304
305
  [--cell <n>] [--tile|--no-tile] [--json]
305
306
  slopcamera image icon <subject> --output <file.svg>
306
307
  [--purpose <mark|illustration>] [--model <model>] [--ink <#rgb>]
308
+ [--context <card|hero|inline>] [--candidates <1-4>]
307
309
  [--rounds <1-4>] [--critique-model <model>] [--keep-raster] [--json]
310
+ slopcamera image icon --set <file.json> --output-dir <directory>
311
+ [--context <card|hero|inline>] [--candidates <1-4>]
312
+ [--set-rounds <1-4>] [--keep-raster] [--json]
308
313
 
309
314
  Explicit --output file commands delegate to @hraness/slopcamera. Vectorization is local,
310
315
  bounded, checksum-pinned, and emits inert SVG. File generation uses Vercel AI Gateway with the
@@ -312,7 +317,10 @@ caller's environment credential. The --prompt spelling without --output is an al
312
317
  content-addressed \`ai image generate\` lane and returns project-composable content hash references.
313
318
  Icon combines a style-locked Gateway raster, local ink extraction and VTracer tracing into a
314
319
  canonical isometric line-art SVG; --rounds above 1 adds a vision-model critique that revises the
315
- prompt between attempts. Gallery fans one subject out to bounded parallel candidates — driven by
320
+ prompt between attempts. --context tunes the prompt and gates to where the art lands, and --set
321
+ generates a manifest-declared family at once: pooled candidates, joint selection toward the
322
+ family's measured coverage and stroke median, and a contact-sheet critique of the whole set before
323
+ anything publishes. Gallery fans one subject out to bounded parallel candidates — driven by
316
324
  variation axes or an explicit candidate list — keeps every candidate with its provenance, and
317
325
  composes a labelled contact sheet plus receipt for agent review. Texture cells repeat the
318
326
  candidate 2×2 so seams are inspectable (--tile/--no-tile overrides). It never replaces existing
@@ -330,7 +338,12 @@ direction, not proof of visual quality or historically authentic footage.`,
330
338
  html: `Usage:
331
339
  slopcamera html catalog [--json]
332
340
  slopcamera html scaffold <${HTML_OVERLAY_SCAFFOLD_KIND_HELP}> --output <file.html>
341
+ slopcamera html init <dir> --template launch-film [--aspect 16:9|1:1|9:16] [--json]
333
342
  slopcamera html render --input <scene.json> [--dry-run] [--json]
343
+ slopcamera html still --input <scene.json> --at <seconds>[,<seconds>...] --output <dir> [--json]
344
+ slopcamera html preview --input <scene.json> --output <dir> [--every <seconds>] [--json]
345
+ slopcamera html deliver <export.json> --basename <name> --poster-at <seconds> --social-at <seconds>
346
+ [--cuts 1:1,9:16,4:5] [--per-beat-clips] [--beats <beats.json>] [--output <dir>] [--json]
334
347
 
335
348
  Catalog lists the closed scaffold profiles in stable order with their primary jobs, render
336
349
  substrates, and current exact browser-library versions. Scaffold creates a complete transparent
@@ -344,7 +357,16 @@ with separate scene-video and original-audio tracks. Device dimensions must be e
344
357
  Duration rounds up to whole frames; audio starts at zero and is trimmed or padded to that
345
358
  duration. Soundtracks encode as 48 kHz stereo AAC at 320 kb/s. Dry run checks source and
346
359
  workload bounds without launching a browser, importing audio, or writing project state.
347
- Progress uses stderr; --json keeps stdout machine-readable.`,
360
+ Progress uses stderr; --json keeps stdout machine-readable.
361
+
362
+ Init creates a launch-film project (film.html, film.css, film.js, build.ts and a sample
363
+ product mockup) in a new directory. Its build writes out/scene.json, out/beats.json and
364
+ out/captions.vtt. Still and preview draw frames with the same injected runtime that render
365
+ uses, so a still matches the rendered frame at that time; preview adds a contact sheet.
366
+ Deliver reads output.path from html render --json output and writes a faststart H.264 MP4,
367
+ a VP9 WebM, a poster, a 1200x630 social still, optional aspect cuts over a blurred fill, and
368
+ optional 6 to 10 second clips per beat. It prints a receipt per file and exits non-zero when
369
+ a file is over its budget: 12 MB per MP4, 10 MB for the WebM, 250 KB per JPEG.`,
348
370
  operations: `Usage:
349
371
  slopcamera operations list [--json]
350
372
  slopcamera operations show <kind>[@<version>] [--json]
@@ -701,8 +723,8 @@ Long inactivity is analyzed and removed by default; use --keep-inactivity to opt
701
723
  };
702
724
 
703
725
  export function commandHelp(topic: readonly string[]): string {
704
- if (topic.length === 0) return GLOBAL_HELP;
705
- return HELP[topic[0]!] ?? GLOBAL_HELP;
726
+ if (topic.length === 0) return globalHelp();
727
+ return HELP[topic[0]!] ?? globalHelp();
706
728
  }
707
729
 
708
730
  export function completions(words: readonly string[]): readonly string[] {
@@ -712,7 +734,7 @@ export function completions(words: readonly string[]): readonly string[] {
712
734
  if (words.length <= 1) return topLevel;
713
735
  const command = words[0];
714
736
  if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
715
- if (command === "html") return ["catalog", "scaffold", "render"];
737
+ if (command === "html") return ["catalog", "scaffold", "init", "render", "still", "preview", "deliver"];
716
738
  if (command === "style") return ["list", "show"];
717
739
  if (command === "direct") return ["init", "anchor", "plan", "start", "inspect", "revise", "generate", "resume", "review", "assemble", "cleanup"];
718
740
  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"];