@hraness/slopcamera 3.6.0 → 3.8.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 (57) hide show
  1. package/README.md +49 -33
  2. package/apps/desktop/README.md +3 -10
  3. package/apps/desktop/application/html-overlay-renderer.ts +6 -0
  4. package/apps/desktop/application/operation.ts +1 -1
  5. package/apps/desktop/application/spatial-assets.ts +1 -1
  6. package/apps/desktop/cli/{menubar-status.ts → activity-status.ts} +24 -21
  7. package/apps/desktop/cli/args.ts +62 -10
  8. package/apps/desktop/cli/command-host-resources.ts +5 -1
  9. package/apps/desktop/cli/commands.ts +26 -9
  10. package/apps/desktop/cli/desktop-status.ts +271 -0
  11. package/apps/desktop/cli/help.ts +36 -28
  12. package/apps/desktop/cli/html-film-init.ts +158 -0
  13. package/apps/desktop/cli/html-film-names.ts +17 -0
  14. package/apps/desktop/cli/html-film.ts +471 -0
  15. package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
  16. package/apps/desktop/cli/html-scene.ts +3 -3
  17. package/apps/desktop/cli/legacy-login.ts +184 -0
  18. package/apps/desktop/cli/main.ts +9 -0
  19. package/apps/desktop/cli/outputs.ts +19 -0
  20. package/apps/desktop/cli/portable-surface.ts +55 -1
  21. package/apps/desktop/cli/registry.ts +314 -0
  22. package/apps/desktop/cli/verbs.ts +193 -0
  23. package/apps/desktop/dist/cli/main.js +338 -316
  24. package/apps/desktop/html-overlay/film.ts +418 -0
  25. package/dist/cli.js +13 -2
  26. package/dist/generate.js +1 -1
  27. package/dist/{index-4z9y2wa2.js → index-842t02pp.js} +1 -1
  28. package/dist/{index-qxfexvj3.js → index-k9bjw80z.js} +20 -4
  29. package/dist/index-p74xy76d.js +3 -0
  30. package/dist/{index-fevf4ppk.js → index-y8spxs77.js} +10 -7
  31. package/dist/index.js +1 -1
  32. package/dist/operations.js +1 -1
  33. package/dist/workflow.js +1 -1
  34. package/docs/README.md +6 -6
  35. package/examples/html/launch-film/README.md +51 -0
  36. package/examples/html/launch-film/build.ts +158 -0
  37. package/examples/html/launch-film/film.css +593 -0
  38. package/examples/html/launch-film/film.html +85 -0
  39. package/examples/html/launch-film/film.js +410 -0
  40. package/examples/html/launch-film/film.json +63 -0
  41. package/examples/html/launch-film/mockups.tsx +74 -0
  42. package/examples/html/launch-film/timeline.ts +57 -0
  43. package/package.json +24 -11
  44. package/skills/slopcamera/SKILL.md +4 -3
  45. package/skills/slopcamera/references/install.md +3 -3
  46. package/skills/slopcamera/references/launch-films.md +158 -0
  47. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  48. package/skills/slopcamera/references/visual-communication.md +1 -1
  49. package/src/cli.ts +125 -9
  50. package/src/generation-pricing.ts +21 -1
  51. package/src/icon-set.ts +1086 -0
  52. package/src/icon.ts +396 -59
  53. package/src/index.ts +1 -0
  54. package/src/operations.ts +52 -0
  55. package/src/version.ts +1 -1
  56. package/apps/desktop/cli/menubar.ts +0 -272
  57. package/dist/index-nxtmy18t.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.5.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. On macOS, `slopcamera menubar` puts Slopcamera in the menu bar and explains each macOS notice before it appears.
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.5.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.5.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.5.0/hraness-slopcamera-3.5.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,15 +103,16 @@ 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 shows
94
- what Slopcamera is rendering, how the last job ended, your last known credits
95
- balance and the newest files in the agent outputs directory; `slopcamera menubar` runs a prebuilt
96
- companion directly and does not install or launch an application bundle. The
97
- companion is a thin product binary over the shared
98
- [desktop-foundation](https://github.com/hraness/desktop-foundation) crate, pinned
99
- by immutable tag. Build it explicitly with
100
- `cargo build --release --manifest-path desktop/menubar/Cargo.toml`
101
- when working from source.
106
+ SlopCamera is a CLI with no menu-bar app or background process.
107
+ `slopcamera status` shows what is running, how the last job ended, your last
108
+ known credits balance, the newest outputs and whether the retired menu-bar
109
+ companion still opens at login. `slopcamera tui` keeps
110
+ that screen open and refreshes it; `slopcamera tui --snapshot` prints it once
111
+ and `--json` returns the data. `slopcamera outputs list|open|reveal` lists the
112
+ newest outputs or opens one on this Mac, `slopcamera commands --json` lists
113
+ every command with what it may change, and `slopcamera legacy retire` stops
114
+ the retired menu-bar companion opening at login by moving its LaunchAgent aside; it
115
+ deletes nothing, and `slopcamera doctor` prints the command that restores it.
102
116
 
103
117
  <details>
104
118
  <summary>Historical Atet release evidence</summary>
@@ -106,14 +120,14 @@ when working from source.
106
120
  [Atet v3.2.3](https://github.com/hraness/atet/releases/tag/v3.2.3) and its
107
121
  [original archive](https://github.com/hraness/atet/releases/download/v3.2.3/hraness-atet-3.2.3.tgz)
108
122
  remain historical publication evidence. Those immutable bytes install Atet,
109
- not Slopcamera. Renaming the repository does not create a renamed package or
123
+ not SlopCamera. Renaming the repository does not create a renamed package or
110
124
  change an existing release.
111
125
 
112
126
  </details>
113
127
 
114
128
  ## Make your first diagram
115
129
 
116
- After installing Slopcamera, this local task needs no model account. In a new directory,
130
+ After installing SlopCamera, this local task needs no model account. In a new directory,
117
131
  create the included diagram, check it, and render it:
118
132
 
119
133
  ```sh
@@ -136,10 +150,12 @@ For detailed native 3D, follow
136
150
  [Your first native film](docs/tutorials/first-native-film.md). To generate editable
137
151
  architecture from dimensions, [build a parametric design](https://slopcamera.com/docs/how-to/parametric-design).
138
152
 
139
- ## What Slopcamera does
153
+ ## What SlopCamera does
140
154
 
141
- Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes,
155
+ SlopCamera lets your coding agent make images, diagrams, animation, 3D scenes,
142
156
  and video from source files it can keep revising.
157
+ Direct cel animation, pixel art, math explainers, and period film looks with the
158
+ [style direction guide](docs/how-to/direct-visual-styles.md).
143
159
 
144
160
  ### Author scenes and direct cameras
145
161
 
@@ -158,7 +174,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
158
174
  capture appearance; they do not establish collision geometry or editable native
159
175
  meshes. See [Directed scenes](docs/spatial-scenes.md).
160
176
 
161
- Slopcamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
177
+ SlopCamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
162
178
  document describes beats, actions, camera coverage, and look intents, compiles
163
179
  into proposed performance/camera/cinema/material-lighting/shot documents, and
164
180
  fans out into bounded galleries per axis. Declared effects bind into renders,
@@ -174,12 +190,12 @@ slopcamera workflows plan cinematic-world --input pack-input.json --json
174
190
  The `cinematic-world` workflow consumes one inert recipe pack — a bounded,
175
191
  content-addressed JSON document an agent can author — and stops before
176
192
  selection. Registration and selection stay explicit `scene project` operations.
177
- See [Direct a cinematic world](docs/how-to/direct-cinematic-worlds.md) and the
193
+ See [Plan camera moves, lighting, and effects for a 3D scene](docs/how-to/direct-cinematic-worlds.md) and the
178
194
  [extension architecture](docs/extension-architecture.md).
179
195
 
180
196
  ### Film native worlds and educational animation
181
197
 
182
- Slopcamera can direct Blender for detailed sets, materials,
198
+ SlopCamera can direct Blender for detailed sets, materials,
183
199
  lighting, skinned characters, cloth and liquid caches; CadQuery for parametric
184
200
  solids and STEP; and Manim Community for mathematical animation. Seven editable
185
201
  starters include a product, character, shaded street, cloth, liquid, CAD bracket,
@@ -195,7 +211,7 @@ camera clock for reuse. Supported GLB derivatives and calibrated cameras can
195
211
  cross between native and portable scenes; rigs, solvers, and procedural materials
196
212
  remain native. Blender and Python environments are installed separately, and
197
213
  source execution requires explicit current-user trust. See [Native film studio](docs/studio.md)
198
- and [Make an educational video](docs/how-to/educational-video.md).
214
+ and [Make a math explainer video with Manim](docs/how-to/educational-video.md).
199
215
 
200
216
  ### Build diagrams and motion graphics
201
217
 
@@ -269,12 +285,12 @@ slopcamera workflows list --json
269
285
  slopcamera workflows show social-variants --json
270
286
  ```
271
287
 
272
- ## How Slopcamera works
288
+ ## How SlopCamera works
273
289
 
274
290
  Keep the source that owns each creative decision. A native scene owns a rig or
275
291
  simulation; a portable scene owns supported geometry, cameras, and media surfaces;
276
292
  a diagram owns its objects and labels; a video project owns cuts and delivery.
277
- Slopcamera connects these through explicit assets and rendered derivatives.
293
+ SlopCamera connects these through explicit assets and rendered derivatives.
278
294
 
279
295
  1. **Prepare the sources.** Import footage and assets or author a scene, diagram,
280
296
  or native program. Inspect available tools before choosing an engine.
@@ -318,7 +334,7 @@ declarative graphs, approvals, and resuming work.
318
334
 
319
335
  ## Design and trust
320
336
 
321
- There is no Slopcamera account, hosted project database, or browser generation service.
337
+ There is no SlopCamera account, hosted project database, or browser generation service.
322
338
  Ordinary editing and rendering remain local. Gateway generation and selected
323
339
  cloud analysis use credentials from the local process and request explicit
324
340
  acknowledgement before uploading named media. The optional hosted API runs each
@@ -349,7 +365,7 @@ vision](https://algal.computer/docs/vision/) states the bet behind it.
349
365
  ## Documentation
350
366
 
351
367
  - **Learn:** [Your first diagram](docs/tutorials/first-diagram.md) · [Your first native film](docs/tutorials/first-native-film.md).
352
- - **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).
368
+ - **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).
353
369
  - **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).
