@hraness/slopcamera 3.4.0 → 3.6.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 (45) hide show
  1. package/README.md +32 -19
  2. package/apps/desktop/application/operation.ts +1 -1
  3. package/apps/desktop/cli/args.ts +3 -1
  4. package/apps/desktop/cli/commands.ts +22 -1
  5. package/apps/desktop/cli/help.ts +37 -13
  6. package/apps/desktop/cli/menubar-status.ts +155 -0
  7. package/apps/desktop/cli/menubar.ts +129 -101
  8. package/apps/desktop/dist/cli/main.js +334 -312
  9. package/apps/desktop/html-overlay/catalog.ts +1 -0
  10. package/apps/desktop/html-overlay/spatial.ts +10 -6
  11. package/apps/desktop/workflows/index.ts +1 -0
  12. package/dist/cli.js +7 -8
  13. package/dist/generate.js +1 -1
  14. package/dist/{index-qtn3s421.js → index-4z9y2wa2.js} +1 -1
  15. package/dist/{index-sy1n4zfy.js → index-fevf4ppk.js} +1 -1
  16. package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
  17. package/dist/index-nxtmy18t.js +3 -0
  18. package/dist/{index-abnk4h9c.js → index-qxfexvj3.js} +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/operations.js +1 -1
  21. package/dist/vectorize/worker.js +1 -1
  22. package/dist/workflow.js +1 -1
  23. package/docs/README.md +3 -3
  24. package/examples/studio/vgpu/README.md +1 -1
  25. package/package.json +5 -6
  26. package/skills/slopcamera/SKILL.md +4 -3
  27. package/skills/slopcamera/references/brand-illustrations.md +237 -0
  28. package/skills/slopcamera/references/gateway-media.md +1 -1
  29. package/skills/slopcamera/references/install.md +10 -1
  30. package/skills/slopcamera/references/patent-drawings.md +4 -4
  31. package/skills/slopcamera/references/support.md +1 -1
  32. package/skills/slopcamera/references/vectorization.md +3 -1
  33. package/src/cli.ts +13 -10
  34. package/src/code/public-operations.ts +9 -0
  35. package/src/generate.ts +44 -1
  36. package/src/generation-pricing.ts +217 -0
  37. package/src/mcp/tools.ts +4 -0
  38. package/src/operations.ts +4 -0
  39. package/src/support.ts +7 -1
  40. package/src/vectorize/command.ts +6 -1
  41. package/src/vectorize/supervisor.ts +23 -3
  42. package/src/vectorize/tool.ts +8 -4
  43. package/src/version.ts +1 -1
  44. package/src/visual-style.ts +4 -1
  45. package/dist/index-77fjfg9f.js +0 -3
