@hraness/slopcamera 3.9.1 → 3.10.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.
package/README.md CHANGED
@@ -1,53 +1,44 @@
1
1
  # SlopCamera
2
2
 
3
- [![A brass optical instrument rendered in Blender from a SlopCamera native starter](apps/web/media/native-product-poster-51b1658c46d3.webp)](https://slopcamera.com/docs/tutorials/first-native-film#inspect-the-finished-example)
3
+ [![Rain, bottled: a cloud rains inside a glass vessel with brass fittings](apps/web/media/rain-bottled-poster-479a32b54a03.webp)](https://slopcamera.com/docs/how-to/remix-the-showcase#rain-bottled)
4
4
 
5
- **SlopCamera is a media studio for coding agents.** SlopCamera lets your coding
5
+ **A multimedia studio for your coding agent.** SlopCamera lets your coding
6
6
  agent make images, diagrams, animation, 3D scenes, and video from source files
7
- it can keep revising. It works with Codex, Claude Code, and other agents.
8
-
9
- A frontier model such as Claude Opus can write a video or a graphic from
10
- scratch in one pass. SlopCamera gives the agent packaged, tested techniques to
11
- reuse instead, for diagrams, motion graphics, 3D scenes, native Blender,
12
- CadQuery, and Manim films, and footage edits. The agent writes a short source
13
- file, and the SlopCamera CLI renders it, checks it, and writes the variants each
14
- technique supports, such as light and dark diagrams or 16:9 and 9:16 cuts of a
15
- video. The source stays beside the output, so a revision is an edit to that
16
- file and a new render.
17
-
18
- [Install](#install-slopcamera) · [First diagram](#make-your-first-diagram) · [Techniques](https://slopcamera.com/docs/reference/techniques) · [Docs](https://slopcamera.com/docs) · [Compare](#compared-with-other-tools) · [slopcamera.com](https://slopcamera.com)
19
-
20
- ## Why SlopCamera
21
-
22
- - **The agent writes the part that is specific to your request.** A diagram
23
- is a JSON file of boxes and arrows, a title animation is an HTML scaffold
24
- for one of seven profiles, and a 3D shot is scene JSON with a named camera.
25
- The renderer, frame timing, encoding, and export formats are already
26
- installed. This does not make it cheaper in tokens: in our
27
- [benchmark](https://slopcamera.com/docs/explanation/token-benchmark), the median cost with
28
- SlopCamera installed was higher than without it on all four tasks.
29
- - **Revisions change the source, not the whole result.** Change one label,
30
- camera, or cut and render again. Every export comes from the same source, so
31
- light and dark versions, or 16:9 and 9:16 cuts, stay in step.
32
- - **Checks run before the agent reports back.** A strict diagram checker,
33
- scene audits, render plans, and `doctor` catch problems the agent can
34
- fix before you look. They check structure, not taste; you still review the
35
- render.
36
- - **The techniques match the installed CLI.** The Bun CLI, a TypeScript SDK,
37
- and an Agent Skill ship at the same version. The skill sends the agent to
38
- the guide for its current job instead of loading every guide at once.
39
- - **Work stays on your machine.** Projects are local files and there is no
40
- SlopCamera account. Generation is optional: it uses your own Vercel AI
41
- Gateway access or, for prompt-only images, prepaid Hraness Credits.
7
+ it can keep revising. Use it with Codex, Claude Code, or another coding agent.
8
+ Direct the style, timing, camera, and sound, then refine the result.
9
+
10
+ Your agent writes the scene or edit as source. SlopCamera renders it with the
11
+ appropriate engine and keeps the inputs so the agent can change a shot,
12
+ rework a composition, or deliver another format. Combine authored scenes,
13
+ generated media, and your own footage in the same project.
14
+
15
+ [Watch the films](https://slopcamera.com/#examples) · [Install](#install-slopcamera) · [Direct a film](https://slopcamera.com/docs/how-to/direct-a-film) · [Docs](https://slopcamera.com/docs) · [GitHub examples](examples/showcase)
16
+
17
+ ## What you can make
18
+
19
+ - **Films and animation.** Author motion graphics, illustrated scenes, and
20
+ native Blender shots. Control camera, materials, lighting, and timing.
21
+ - **Visual explanations.** Make diagrams and educational films with editable
22
+ labels, mathematics, narration, and graphics.
23
+ - **Images and designs.** Create illustrations, trace raster artwork to SVG,
24
+ or revise a parametric architectural model.
25
+ - **Edits of your footage.** Compose shots, sound, overlays, and captions,
26
+ then export horizontal, vertical, square, or portrait versions.
27
+
28
+ The [techniques catalog](https://slopcamera.com/docs/reference/techniques)
29
+ connects each job to its supported commands and requirements. SlopCamera is
30
+ free and open source. Local editing and rendering need no SlopCamera account.
31
+ Generation uses your own Vercel AI Gateway account, or prepaid Hraness Credits
32
+ for prompt-only hosted images; model usage is billed separately.
42
33
 
43
34
  ## Install SlopCamera
44
35
 
45
36
  Install [Bun 1.3.14 or newer](https://bun.sh), then install the
46
- [SlopCamera v3.9.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.9.0)
37
+ [SlopCamera v3.9.2 release](https://github.com/hraness/slopcamera/releases/tag/v3.9.2)
47
38
  and its matching Agent Skill:
48
39
 
49
40
  ```sh
50
- bun add --global https://github.com/hraness/slopcamera/releases/download/v3.9.0/hraness-slopcamera-3.9.0.tgz
41
+ bun add --global https://github.com/hraness/slopcamera/releases/download/v3.9.2/hraness-slopcamera-3.9.2.tgz
51
42
  slopcamera skill install --target agents
52
43
  ```
53
44
 
@@ -56,8 +47,10 @@ The `agents` target works for agents that read `~/.agents/skills`. Use
56
47
  inside a repository for a project-only install. Start a new
57
48
  agent session so it loads the skill, then ask for a result:
58
49
 
59
- > Create a two-box diagram showing ingestion flowing into review, render it,
60
- > and show me the dark PNG.
50
+ > Make a short illustrated film of a late-night tram whose destination
51
+ > changes to the moon. Give it warm windows, a rainy city, and a quiet ending.
52
+ > Start with a frame and a motion test so we can direct it before the final
53
+ > render. Keep the source and sound editable.
61
54
 
62
55
  Setup guides: [Claude Code](https://slopcamera.com/docs/tutorials/claude-code) ·
63
56
  [Codex](https://slopcamera.com/docs/tutorials/codex) ·
@@ -67,16 +60,13 @@ Blender install separately; the
67
60
  [capability reference](docs/reference/capabilities.md) lists what each
68
61
  technique needs.
69
62
 
70
- From 3.8.0, SlopCamera is a CLI with no menu-bar app or background process.
71
- `slopcamera status` shows what is running, how the last job ended, your last
72
- known credits balance, the newest outputs and whether the retired menu-bar
73
- companion still opens at login. `slopcamera tui` keeps
74
- that screen open and refreshes it; `slopcamera tui --snapshot` prints it once
75
- and `--json` returns the data. `slopcamera outputs list|open|reveal` lists the
76
- newest outputs or opens one on this Mac, `slopcamera commands --json` lists
77
- every command with what it may change, and `slopcamera legacy retire` stops
78
- the retired menu-bar companion opening at login by moving its LaunchAgent aside; it
79
- deletes nothing, and `slopcamera doctor` prints the command that restores it.
63
+ On macOS, start with [your first animation](https://slopcamera.com/docs/tutorials/first-animation)
64
+ for a supplied scene you can render and revise without a model account. It
65
+ needs the browser and FFmpeg runtimes reported by `slopcamera doctor --json`.
66
+ For a first result on macOS, Linux, or Windows with no browser or paid model,
67
+ follow [your first diagram](https://slopcamera.com/docs/tutorials/first-diagram).
68
+ The [directing guide](docs/how-to/direct-a-film.md) explains how to ask for useful
69
+ changes to composition, timing, camera, and sound.
80
70
 
81
71
  <details>
82
72
  <summary>Build from source</summary>
@@ -110,70 +100,32 @@ placement and resuming long runs.
110
100
 
111
101
  </details>
112
102
 
113
- ## Make your first diagram
114
-
115
- This local task needs no model account. In a new directory, create the
116
- included diagram, check it, and render it:
103
+ CLI updates in SlopCamera 3.10.0 and newer are enabled by default for supported
104
+ Bun and npm global installations. See [update controls](docs/reference/capabilities.md#cli-updates)
105
+ to keep a version or check for a release.
117
106
 
118
- ```sh
119
- mkdir slopcamera-first
120
- cd slopcamera-first
121
- slopcamera diagram init first.diagram.json
122
- slopcamera diagram check first.diagram.json --strict
123
- slopcamera diagram render first.diagram.json
124
- ```
107
+ ## Make a film. Then direct it again.
125
108
 
126
- You now have `example-flow.tldr`, `example-flow.light.svg`,
127
- `example-flow.dark.svg`, `example-flow.light.png`, and
128
- `example-flow.dark.png`. The JSON stays editable, and the `.tldr` file opens in
129
- tldraw. Rendering again replaces those five files.
130
-
131
- <picture>
132
- <source media="(prefers-color-scheme: dark)" srcset="apps/web/media/source-to-film-dark-png-b8db55ec6e42.png">
133
- <img alt="Diagram export, rendered scene, and footage point into Project, which points to Preview and Delivery." src="apps/web/media/source-to-film-light-png-c3c24c460490.png">
134
- </picture>
135
-
136
- The diagram above was rendered from one
137
- [JSON file](examples/showcase/diagram/source-to-film.diagram.json). Each box in
138
- it is a short object the agent writes:
139
-
140
- ```json
141
- {
142
- "id": "diagram",
143
- "type": "rect",
144
- "x": 100,
145
- "y": 140,
146
- "width": 320,
147
- "height": 140,
148
- "radius": 24,
149
- "label": "Diagram export",
150
- "labelFontSize": 36,
151
- "labelWeight": 500,
152
- "tone": "blue"
153
- }
154
- ```
109
+ [Last tram to the moon](https://slopcamera.com/docs/tutorials/first-animation)
110
+ is a complete first project: original illustrated artwork, a twelve-second
111
+ journey, and a visible revision. The tutorial renders locally without a model
112
+ account or downloaded artwork.
155
113
 
156
- To revise it, the agent changed one label, and the file name for the new
157
- outputs, in a
158
- [copy of that file](examples/showcase/diagram/source-to-film-revised.diagram.json):
114
+ The two supplied render requests use the same scene. The first sets the city
115
+ and departure; the second enlarges the moon while keeping the journey, palette,
116
+ and timing. Ask your agent:
159
117
 
160
- ```diff
161
- - "name": "source-to-film",
162
- + "name": "source-to-film-revised",
163
- ...
164
- - "label": "Delivery",
165
- + "label": "Social delivery",
166
- ```
118
+ > Make the moon dominate the destination. Preserve the copper tram and rainy
119
+ > city. Keep both versions so I can compare their endings.
167
120
 
168
- ```sh
169
- slopcamera diagram render source-to-film-revised.diagram.json
170
- ```
121
+ [Follow the animation tutorial](https://slopcamera.com/docs/tutorials/first-animation),
122
+ then [remix the showcase](https://slopcamera.com/docs/how-to/remix-the-showcase):
123
+ a bottled storm, a paper ocean, a dancing laundromat, a band of harmonics,
124
+ and three edits of the same eclipse footage. Each piece has source,
125
+ production instructions, and a suggested creative revision.
171
126
 
172
- That two-line change produced a new `.tldr` file, both SVGs, and both PNGs,
173
- including the
174
- [revised light PNG](apps/web/media/source-to-film-revised-light-png-9ad9767c2bdf.png).
175
- [Your first diagram](https://slopcamera.com/docs/tutorials/first-diagram) walks
176
- through the same edit.
127
+ For a smaller first task, [make a diagram](https://slopcamera.com/docs/tutorials/first-diagram)
128
+ and export its editable tldraw document, light and dark SVGs, and PNGs.
177
129
 
178
130
  ## What SlopCamera does
179
131
 
@@ -210,10 +162,10 @@ characters, cloth, and liquid caches; CadQuery for parametric solids and STEP;
210
162
  and Manim Community for mathematical animation. Seven editable starters
211
163
  include a product, character, shaded street, cloth, liquid, CAD bracket, and
212
164
  educational presenter. Watch the
213
- [six-second product film](https://slopcamera.com/docs/tutorials/first-native-film),
214
- then change its [geometry, materials, and camera](examples/showcase/native/product).
165
+ [Rain, bottled](https://slopcamera.com/docs/how-to/remix-the-showcase#rain-bottled),
166
+ then change its [weather, materials, and camera](examples/showcase/studio-relaunch/rain-bottled).
215
167
  For 2D motion, try the
216
- [editable editorial animation](https://slopcamera.com/docs/tutorials/first-animation).
168
+ [illustrated tram film](https://slopcamera.com/docs/tutorials/first-animation).
217
169
  The [portrait geometry lesson](https://slopcamera.com/docs/how-to/educational-video)
218
170
  adds mathematical typesetting, a presenter, and captions.
219
171
 
@@ -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.9.1" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.10.0" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -365,7 +365,7 @@ import {
365
365
  workflowRunStore,
366
366
  } from "./workflow-runs";
367
367
 
368
- export const SLOPCAMERA_VERSION = "3.9.1";
368
+ export const SLOPCAMERA_VERSION = "3.10.0";
369
369
 
370
370
  // Legacy direct renders predate per-target output contracts. Keep them
371
371
  // bounded generously enough for long-form production while preventing one
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bun
2
+ import { fileURLToPath } from "node:url";
3
+ import type { CliUpdateOptions, StartupResult } from "@hraness/cli-update";
4
+ import { SLOPCAMERA_VERSION } from "../../../src/version";
5
+ import { slopcameraUpdatePolicy } from "./update-policy";
6
+
7
+ type EntryPorts = {
8
+ update(options: CliUpdateOptions): Promise<StartupResult>;
9
+ main(): Promise<void>;
10
+ };
11
+
12
+ export async function runSlopcameraEntrypoint(argv = process.argv.slice(2), ports: Partial<EntryPorts> = {}): Promise<void> {
13
+ const main = ports.main ?? (async () => (await import("./main")).runMainEntrypoint({}, true));
14
+ // Compiled private binaries and their embedded workers keep the native installer.
15
+ if (import.meta.path.includes("$bunfs")) { await main(); return; }
16
+ // The product renders its own command help; the updater sees an inert help request.
17
+ const gateArgv = argv[0] === "update" && argv.slice(1).some(arg => arg === "--help" || arg === "-h")
18
+ ? ["help", "update"] : argv;
19
+ const update = await (ports.update ?? (async options => (await import("@hraness/cli-update")).runCliUpdate(options)))({
20
+ packageName: "@hraness/slopcamera", version: SLOPCAMERA_VERSION, binName: "slopcamera",
21
+ entrypoint: fileURLToPath(import.meta.url), argv: gateArgv,
22
+ provider: { kind: "github", repository: "hraness/slopcamera", assetName: "hraness-slopcamera-{version}.tgz" },
23
+ ...slopcameraUpdatePolicy(gateArgv),
24
+ verifyArtifact: async artifact => (await import("./update-verification")).verifySlopcameraUpdate(artifact),
25
+ });
26
+ if (update.handled) { process.exitCode = update.exitCode; return; }
27
+ try { await main(); }
28
+ finally { await update.release(); }
29
+ }
30
+
31
+ if (import.meta.main) await runSlopcameraEntrypoint();
@@ -33,6 +33,7 @@ Commands:
33
33
  runs list|show|resume|approve|cancel
34
34
  Inspect and control durable workflow runs
35
35
  doctor Check local render and asset capabilities
36
+ update Update the CLI or manage automatic updates
36
37
  ai models|image|video|speech|transcribe
37
38
  Discover and run Vercel AI Gateway media models
38
39
  credits status|topup|wait|forget
@@ -719,6 +720,13 @@ compiles a bounded set of single-axis candidate plans under cinema/galleries/, e
719
720
  re-hashed through the canonical composition digest; it never selects a candidate.
720
721
 
721
722
  Imported media starts unverified. Align its audio before relying on synchronization. Structural edits are project-time operations and affect every placement. Camera moves address any placed video stream; metadata-driven screen zooms still require a recording-backed source placement. Face analysis and geometry tracks remain local. --select largest follows the largest currently visible prepared-layer face per frame. --require-all-selected makes a missing explicit/all-selected face invoke the chosen gap policy.`,
723
+ update: `Usage: slopcamera update [check|status|enable|disable] [--json]
724
+
725
+ Supported Bun and npm global installations on macOS and Linux check the immutable
726
+ GitHub release before work starts, at most once a day. Verification needs GitHub CLI.
727
+ Use slopcamera update disable to keep this version, or HRANESS_NO_UPDATE=1 for one
728
+ invocation. Exact Bun version pins require slopcamera update enable. Local image
729
+ vectorization, SDK imports, and the private compiled binary do not update automatically.`,
722
730
  render: `Usage: slopcamera render <plan|run> <recording> [--display <id|primary>] [--output <path>] [--dry-run] [--keep-inactivity] [--json]
723
731
 
724
732
  Long inactivity is analyzed and removed by default; use --keep-inactivity to opt out.`,
@@ -731,10 +739,11 @@ export function commandHelp(topic: readonly string[]): string {
731
739
 
732
740
  export function completions(words: readonly string[]): readonly string[] {
733
741
  const topLevel = [
734
- "capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "support", "outputs", "status", "tui", "commands", "legacy", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
742
+ "update", "capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "support", "outputs", "status", "tui", "commands", "legacy", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
735
743
  ];
736
744
  if (words.length <= 1) return topLevel;
737
745
  const command = words[0];
746
+ if (command === "update") return ["check", "status", "enable", "disable"];
738
747
  if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
739
748
  if (command === "html") return ["catalog", "scaffold", "init", "render", "still", "preview", "deliver"];
740
749
  if (command === "style") return ["list", "show"];
@@ -0,0 +1,18 @@
1
+ export function slopcameraUpdatePolicy(argv: readonly string[]): { effectFree: boolean; offline: boolean } {
2
+ const [command, action] = argv;
3
+ const portable = ["diagram", "mcp", "canvas", "skill"].includes(command ?? "")
4
+ || (command === "style" && !argv.includes("--help") && !argv.includes("-h"))
5
+ || (command === "code" && ["search", "execute"].includes(action ?? ""))
6
+ || (command === "media" && action === "soundtrack")
7
+ || (command === "image" && ["vectorize", "icon", "gallery"].includes(action ?? ""))
8
+ || (command === "image" && action === "generate" && argv.includes("--output"));
9
+ const registry = ["status", "tui", "commands", "outputs", "legacy", "support"].includes(command ?? "");
10
+ return {
11
+ effectFree: command === undefined || ["help", "--help", "-h", "version", "--version", "__complete", "capabilities"].includes(command)
12
+ || (command === "diagram" && ["help", "--help", "-h"].includes(action ?? ""))
13
+ || (!portable && !registry && (argv.includes("--help") || argv.includes("-h"))),
14
+ offline: command === "diagram"
15
+ || (command === "image" && ["vectorize", "icon"].includes(action ?? ""))
16
+ || (command === "media" && action === "soundtrack"),
17
+ };
18
+ }
@@ -0,0 +1,76 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const repository = "hraness/slopcamera";
8
+ const maximumMetadataBytes = 2 * 1024 * 1024;
9
+ type Gh = (args: readonly string[]) => Promise<Buffer>;
10
+
11
+ const runGh: Gh = args => new Promise((resolve, reject) => {
12
+ execFile("gh", [...args], {
13
+ encoding: "buffer", timeout: 30_000, maxBuffer: maximumMetadataBytes,
14
+ env: { ...process.env, GH_HOST: "github.com", GH_PROMPT_DISABLED: "1", GH_PAGER: "cat" },
15
+ }, (error, stdout) => {
16
+ // gh's error can include authenticated response bodies. Keep diagnostics fixed.
17
+ if (error !== null) reject(new Error("SlopCamera release verification failed. Check GitHub CLI access and retry slopcamera update."));
18
+ else resolve(stdout);
19
+ });
20
+ });
21
+
22
+ function record(value: unknown): Record<string, unknown> {
23
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Invalid SlopCamera release metadata.");
24
+ return value as Record<string, unknown>;
25
+ }
26
+
27
+ /** Keep the install guide's immutable-release and source-bound attestation checks. */
28
+ export async function verifySlopcameraUpdate(
29
+ artifact: { path: string; version: string; sha256: string },
30
+ gh: Gh = runGh,
31
+ ): Promise<void> {
32
+ if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u.test(artifact.version)
33
+ || artifact.version.length > 50 || !/^[a-f0-9]{64}$/u.test(artifact.sha256)) throw new Error("Invalid SlopCamera update identity.");
34
+ const tag = `v${artifact.version}`;
35
+ const json = async (path: string) => {
36
+ const bytes = await gh(["api", path]);
37
+ if (bytes.length > maximumMetadataBytes) throw new Error("SlopCamera release metadata is too large.");
38
+ return record(JSON.parse(bytes.toString("utf8")) as unknown);
39
+ };
40
+ await gh(["release", "verify", tag, "--repo", repository]);
41
+ await gh(["release", "verify-asset", tag, artifact.path, "--repo", repository]);
42
+ const reference = record((await json(`repos/${repository}/git/ref/tags/${tag}`)).object);
43
+ if (reference.type !== "tag" || typeof reference.sha !== "string" || !/^[a-f0-9]{40}$/u.test(reference.sha)) {
44
+ throw new Error("SlopCamera updates require an annotated release tag.");
45
+ }
46
+ const annotated = await json(`repos/${repository}/git/tags/${reference.sha}`);
47
+ const source = record(annotated.object);
48
+ if (annotated.tag !== tag || source.type !== "commit" || typeof source.sha !== "string" || !/^[a-f0-9]{40}$/u.test(source.sha)) {
49
+ throw new Error("SlopCamera release tag does not identify its source commit.");
50
+ }
51
+ const release = await json(`repos/${repository}/releases/tags/${tag}`);
52
+ if (release.tag_name !== tag || release.immutable !== true || !Array.isArray(release.assets)) {
53
+ throw new Error("SlopCamera release is not immutable.");
54
+ }
55
+ const bundles = release.assets.map(record).filter(asset => asset.name === "provenance.jsonl");
56
+ const bundle = bundles[0];
57
+ if (bundles.length !== 1 || bundle === undefined || bundle.state !== "uploaded"
58
+ || typeof bundle.id !== "number" || !Number.isSafeInteger(bundle.id) || bundle.id <= 0
59
+ || typeof bundle.size !== "number" || !Number.isSafeInteger(bundle.size) || bundle.size < 1 || bundle.size > maximumMetadataBytes
60
+ || typeof bundle.digest !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(bundle.digest)) {
61
+ throw new Error("SlopCamera release lacks a checked provenance bundle.");
62
+ }
63
+ const bytes = await gh(["api", `repos/${repository}/releases/assets/${bundle.id}`, "--header", "Accept: application/octet-stream"]);
64
+ if (bytes.length !== bundle.size || `sha256:${createHash("sha256").update(bytes).digest("hex")}` !== bundle.digest) {
65
+ throw new Error("SlopCamera provenance bundle differs from the immutable release.");
66
+ }
67
+ const directory = await mkdtemp(join(tmpdir(), "slopcamera-update-verification-"));
68
+ try {
69
+ const path = join(directory, "provenance.jsonl");
70
+ await writeFile(path, bytes, { mode: 0o600, flag: "wx" });
71
+ await gh(["attestation", "verify", artifact.path, "--repo", repository,
72
+ "--signer-workflow", `${repository}/.github/workflows/release.yml`,
73
+ "--signer-digest", source.sha, "--source-digest", source.sha,
74
+ "--source-ref", `refs/tags/${tag}`, "--deny-self-hosted-runners", "--bundle", path]);
75
+ } finally { await rm(directory, { recursive: true, force: true }); }
76
+ }
@@ -21,6 +21,11 @@ export interface VerbRow {
21
21
  const row = (words: string, opClass: OpClass, summary: string): VerbRow => ({ path: words.split(" "), opClass, summary });
22
22
 
23
23
  export const CLI_VERBS: readonly VerbRow[] = [
24
+ row("update", "operate", "Install a verified newer CLI release"),
25
+ row("update check", "operate", "Check release availability and save the check time"),
26
+ row("update status", "read", "Show installation support and update preferences"),
27
+ row("update enable", "operate", "Enable automatic CLI updates"),
28
+ row("update disable", "operate", "Keep the installed CLI version"),
24
29
  row("capabilities", "read", "List what this build can do and how well each part is tested"),
25
30
  row("doctor", "read", "Check local render tools, asset caches and old login items"),
26
31
  row("operations list", "read", "List the operations agents can run and their limits"),