354
370
  - **Understand the system:** [Architecture](docs/architecture.md) · [Native studio](docs/studio.md) · [Directed scenes](docs/spatial-scenes.md) · [Extension architecture](docs/extension-architecture.md).
355
371
 
@@ -413,4 +429,4 @@ an acknowledgment; the unacknowledged offer expires after ten minutes and does n
413
429
  consume the weekly shown cooldown. `support dismiss` opts out across the local suite;
414
430
  `support snooze` pauses for 30 days. `HRANESS_SUPPORT_AUDIENCE=off` disables ambient
415
431
  notices and claimed offers. Payment is an explicit human choice on the shared support
416
- page; Slopcamera has no product-updates mailing list or account-status lookup.
432
+ page; SlopCamera has no product-updates mailing list or account-status lookup.
@@ -1,6 +1,6 @@
1
- # Slopcamera local host and CLI
1
+ # SlopCamera local host and CLI
2
2
 
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.
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. `slopcamera status` and `slopcamera tui` show what is running, credits and the newest agent outputs in the terminal.
4
4
 
5
5
  ## Recording bundles
6
6
 
@@ -819,13 +819,6 @@ bun run test:html-overlay
819
819
  bun run benchmark:code-concurrency
820
820
  ```
821
821
 
822
- The optional Rust menu-bar companion lives under `desktop/` at the repository root and builds with Cargo:
823
-
824
- ```sh
825
- cargo build --manifest-path desktop/menubar/Cargo.toml
826
- slopcamera menubar
827
- ```
828
-
829
822
  `test:html-overlay` is portable and leaves the real-browser tests registered as
830
823
  skipped. `test:html-overlay:browser:macos` runs the real Chrome frame suite,
831
824
  including a declared PNG. `test:html-overlay:libraries:macos` additionally
@@ -847,4 +840,4 @@ The compiled smoke copies the executable away from the checkout and proves
847
840
  diagram initialization/rendering plus PNG-to-SVG vectorization without
848
841
  `node_modules`.
849
842
 
850
- Screen recording, microphone, camera, Input Monitoring, and Accessibility are separate macOS permissions. A missing optional source becomes a typed diagnostic; strict-input mode fails before recording begins. Slopcamera ships as a CLI with an unbundled menu-bar companion; it does not produce, install, sign, or notarize a desktop application.
843
+ Screen recording, microphone, camera, Input Monitoring, and Accessibility are separate macOS permissions. A missing optional source becomes a typed diagnostic; strict-input mode fails before recording begins. Slopcamera ships as a CLI; it does not produce, install, sign, or notarize a desktop application.
@@ -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.6.0" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.8.0" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -459,7 +459,7 @@ export async function withPreparedSpatialAssets<Result>(
459
459
  }
460
460
  const decoded = await raster(bytes, interpretation.alpha === "opaque");
461
461
  if (decoded.width !== interpretation.width || decoded.height !== interpretation.height) throw new RangeError("Image dimensions differ from its interpretation.");
462
- profiles.add("sdr-png-jpeg-sharp-0.35.3");
462
+ profiles.add("sdr-png-jpeg-sharp-0.35.4");
463
463
  return publishRaster(decoded);
464
464
  };
465
465
  const native = async (argv: readonly [string, ...string[]], maxOutputBytes = 16_384) => {
@@ -5,18 +5,20 @@ import { join } from "node:path";
5
5
  import type { CliCommand } from "./args";
6
6
 
7
7
  /**
8
- * The small status file the menu bar reads: what Slopcamera is doing now,
8
+ * The small status file `slopcamera status` and `slopcamera tui` read: what Slopcamera is doing now,
9
9
  * how the last render ended, and the last credits balance a command saw.
10
10
  * It holds plain labels, times and fixed error codes only — never paths,
11
11
  * prompts, tokens or account identifiers. Writes are best effort: a status
12
12
  * that can't be saved never fails the command.
13
13
  */
14
- export const MENUBAR_STATUS_FILE = "menubar-status.json";
14
+ // The on-disk name predates the menu bar's retirement and stays frozen, so a
15
+ // rolled-back release and existing state keep reading the same file.
16
+ export const ACTIVITY_STATUS_FILE = "menubar-status.json";
15
17
  const MAX_STATUS_BYTES = 16 * 1024;
16
18
 
17
19
  export type ActivityState = "running" | "done" | "failed";
18
20
 
19
- export interface MenubarActivity {
21
+ export interface RecordedActivity {
20
22
  readonly state: ActivityState;
21
23
  readonly label: string;
22
24
  readonly startedAt: number;
@@ -24,16 +26,16 @@ export interface MenubarActivity {
24
26
  readonly error?: string;
25
27
  }
26
28
 
27
- export interface MenubarCredits {
29
+ export interface RecordedCredits {
28
30
  readonly usd: string;
29
31
  readonly low: boolean;
30
32
  readonly checkedAt: number;
31
33
  }
32
34
 
33
- export interface MenubarStatus {
35
+ export interface ActivityStatus {
34
36
  readonly schemaVersion: 1;
35
- readonly activity?: MenubarActivity;
36
- readonly credits?: MenubarCredits;
37
+ readonly activity?: RecordedActivity;
38
+ readonly credits?: RecordedCredits;
37
39
  }
38
40
 
39
41
  /** Plain labels for the commands that make media. Everything else leaves the status alone. */
@@ -43,6 +45,7 @@ export function activityLabel(command: CliCommand): string | undefined {
43
45
  case "project-render": return command.action === "run" && !command.dryRun ? "Rendering a project" : undefined;
44
46
  case "project-cinema": return "Rendering a project";
45
47
  case "html-render": return "Rendering an HTML scene";
48
+ case "html-film": return command.action === "deliver" ? "Encoding a film" : "Rendering film stills";
46
49
  case "diagram-render": return "Rendering a diagram";
47
50
  case "image-vectorize": return "Vectorizing an image";
48
51
  case "ai-image-generate": return "Generating an image";
@@ -57,13 +60,13 @@ export function activityLabel(command: CliCommand): string | undefined {
57
60
  }
58
61
  }
59
62
 
60
- function parse(text: string): MenubarStatus {
63
+ function parse(text: string): ActivityStatus {
61
64
  try {
62
65
  const value: unknown = JSON.parse(text);
63
66
  if (typeof value !== "object" || value === null || (value as { schemaVersion?: unknown }).schemaVersion !== 1) return { schemaVersion: 1 };
64
67
  const record = value as Record<string, unknown>;
65
- const activity = record.activity as MenubarActivity | undefined;
66
- const credits = record.credits as MenubarCredits | undefined;
68
+ const activity = record.activity as RecordedActivity | undefined;
69
+ const credits = record.credits as RecordedCredits | undefined;
67
70
  return {
68
71
  schemaVersion: 1,
69
72
  ...(activity !== undefined && typeof activity === "object" && typeof activity.label === "string" ? { activity } : {}),
@@ -74,8 +77,8 @@ function parse(text: string): MenubarStatus {
74
77
  }
75
78
  }
76
79
 
77
- export function readMenubarStatus(stateRoot: string): MenubarStatus {
78
- const path = join(stateRoot, MENUBAR_STATUS_FILE);
80
+ export function readActivityStatus(stateRoot: string): ActivityStatus {
81
+ const path = join(stateRoot, ACTIVITY_STATUS_FILE);
79
82
  try {
80
83
  const info = lstatSync(path);
81
84
  if (!info.isFile() || info.size > MAX_STATUS_BYTES) return { schemaVersion: 1 };
@@ -85,7 +88,7 @@ export function readMenubarStatus(stateRoot: string): MenubarStatus {
85
88
  }
86
89
  }
87
90
 
88
- const LOCK_FILE = `.${MENUBAR_STATUS_FILE}.lock`;
91
+ const LOCK_FILE = `.${ACTIVITY_STATUS_FILE}.lock`;
89
92
  /** A held lock blocks a write for this long before it is skipped. */
90
93
  const LOCK_BUDGET_MS = 250;
91
94
  /** A lock older than this is a crashed writer's and is reclaimed. */
@@ -127,14 +130,14 @@ function withStatusLock<T>(stateRoot: string, update: () => T): T | undefined {
127
130
  }
128
131
 
129
132
  /** Replaces the status file atomically with an owner-only file. Never throws. */
130
- export function writeMenubarStatus(stateRoot: string, update: (current: MenubarStatus) => MenubarStatus): void {
133
+ export function writeActivityStatus(stateRoot: string, update: (current: ActivityStatus) => ActivityStatus): void {
131
134
  withStatusLock(stateRoot, () => {
132
- const path = join(stateRoot, MENUBAR_STATUS_FILE);
133
- const staged = join(stateRoot, `.${MENUBAR_STATUS_FILE}.${randomUUID()}.tmp`);
135
+ const path = join(stateRoot, ACTIVITY_STATUS_FILE);
136
+ const staged = join(stateRoot, `.${ACTIVITY_STATUS_FILE}.${randomUUID()}.tmp`);
134
137
  try {
135
138
  const root = lstatSync(stateRoot);
136
139
  if (!root.isDirectory()) return;
137
- const text = `${JSON.stringify(update(readMenubarStatus(stateRoot)))}\n`;
140
+ const text = `${JSON.stringify(update(readActivityStatus(stateRoot)))}\n`;
138
141
  if (Buffer.byteLength(text) > MAX_STATUS_BYTES) return;
139
142
  const fd = openSync(staged, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
140
143
  try { writeSync(fd, text); } finally { closeSync(fd); }
@@ -146,10 +149,10 @@ export function writeMenubarStatus(stateRoot: string, update: (current: MenubarS
146
149
  });
147
150
  }
148
151
 
149
- export function recordActivity(stateRoot: string, activity: MenubarActivity): void {
150
- writeMenubarStatus(stateRoot, (current) => ({ ...current, activity }));
152
+ export function recordActivity(stateRoot: string, activity: RecordedActivity): void {
153
+ writeActivityStatus(stateRoot, (current) => ({ ...current, activity }));
151
154
  }
152
155
 
153
- export function recordCredits(stateRoot: string, credits: MenubarCredits): void {
154
- writeMenubarStatus(stateRoot, (current) => ({ ...current, credits }));
156
+ export function recordCredits(stateRoot: string, credits: RecordedCredits): void {
157
+ writeActivityStatus(stateRoot, (current) => ({ ...current, credits }));
155
158
  }
@@ -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
@@ -333,7 +335,6 @@ export type CliCommand =
333
335
  readonly tint: number | undefined;
334
336
  readonly videoStreamIndex: number;
335
337
  } & JsonOption)
336
- | ({ readonly kind: "menubar"; readonly action: "run" | "install" | "uninstall" | "status"; readonly mode: "foreground" | "background" } & JsonOption)
337
338
  | ({ readonly kind: "outputs" } & JsonOption)
338
339
  | ({ readonly kind: "credits-status" } & JsonOption)
339
340
  | ({
@@ -731,6 +732,63 @@ function fail(message: string): never {
731
732
  throw new CliError("usage", message);
732
733
  }
733
734
 
735
+ const HTML_STILL_USAGE = "Use html still --input <scene.json> --at <seconds>[,<seconds>...] --output <dir> [--json].";
736
+ const HTML_PREVIEW_USAGE = "Use html preview --input <scene.json> --output <dir> [--every <seconds>] [--json].";
737
+ 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].";
738
+
739
+ function secondsList(value: string | undefined, usage: string): readonly number[] {
740
+ if (value === undefined) fail(usage);
741
+ return value.split(",").map(part => {
742
+ 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}.`);
743
+ return Number(part.trim());
744
+ });
745
+ }
746
+
747
+ function parseHtmlFilmArgs(action: "still" | "preview" | "deliver", argv: readonly string[]): HtmlFilmCommand {
748
+ if (action === "still") {
749
+ const parsed = parseOptions(argv, { "--input": "value", "--at": "value", "--output": "value", "--json": "flag" });
750
+ const input = optionString(parsed, "--input");
751
+ const output = optionString(parsed, "--output");
752
+ if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_STILL_USAGE);
753
+ return { kind: "html-film", action, input, output, at: secondsList(optionString(parsed, "--at"), HTML_STILL_USAGE), json: optionFlag(parsed, "--json") };
754
+ }
755
+ if (action === "preview") {
756
+ const parsed = parseOptions(argv, { "--input": "value", "--every": "value", "--output": "value", "--json": "flag" });
757
+ const input = optionString(parsed, "--input");
758
+ const output = optionString(parsed, "--output");
759
+ if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_PREVIEW_USAGE);
760
+ const every = optionString(parsed, "--every");
761
+ const [seconds] = every === undefined ? [2] : secondsList(every, HTML_PREVIEW_USAGE);
762
+ if (seconds === undefined || seconds <= 0) fail("--every must be more than 0 seconds.");
763
+ return { kind: "html-film", action, input, output, every: seconds, json: optionFlag(parsed, "--json") };
764
+ }
765
+ const parsed = parseOptions(argv, {
766
+ "--basename": "value", "--poster-at": "value", "--social-at": "value", "--cuts": "value",
767
+ "--per-beat-clips": "flag", "--beats": "value", "--output": "value", "--json": "flag",
768
+ });
769
+ const basename = optionString(parsed, "--basename");
770
+ if (parsed.positionals.length !== 1 || basename === undefined) fail(HTML_DELIVER_USAGE);
771
+ const single = (name: string) => {
772
+ const values = secondsList(optionString(parsed, name), HTML_DELIVER_USAGE);
773
+ if (values.length !== 1) fail(`${name} takes one time in seconds.`);
774
+ return values[0]!;
775
+ };
776
+ const cutsInput = optionString(parsed, "--cuts");
777
+ const cuts = cutsInput === undefined ? [] : cutsInput.split(",").map(cut => {
778
+ if (!(HTML_FILM_CUTS as readonly string[]).includes(cut)) fail(`--cuts takes ${HTML_FILM_CUTS.join(", ")}; got ${cut}.`);
779
+ return cut as HtmlFilmCut;
780
+ });
781
+ const beats = optionString(parsed, "--beats");
782
+ const output = optionString(parsed, "--output");
783
+ return {
784
+ kind: "html-film", action, exportPath: parsed.positionals[0]!, basename,
785
+ posterAt: single("--poster-at"), socialAt: single("--social-at"), cuts,
786
+ perBeatClips: optionFlag(parsed, "--per-beat-clips"),
787
+ ...(beats === undefined ? {} : { beats }), ...(output === undefined ? {} : { output }),
788
+ json: optionFlag(parsed, "--json"),
789
+ };
790
+ }
791
+
734
792
  function parseOptions(argv: readonly string[], spec: OptionSpec): ParsedOptions {
735
793
  const options: Record<string, OptionValue> = {};
736
794
  const positionals: string[] = [];
@@ -3631,6 +3689,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3631
3689
  const command = argv[0]!;
3632
3690
  switch (command) {
3633
3691
  case "html": {
3692
+ if (argv[1] === "still" || argv[1] === "preview" || argv[1] === "deliver") return parseHtmlFilmArgs(argv[1], argv.slice(2));
3634
3693
  if (argv[1] !== "render") fail("Use html render --input <scene.json> [--dry-run] [--json].");
3635
3694
  const parsed = parseOptions(argv.slice(2), { "--input": "value", "--dry-run": "flag", "--json": "flag" });
3636
3695
  const input = optionString(parsed, "--input");
@@ -3656,15 +3715,8 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3656
3715
  case "project": return parseProject(argv.slice(1));
3657
3716
  case "inspect": return parseInspect(argv.slice(1));
3658
3717
  case "events": return parseEvents(argv.slice(1));
3659
- case "menubar": {
3660
- const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--foreground": "flag", "--background": "flag" });
3661
- const positionals = parsed.positionals;
3662
- 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
- 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
- // `start` opens it now and returns, like --background; the login item's hints use it.
3665
- if (positionals[0] === "start") return { kind: "menubar", action: "run", mode: "background", json: optionFlag(parsed, "--json") };
3666
- return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
3667
- }
3718
+ // Retired with the menu-bar companion; the old command name explains where it went.
3719
+ case "menubar": throw new CliError("usage", "The menu bar was retired. Use slopcamera status or slopcamera tui to see what Slopcamera is doing, and slopcamera legacy retire to stop an old copy opening at login.");
3668
3720
  case "outputs": {
3669
3721
  const parsed = parseOptions(argv.slice(1), JSON_SPEC);
3670
3722
  exactPositionals(parsed, 0, "slopcamera outputs [--json]");
@@ -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"]);
@@ -261,7 +266,6 @@ export function commandHostResourceClaims(
261
266
  case "help":
262
267
  case "capabilities":
263
268
  case "inspect":
264
- case "menubar":
265
269
  case "operations-list":
266
270
  case "operations-show":
267
271
  case "outputs":
@@ -1,4 +1,4 @@
1
- import { activityLabel, recordActivity, recordCredits } from "./menubar-status";
1
+ import { activityLabel, recordActivity, recordCredits } from "./activity-status";
2
2
  import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
3
3
  import { SlopcameraCloudError } from "../../../src/cloud-errors";
4
4
  import {
@@ -188,8 +188,10 @@ import { commandHelp, completions } from "./help";
188
188
  import { createLocalSlopcameraCapabilityManifest } from "./capability-manifest";
189
189
  import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
190
190
  import { executeHtmlSceneCommand } from "./html-scene";
191
+ import { executeHtmlFilmCommand, formatHtmlFilmResult } from "./html-film";
191
192
  import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
192
- import { launchMenubar, manageMenubar, reportOutputsRoot } from "./menubar";
193
+ import { reportOutputsRoot } from "./outputs";
194
+ import { inspectLegacyLogin, retiredLegacyLogins } from "./legacy-login";
193
195
  import {
194
196
  codePreparationHostResourceClaims,
195
197
  combineHostResourceClaims,
@@ -363,7 +365,7 @@ import {
363
365
  workflowRunStore,
364
366
  } from "./workflow-runs";
365
367
 
366
- export const SLOPCAMERA_VERSION = "3.6.0";
368
+ export const SLOPCAMERA_VERSION = "3.8.0";
367
369
 
368
370
  // Legacy direct renders predate per-target output contracts. Keep them
369
371
  // bounded generously enough for long-form production while preventing one
@@ -7450,6 +7452,16 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
7450
7452
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
7451
7453
  return;
7452
7454
  }
7455
+ case "html-film": {
7456
+ const output = await executeHtmlFilmCommand(applicationContext(context), command,
7457
+ context.abortSignal ?? new AbortController().signal,
7458
+ { progress: stage => context.io.stderr(`HTML film: ${stage}\n`) });
7459
+ writeValue(context.io, command.json, output, () => formatHtmlFilmResult(output));
7460
+ if (output.kind === "slopcamera.html-film-delivery" && !output.withinBudget) {
7461
+ throw new CliError("invalid-data", "One or more delivered files are over budget.");
7462
+ }
7463
+ return;
7464
+ }
7453
7465
  case "studio": {
7454
7466
  const output = await executeStudioCommand(applicationContext(context), command, context.abortSignal ?? new AbortController().signal);
7455
7467
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
@@ -7973,6 +7985,10 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
7973
7985
  platform: { architecture: process.arch, macOSVersion: macVersion.stdout.trim(), name: context.io.platform },
7974
7986
  repositoryRoot: context.paths.repositoryRoot,
7975
7987
  tools: Object.fromEntries(capabilities.map((capability) => [capability.name, capability])),
7988
+ legacyLoginItem: {
7989
+ ...inspectLegacyLogin(context.io.env.HOME),
7990
+ retired: retiredLegacyLogins(context.io.env.HOME),
7991
+ },
7976
7992
  version: context.version,
7977
7993
  };
7978
7994
  writeValue(context.io, command.json, output, () => [
@@ -7981,6 +7997,12 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
7981
7997
  `platform ${output.platform.name} ${output.platform.macOSVersion}`,
7982
7998
  ...capabilities.map((item) => `${item.name} ${item.available ? item.version ?? item.command : "unavailable"}`),
7983
7999
  `emoji ${emoji.provenance} ${emoji.installedCount}/${emoji.catalogCount}; generate: ${emoji.generationCommand}`,
8000
+ output.legacyLoginItem.state === "found"
8001
+ ? "login the old menu bar still opens at login; run: slopcamera legacy retire"
8002
+ : output.legacyLoginItem.state === "not-ours"
8003
+ ? `login ${output.legacyLoginItem.label ?? "an item"} was changed outside Slopcamera and is left alone`
8004
+ : "login nothing starts at login",
8005
+ ...output.legacyLoginItem.retired.map((item) => `retired ${item.path}; restore: ${item.restore}`),
7984
8006
  ].join("\n"));
7985
8007
  return;
7986
8008
  }
@@ -8021,11 +8043,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
8021
8043
  case "credits-forget": await handleCreditsForget(context, command); return;
8022
8044
  case "media-audio": await handleMediaAudio(context, command); return;
8023
8045
  case "media-color": await handleMediaColor(context, command); return;
8024
- case "menubar": {
8025
- if (command.action === "run") await launchMenubar(context.io, context.paths.repositoryRoot, command.json, command.mode);
8026
- else await manageMenubar(context.io, context.paths.repositoryRoot, command.action, command.json);
8027
- return;
8028
- }
8029
8046
  case "outputs": {
8030
8047
  await reportOutputsRoot(context.io, context.stateRoot, command.json);
8031
8048
  return;
@@ -8269,6 +8286,7 @@ type MutationReference =
8269
8286
  function commandMutationReference(command: CliCommand): MutationReference | undefined {
8270
8287
  switch (command.kind) {
8271
8288
  case "html-render": return undefined; // A fresh retained attempt owns its publication lease.
8289
+ case "html-film": return undefined; // Fresh private job directories; outputs are plain caller files.
8272
8290
  case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
8273
8291
  case "directing": return undefined; // The directing store owns its explicit lease.
8274
8292
  case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
@@ -8353,7 +8371,6 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
8353
8371
  case "fillers-list":
8354
8372
  case "emoji-search":
8355
8373
  case "emoji-resolve":
8356
- case "menubar":
8357
8374
  case "outputs":
8358
8375
  case "complete": return undefined;
8359
8376
  // Fresh projects write outside an existing mutable bundle and publish by