@@ -0,0 +1,237 @@
1
+ # Brand illustrations
2
+
3
+ Read this before you generate, trace, retouch, recolor, resize or ship any
4
+ product illustration or topic icon for a marketing page, docs landing, project
5
+ card or social card. A trace that "looks fine" at 448 px is not evidence. The
6
+ gate below is.
7
+
8
+ Scope: the **illustration** class of Slopcamera's two-class product artwork
9
+ (`slopcamera image icon --purpose illustration`), meaning marketing and topic
10
+ art shown at 24 to 160 px on Hraness sites. Marks (`--purpose mark`) keep their
11
+ own contract. Interface icons (nav, buttons, labels) stay on the stroke sprite
12
+ algal.computer uses (hugeicons, 24 viewBox, stroke 1.5, currentColor).
13
+
14
+ ### The icon language
15
+
16
+ Every illustration in the portfolio is one family. Product identity comes from
17
+ the subject and the palette primary the site applies. It does not come from
18
+ per-product line weight, per-product ink or a different drawing style.
19
+
20
+ | Property | Rule |
21
+ | --- | --- |
22
+ | Frame | `viewBox="0 0 64 64"`, square. The subject's long side is 48 to 54 units, optically centered. The 5-unit margin is empty. |
23
+ | Line | Monoline centerline strokes: `fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`. |
24
+ | Weights | Four tones and nothing else: **line** 2 u (the body of the drawing); **detail** 1.5 u (interior detail only, at most 25% of total line length); **accent** 3 u (at most one element, the focal point); **tint plane** `fill="currentColor" fill-opacity=".16"` (at most 2 paths, on the primary face only). No hairlines under 1.5 u. No solid fills except a tint plane. |
25
+ | Projection | Simple 30-degree isometric, or front orthographic for flat subjects such as a page, card or screen. Use one projection per site set. |
26
+ | Subject | One main object and at most one small secondary prop. At most 8 connected parts. No text, digits, letters, logos, hatching, texture, shading, gradients, shadows, ground planes, badges, frames or container tiles. |
27
+ | Spacing | Parallel strokes are at least 3 u apart (1.5 times the line). Closed counters are at least 6 u across. No stroke ends closer than 2 u to another stroke unless they join. |
28
+ | Color | `currentColor` only. No hex, rgb, named color, `<style>`, media query or `class` paint in the file. The site sets `color` (normally `var(--primary)`). |
29
+ | Markup | Only `<svg>`, `<g>`, `<path>`, `<circle>`, `<ellipse>`, `<rect>`, `<line>`, `<polyline>`. At most 24 elements and 400 path commands, 0.1-unit precision, 6 KB or less. No `mask`, `clipPath`, `filter`, `transform`, `vector-effect`, `use`, `image`, `style`, `script`, `href` or `url()`. No `data-slopcamera-line-weight`. |
30
+
31
+ #### Optical sizes
32
+
33
+ Stroke weight is fixed in frame units, so rendered weight scales with display
34
+ size. Three optical sizes keep the rendered line between about 1.2 and 3 CSS px,
35
+ roughly the square root of the size ratio to algal's 1.2 to 1.3 rem accent icons
36
+ (1.5/24 stroke gives 1.2 px at 19.2 px):
37
+
38
+ | Optical size | Display | Stroke | Detail length | Parts | Tint plane | File |
39
+ | --- | --- | --- | --- | --- | --- | --- |
40
+ | `s` | 24 to 40 px | 3 u (1.1 to 1.9 px) | 100 to 280 u | 4 or fewer | none | `<id>.s.svg` |
41
+ | `m` (default) | 44 to 120 px | 2 u (1.4 to 3.75 px) | 160 to 460 u | 8 or fewer | 1 or fewer | `<id>.svg` |
42
+ | `l` | 128 px and up | 1.5 u (3 px and up) | 200 to 640 u | 10 or fewer | 2 or fewer | `<id>.l.svg` |
43
+
44
+ Below 24 px, use the product **mark**, never an illustration. Derive `s` from
45
+ `m` by pruning, never by scaling: drop detail strokes, merge near-parallel
46
+ lines, then re-stroke at 3 u.
47
+
48
+ #### Delivery
49
+
50
+ Sites render illustrations so the palette reaches them. Never ship them through
51
+ a bare `<img>` with a baked color.
52
+
53
+ 1. **Preferred:** a generated per-site sprite, `/illustrations.svg`, with one
54
+ `<symbol id="<id>" viewBox="0 0 64 64">` per illustration, rendered as
55
+ `<svg class="hraness-illustration" data-size="m" aria-hidden="true"><use href="/illustrations.svg#<id>"/></svg>`.
56
+ `currentColor` inherits through `<use>`, the tint plane keeps its opacity,
57
+ and forced-colors mode works natively. This is the same sprite pattern
58
+ algal.computer uses for interface icons (`/icons.svg`).
59
+ 2. **Fallback** where only an image URL is possible, such as Markdown or a CMS:
60
+ the design-kit mask utility (`.hraness-illustration` with a painted
61
+ mask layer) paints `var(--hraness-illustration-ink, var(--primary))`
62
+ through the SVG's alpha.
63
+ It follows the same structure as `.hraness-foil-mark`.
64
+ 3. Size and color come from design-kit classes (`.hraness-illustration[data-size="s|m|l"]`).
65
+ Sites must not own a `.x-topic-icon { width: 88px … }` rule, an `opacity`,
66
+ a `filter: hue-rotate()` or an `invert()`.
67
+ 4. Contrast: `--primary` against the surface the illustration sits on must
68
+ reach at least 3:1 (WCAG 1.4.11, non-text) in both schemes. Where a card
69
+ surface is tinted, set `--hraness-illustration-ink` to the palette's
70
+ strong-primary token instead of lowering opacity.
71
+
72
+ ### Generate
73
+
74
+ ```sh
75
+ slopcamera image icon '<subject>' --purpose illustration \
76
+ --concept '<one-sentence metaphor>' --size m --rounds 2 --output icons/<id>.svg --json
77
+ slopcamera image icon normalize icons/<id>.svg --size s --output icons/<id>.s.svg --json
78
+ slopcamera image icon check icons/ --json # gate every file; non-zero exit on any failure
79
+ slopcamera image icon sheet icons/ --palette <family> --output review.png # 32/64/88 px, light+dark
80
+ ```
81
+
82
+ (`--concept`, `--size`, `normalize`, `check` and `sheet` are the proposed
83
+ additions. Today only `image icon` exists.)
84
+
85
+ 1. **Concept first.** Turn the product verb into one physical object metaphor,
86
+ in one sentence. Write three candidates. Generate a gallery
87
+ (`slopcamera image gallery`) of the best two before spending icon rounds.
88
+ Reject metaphors another portfolio product already owns (see the shared
89
+ pool in the registry) unless you deliberately reuse the pooled file.
90
+ 2. **Raster.** The prompt template below fixes ink, weight ratio, projection
91
+ and element budget. The model draws black on white. Color is never a
92
+ generation parameter.
93
+ 3. **Normalize** (deterministic, local, no network): centerline re-trace to
94
+ 2 u strokes, strip color to `currentColor`, reframe to 64, emit, then
95
+ re-measure. See "Normalization" below.
96
+ 4. **Gate.** Hard numeric gate (below). A failure feeds a concrete correction
97
+ into the next round ("remove the 6 shelf slats; keep 2"), not a restyle.
98
+ 5. **Critique.** The vision reviewer sees the **sheet**: 32, 64 and 88 px on
99
+ the target palette's light and dark surfaces, beside the site's other
100
+ illustrations, with the gate metrics printed underneath. A pass at 448 px
101
+ alone does not count.
102
+ 6. **Register.** Record the file in the artwork registry with subject,
103
+ concept, sha256, gate version and metrics. Sites consume registry output.
104
+ They do not keep hand-copied SVGs.
105
+
106
+ #### Prompt template
107
+
108
+ ```text
109
+ A single product-brand line illustration of {subject}, shown as {concept}.
110
+
111
+ Drawing rules:
112
+ - Pure black (#000000) lines on a plain white (#FFFFFF) background. No other color, no gray.
113
+ - Every line has the same thickness, about 3% of the image width (about 32 px on a 1024 px image).
114
+ No thin detail lines. No filled or solid areas{accent_clause}.
115
+ - Simple 30-degree isometric view of one main object{prop_clause}. At most {max_parts} separate parts.
116
+ Draw as few lines as you can. Every line must still be clearly visible when the image is 64 px wide.
117
+ - Closed shapes are outlines, not filled. Leave at least one line-width of white between parallel lines.
118
+ - Round line ends and rounded corners.
119
+ - No text, numbers, letters, logos, hatching, texture, shading, gradients, shadows, floor, frame,
120
+ badge, tile or background shape.
121
+ - The object fills about 80% of the image and is centered.
122
+ {feedback_clause}
123
+ ```
124
+
125
+ - `accent_clause`: empty by default. For a deliberate focal accent, use
126
+ `, except one small solid {part} no larger than a tenth of the drawing`.
127
+ The normalizer converts it to the tint plane.
128
+ - `prop_clause`: empty, or `, with one small {prop} beside it`.
129
+ - `max_parts`: 8 for `m`, 4 for `s`, 10 for `l`.
130
+ - `feedback_clause`: `The previous attempt was rejected. Change only this: {gate reason codes rendered as concrete edits}`.
131
+
132
+ Do not ask for brand color, "medium weight", "clean", "minimal" or "premium".
133
+ Those words do not constrain the raster. The numbers do.
134
+
135
+ #### Normalization
136
+
137
+ `slopcamera image icon normalize` makes weight and color properties of the
138
+ normalizer, not the image model:
139
+
140
+ 1. Parse the SVG and reject forbidden content (list in "The icon language").
141
+ 2. Rasterize its alpha on a **transparent** background, with the art's bounding
142
+ box fitted into 54 u inside a 64 u frame at 8 px/unit (512 by 512, pinned
143
+ librsvg/sharp).
144
+ 3. Binarize at alpha 128 or more. Compute the Euclidean distance transform and a
145
+ Zhang–Suen skeleton.
146
+ 4. Separate **masses** (ink more than 2 u from any background pixel, meaning a
147
+ region wider than 4 u). Keep at most 2 masses: trace their outer contours
148
+ with marching squares and emit them as tint planes
149
+ (`fill="currentColor" fill-opacity=".16"`). If masses exceed 12% of ink,
150
+ stop with `mass.excess`: the art is mark-weight and must be regenerated.
151
+ 5. Turn the skeleton into a graph (junction and end pixels are nodes). Prune
152
+ spurs shorter than 2 u. Merge parallel chains closer than 3 u. Remove
153
+ components smaller than 1.5 u².
154
+ 6. Trace each edge chain into a polyline, simplify it (Ramer–Douglas–Peucker
155
+ with epsilon 0.3 u), fit centripetal Catmull–Rom curves to cubic Béziers
156
+ with corner detection above 35 degrees, and join chains that meet at a node
157
+ of degree 2.
158
+ 7. Emit at most 24 paths on `viewBox="0 0 64 64"` with the root attributes
159
+ `fill="none" stroke="currentColor" stroke-width="{2|3|1.5}" stroke-linecap="round" stroke-linejoin="round"`,
160
+ rounded to 0.1 u. Chains whose source width was under 1.8 u become detail
161
+ strokes (1.5 u, `m` and `l` only). One chain marked by the concept as the
162
+ accent becomes 3 u.
163
+ 8. Re-render the emitted SVG and run the gate. Normalization never loosens a
164
+ threshold to pass.
165
+ 9. Write a receipt: source sha256, output sha256, gate version, every metric
166
+ and the pruning counts.
167
+
168
+ Sketch (TypeScript, Bun; reuses Slopcamera's pinned sharp and has no new
169
+ native dependency):
170
+
171
+ ```ts
172
+ export async function normalizeIllustration(svg: string, size: "s" | "m" | "l"): Promise<NormalizedIllustration> {
173
+ assertAllowedMarkup(svg) // forbidden tags/attrs/url()
174
+ const alpha = await renderAlphaFramed(svg, { frame: 64, art: 54, pxPerUnit: 8, background: "transparent" })
175
+ const ink = binarize(alpha, 128)
176
+ const dist = euclideanDistance(ink) // Felzenszwalb–Huttenlocher
177
+ const masses = regions(ink, (i) => dist[i] > 2 * 8)
178
+ if (share(masses, ink) > 0.12) throw gateError("mass.excess")
179
+ const graph = skeletonGraph(zhangSuen(ink))
180
+ prune(graph, { spurUnits: 2, mergeParallelUnits: 3, minComponentUnits2: 1.5 })
181
+ const chains = graph.chains().map((c) => fitCubic(rdp(c.points, 0.3 * 8), { cornerDeg: 35 }))
182
+ const out = emitSvg({
183
+ viewBox: "0 0 64 64",
184
+ stroke: { s: 3, m: 2, l: 1.5 }[size],
185
+ chains: classify(chains, dist), // line | detail | accent
186
+ tints: masses.slice(0, size === "s" ? 0 : size === "m" ? 1 : 2).map(marchingSquares),
187
+ precision: 0.1,
188
+ })
189
+ const metrics = await measureIllustration(out) // same code path as `icon check`
190
+ const problems = illustrationGate(metrics, size)
191
+ if (problems.length) throw gateError(problems)
192
+ return { svg: out, metrics, receipt: receiptFor(svg, out, metrics) }
193
+ }
194
+ ```
195
+
196
+ Re-stroking rescues drawings whose composition is sound but whose weight was
197
+ set by the image model. It cannot rescue mass art or over-detailed art; those
198
+ need regeneration from the prompt template with a smaller element budget.
199
+
200
+ #### Gate (version 1)
201
+
202
+ All metrics come from the framed render in normalization step 2, identical in
203
+ `image icon`, `icon check`, the jungle registry and CI.
204
+
205
+ | Code | Metric | Pass band (`m`) | `s` | `l` |
206
+ | --- | --- | --- | --- | --- |
207
+ | `weight.faint` / `weight.heavy` | median stroke width at skeleton points, (2·EDT − 1)/8 | 1.7 to 2.4 u | 2.6 to 3.4 u | 1.3 to 1.9 u |
208
+ | `weight.hairline` | share of skeleton under 1.0 u wide | 5% or less | 3% or less | 8% or less |
209
+ | `weight.mixed` | p90 stroke width while mass is above 2% | 3.0 u or less | 3.8 u or less | 3.0 u or less |
210
+ | `mass.excess` | ink more than 2 u from background, as share of ink | 12% or less | 0% | 12% or less |
211
+ | `coverage.sparse` / `coverage.dense` | Σα / frame area (transparent letterbox) | 0.09 to 0.22 | 0.08 to 0.20 | 0.07 to 0.22 |
212
+ | `detail.thin` / `detail.crowded` | skeleton length | 160 to 460 u | 100 to 280 u | 200 to 640 u |
213
+ | `parts.fragmented` | 8-connected components of 1 u² or more | 8 or fewer | 4 or fewer | 10 or fewer |
214
+ | `parts.specks` | components under 1.5 u² | 0 | 0 | 0 |
215
+ | `frame.aspect` | art bounding-box aspect | 1.6 or less | 1.4 or less | 1.8 or less |
216
+ | `svg.frame` | viewBox | `0 0 64 64` | same | same |
217
+ | `svg.color` | literal paint present, or `currentColor` absent | none / present | same | same |
218
+ | `svg.markup` | forbidden element or attribute | none | same | same |
219
+ | `svg.paths` / `svg.bytes` | element count / file size | 24 or fewer / 6 KB or less | 12 or fewer / 3 KB or less | 32 or fewer / 9 KB or less |
220
+
221
+ The bands hang together. At a 2 u line, 160 to 460 u of drawn length is 320 to
222
+ 920 u² of ink, which is 0.08 to 0.22 of the 4096 u² frame. Coverage and detail
223
+ length therefore agree unless the art contains masses or hairlines, and those
224
+ have their own codes.
225
+
226
+ #### Review checklist
227
+
228
+ Run on the sheet, not on a single large render:
229
+
230
+ - [ ] Recognizable at 32 px without its label, on both light and dark surfaces.
231
+ - [ ] Line weight matches its neighbors on the site sheet. No drawing reads fainter or bolder than the rest.
232
+ - [ ] No stroke, gap or counter is narrower than the line.
233
+ - [ ] At most one accent and one tint plane, both on the focal element.
234
+ - [ ] Renders in the site's `--primary` through the sprite or mask, not in `#2474d4`. Contrast is 3:1 or better on the actual card surface.
235
+ - [ ] The concept matches the copy of the section it heads, and differs from other products' concepts unless it is pooled.
236
+ - [ ] `s` exists wherever the site shows the illustration at 40 px or less.
237
+ - [ ] `icon check` passes and the registry receipt is updated. There is no hand-edited copy in any site.
@@ -136,7 +136,7 @@ Use [directing video](directing-video.md) for budgeted takes, accepted predecess
136
136
 
137
137
  The small portable `slopcamera image generate '<prompt>' --output image.webp` command defaults to `recraft/recraft-v4.1-utility` and admits PNG/JPEG/WebP output. It is distinct from the local `ai` catalog workflow. Preserve literal prompts when requested, keep its output inside the intended workspace and never automatically retry a failed paid call. Its sibling `slopcamera image gallery '<subject>' --output-dir <directory>` shares that bounded model contract and composes several candidates into one review sheet; see [image galleries](image-galleries.md).
138
138
 
139
- For product identity SVGs, `slopcamera image icon '<subject>' --purpose <mark|illustration> --output icon.svg` is the canned pipeline. `mark` produces a compact favicon/app/header mark: one to three bold masses in one ink, gated for 16–32 px recognition and low path count. `illustration` (the compatibility default) produces the related marketing illustration: simple isometric structure in one ink, gated against hairlines, dense fills, and detail lost at 64 px. Both run one style-locked Gateway raster through ink normalization, local tracing, and deterministic geometry gates. `--rounds 2` (the default) adds a purpose-specific vision critique that revises the prompt between attempts; `--rounds 1` is a single paid generation with no critique call. `--ink` overrides the ink color and `--keep-raster` retains the normalized line-art PNG beside the SVG.
139
+ For product identity SVGs, `slopcamera image icon '<subject>' --purpose <mark|illustration> --output icon.svg` is the canned pipeline. `mark` produces a compact favicon/app/header mark: one to three bold masses in one ink, gated for 16–32 px recognition and low path count. `illustration` (the compatibility default) produces the related marketing illustration: simple isometric structure in one ink, gated against hairlines, dense fills, and detail lost at 64 px. Both run one style-locked Gateway raster through ink normalization, local tracing, and deterministic geometry gates. `--rounds 2` (the default) adds a purpose-specific vision critique that revises the prompt between attempts; `--rounds 1` is a single paid generation with no critique call. `--ink` overrides the ink color and `--keep-raster` retains the normalized line-art PNG beside the SVG. For portfolio illustrations and topic icons, follow [brand illustrations](brand-illustrations.md): one shared line language, `currentColor` output, measured weight and density bands, and the site sheet review.
140
140
 
141
141
  ## Style recipes
142
142
 
@@ -14,7 +14,16 @@ command -v slopcamera
14
14
 
15
15
  A restricted shell can omit package-manager paths. Check known host installation paths before declaring a tool unavailable. If Bun is genuinely absent, follow its official [installation guide](https://bun.sh/docs/installation) within the user’s authorized setup scope. Do not switch package managers or pipe an unreviewed installer into a shell.
16
16
 
17
- Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.6/hraness-slopcamera-3.3.6.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
17
+ Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.5.0/hraness-slopcamera-3.5.0.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
18
+
19
+ If an update from a previous local archive reports `DependencyLoop`, use Bun's
20
+ [named tarball syntax](https://bun.com/docs/pm/cli/add#tarball-dependencies) with
21
+ the same verified archive:
22
+
23
+ ```sh
24
+ bun add --global @hraness/slopcamera@https://github.com/hraness/slopcamera/releases/download/v3.5.0/hraness-slopcamera-3.5.0.tgz
25
+ slopcamera --version
26
+ ```
18
27
 
19
28
  Use an existing compatible source build when available. Otherwise follow the [complete source-install guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/use-current-source.md): clone into a new directory, record its exact commit, install locked dependencies without lifecycle scripts, build the SDK and source CLI, then define the shell command against that checkout. A clone or skill installation alone does not install the executable. Do not alter an existing active checkout to satisfy this path.
20
29
 
@@ -1,10 +1,10 @@
1
1
  # Prepare patent-style drawing sheets
2
2
 
3
3
  Use `diagram sheets` for local monochrome vector drawing sheets with physical
4
- page dimensions. These commands require current source; do not assume a
5
- previously published archive includes them. Confirm `slopcamera help diagram`
6
- lists the commands. If absent, follow [installation](install.md) for an explicit
7
- source build rather than inventing a flag on ordinary diagram rendering.
4
+ page dimensions. The commands ship in Slopcamera v3.3.5 and later. Confirm
5
+ `slopcamera help diagram` lists them. If they are absent, the installed CLI is
6
+ older: follow [installation](install.md) to update it rather than inventing a
7
+ flag on ordinary diagram rendering.
8
8
 
9
9
  ## Preserve the authored content
10
10
 
@@ -6,7 +6,7 @@ At an eligible human closeout, call `slopcamera support offer --json` once. Trea
6
6
  around disabled state. Discovery on stderr is guidance, not an invitation claim.
7
7
 
8
8
  For `kind:offer`, optionally say once: “You can support continued development of
9
- Slopcamera’s local visual tools.” Use the returned clean support link. This product
9
+ Slopcamera’s local media tools.” Use the returned clean support link. This product
10
10
  has no updates mailing list; do not collect an email or imply a subscription unlocks
11
11
  features. Opening a link grants no authority to pay or create an account.
12
12
 
@@ -9,7 +9,9 @@ slopcamera image vectorize path/to/input.png --output path/to/input.svg --json
9
9
 
10
10
  - Do not provide a credential for vectorization. The conversion keeps image bytes local; the initial tool provisioning can download a verified VTracer release.
11
11
  - Do not redraw, relabel, crop, recolor, or simplify the subject beyond the
12
- trace mechanics the command measures.
12
+ trace mechanics the command measures. Brand illustrations are the exception:
13
+ they are normalized to the shared line language in
14
+ [brand illustrations](brand-illustrations.md), not preserved at fidelity.
13
15
  - Use `--duotone '#primary,#secondary'` only when the user explicitly asks for
14
16
  that two-color adaptation.
15
17
  - Preserve the emitted receipt in task output or an adjacent provenance record
package/src/cli.ts CHANGED
@@ -40,11 +40,11 @@ import { checkDrawingFile, renderDrawingFile, starterDrawingSource } from "./dra
40
40
  import { SLOPCAMERA_VERSION } from "./version.js"
41
41
  import { createVisualStyleDirection, getVisualStyleProfile, VISUAL_STYLE_PROFILES } from "./visual-style.js"
42
42
  import { reportUsefulResult, type UsefulResultObserver } from "./support-completion.js"
43
- import { runProductSupportCommand, showProductSupportInvitation, standaloneSupportEnvironment } from "./support.js"
43
+ import { runProductSupportCommand, showProductSupportInvitation, slopcameraSupportAdvancedHelp, slopcameraSupportHelpLine, standaloneSupportEnvironment } from "./support.js"
44
44
 
45
45
  export const slopcameraCliVersion = SLOPCAMERA_VERSION
46
46
 
47
- const help = `slopcamera ${slopcameraCliVersion}
47
+ const help = () => `slopcamera ${slopcameraCliVersion}
48
48
 
49
49
  Turn source material into deterministic diagrams, images, and canvas assets.
50
50
 
@@ -69,7 +69,6 @@ Usage:
69
69
  slopcamera code execute <operation> --input <JSON>
70
70
  slopcamera mcp --root <workspace>
71
71
  slopcamera doctor
72
- slopcamera support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
73
72
  slopcamera skill path
74
73
  slopcamera skill install [--target codex|claude|agents] [--scope user|project] [--force]
75
74
 
@@ -109,9 +108,9 @@ into one labelled contact sheet plus a receipt. Use it to review texture,
109
108
  skybox, backdrop, sprite, or design alternatives, then promote a chosen
110
109
  candidate file explicitly — nothing is applied automatically.
111
110
 
112
- Optional support: after useful work, agents can read slopcamera support protocol --json.
113
- Discovery uses stderr without claiming an invitation; HRANESS_SUPPORT_AUDIENCE=off disables it.
114
- No feature requires payment. Imported CLI/SDK calls and probes stay quiet.
111
+ ${slopcameraSupportHelpLine()}
112
+
113
+ Advanced verbs, including the support protocol, live under \`slopcamera help advanced\`.
115
114
 
116
115
  Code mode searches and executes a fixed semantic registry. Execute accepts
117
116
  typed JSON for one exact owned operation code; it never evaluates source text.
@@ -364,7 +363,7 @@ function canonicalArguments(args: readonly string[]): readonly string[] {
364
363
  surface === "vectorize" ||
365
364
  surface === "generate"
366
365
  ) {
367
- throw new Error(`The flat \`${surface}\` command moved to a namespaced Slopcamera surface.\n\n${help}`)
366
+ throw new Error(`The flat \`${surface}\` command moved to a namespaced Slopcamera surface.\n\n${help()}`)
368
367
  }
369
368
  return args
370
369
  }
@@ -408,8 +407,12 @@ export async function main(
408
407
  return
409
408
  }
410
409
  const [command, ...rest] = canonicalArguments(args)
411
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
412
- console.log(help)
410
+ if (command === undefined || command === "--help" || command === "-h") {
411
+ console.log(help())
412
+ return
413
+ }
414
+ if (command === "help") {
415
+ console.log(rest[0] === "advanced" ? slopcameraSupportAdvancedHelp() : help())
413
416
  return
414
417
  }
415
418
  if (command === "version" || command === "--version" || command === "-v") {
@@ -849,7 +852,7 @@ export async function main(
849
852
  throw new Error("Use slopcamera skill path or install")
850
853
  }
851
854
 
852
- throw new Error(`Unknown command: ${command}\n\n${help}`)
855
+ throw new Error(`Unknown command: ${command}\n\n${help()}`)
853
856
  }
854
857
 
855
858
  if (import.meta.main) {
@@ -304,6 +304,15 @@ export interface PortableSlopcameraOperationResultMap {
304
304
  readonly "slopcamera.image.vectorize": SlopcameraImageVectorizeOutput
305
305
  }
306
306
 
307
+ /**
308
+ * The typed portable projection: four of the six `slopcameraOperationCodes`.
309
+ * `slopcamera.image.icon` and `slopcamera.image.gallery` stay outside it on
310
+ * purpose. Each runs several paid Gateway calls and can publish more than one
311
+ * artifact, which the single-output, single-dispatch contract model here
312
+ * cannot express; they remain reachable through `executeSlopcameraOperation`,
313
+ * `execute_slopcamera`, and the CLI. Public copy cites this count, and
314
+ * `scripts/check-copy.ts` fails when the copy and this list disagree.
315
+ */
307
316
  export const PORTABLE_SLOPCAMERA_OPERATION_KINDS = Object.freeze([
308
317
  "slopcamera.diagram.check",
309
318
  "slopcamera.diagram.render",
package/src/generate.ts CHANGED
@@ -2,6 +2,13 @@ import { createHash, randomUUID } from "node:crypto"
2
2
  import { link, rm, writeFile } from "node:fs/promises"
3
3
  import { dirname, extname, resolve } from "node:path"
4
4
  import { SlopcameraCloudError } from "./cloud-errors.js"
5
+ import {
6
+ imageCallCosts,
7
+ type SlopcameraImageUsage,
8
+ type SlopcameraProviderCost,
9
+ } from "./generation-pricing.js"
10
+
11
+ export type { SlopcameraProviderCost } from "./generation-pricing.js"
5
12
 
6
13
  export const slopcameraGatewayApiBaseUrl =
7
14
  "https://ai-gateway.vercel.sh/v4/ai" as const
@@ -85,6 +92,12 @@ export interface SlopcameraGenerateDependencies {
85
92
  readonly fetch?: SlopcameraGatewayFetch
86
93
  readonly loadRuntime?: () => Promise<GatewayRuntime>
87
94
  readonly maximumResponseBytes?: number
95
+ /**
96
+ * Called once for every image call that reached the provider, including a
97
+ * call that failed afterwards, with that call's provider cost lines.
98
+ * Billing uses it to settle or release with the actual cost.
99
+ */
100
+ readonly onProviderCost?: (costs: readonly SlopcameraProviderCost[]) => void
88
101
  }
89
102
 
90
103
  const defaultGenerationTimeoutMs = 5 * 60_000
@@ -522,6 +535,15 @@ function warningReceipt(value: unknown): string {
522
535
  return `${type} sha256:${createHash("sha256").update(detail).digest("hex")}`
523
536
  }
524
537
 
538
+ function returnedUsage(value: unknown): SlopcameraImageUsage | undefined {
539
+ if (!isObject(value) || !isObject(value.usage)) return undefined
540
+ const { inputTokens, outputTokens } = value.usage
541
+ return {
542
+ inputTokens: typeof inputTokens === "number" ? inputTokens : undefined,
543
+ outputTokens: typeof outputTokens === "number" ? outputTokens : undefined,
544
+ }
545
+ }
546
+
525
547
  function parseResult(
526
548
  value: unknown,
527
549
  model: string,
@@ -590,6 +612,24 @@ async function performGeneration(
590
612
  const prompt = validatePrompt(input.prompt)
591
613
  const credential = resolveSlopcameraGatewayCredential(dependencies.environment)
592
614
  const timeout = combineSignals(input.signal, validateTimeout(input.timeoutMs))
615
+ // Cost accounting: once the provider call starts it may be billed, so every
616
+ // exit after that point reports a cost, from returned usage when there is
617
+ // one, otherwise the worst case. A 4xx Gateway answer is a refusal the
618
+ // provider does not bill; a 5xx may come after the provider already
619
+ // produced the image, so it still reports the worst case.
620
+ let dispatched = false
621
+ let rejected = false
622
+ let reported = false
623
+ const report = (usage: SlopcameraImageUsage | undefined): void => {
624
+ if (reported || !dispatched || rejected) return
625
+ reported = true
626
+ dependencies.onProviderCost?.(imageCallCosts(model, prompt, usage))
627
+ }
628
+ const observedFetch: SlopcameraGatewayFetch = async (request, init) => {
629
+ const response = await (dependencies.fetch ?? globalThis.fetch)(request, init)
630
+ if (response.status >= 400 && response.status < 500) rejected = true
631
+ return response
632
+ }
593
633
  try {
594
634
  const generation = (async () => {
595
635
  assertGenerationActive(timeout.signal)
@@ -602,13 +642,14 @@ async function performGeneration(
602
642
  apiKey: credential.token,
603
643
  baseURL: slopcameraGatewayApiBaseUrl,
604
644
  fetch: createFixedGatewayFetch({
605
- ...(dependencies.fetch === undefined ? {} : { fetch: dependencies.fetch }),
645
+ fetch: observedFetch,
606
646
  ...(dependencies.maximumResponseBytes === undefined
607
647
  ? {}
608
648
  : { maximumResponseBytes: dependencies.maximumResponseBytes }),
609
649
  }),
610
650
  })
611
651
  assertGenerationActive(timeout.signal)
652
+ dispatched = true
612
653
  const generated = await runtime.generateImage({
613
654
  abortSignal: timeout.signal,
614
655
  maxRetries: 0,
@@ -616,10 +657,12 @@ async function performGeneration(
616
657
  n: 1,
617
658
  prompt,
618
659
  })
660
+ report(returnedUsage(generated))
619
661
  return parseResult(generated, model)
620
662
  })()
621
663
  return await Promise.race([generation, timeout.interruption])
622
664
  } catch (error) {
665
+ report(undefined)
623
666
  if (error instanceof SlopcameraCloudError) throw error
624
667
  throw new SlopcameraCloudError(
625
668
  "GENERATION_FAILED",