@hraness/slopcamera 3.2.7 → 3.3.1

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 (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. package/src/desktop.ts +0 -253
package/src/cli.ts CHANGED
@@ -1,18 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { writeFile } from "node:fs/promises"
3
+ import { readFile, writeFile } from "node:fs/promises"
4
4
  import { resolve } from "node:path"
5
- import { createInterface } from "node:readline/promises"
6
5
  import {
7
6
  artifactSummary,
8
7
  checkDiagramFile,
9
- desktopStatus,
10
- getLatestDesktopRelease,
11
- installDesktop,
12
- openInDesktop,
13
8
  renderDiagramFile,
14
9
  runMcpServer,
15
- selectDesktopAsset,
16
10
  vectorizeImage,
17
11
  } from "./index.js"
18
12
  import {
@@ -21,6 +15,17 @@ import {
21
15
  slopcameraImageModels,
22
16
  type SlopcameraImageModel,
23
17
  } from "./generate.js"
18
+ import {
19
+ generateSlopcameraIcon,
20
+ slopcameraIconMaximumRounds,
21
+ } from "./icon.js"
22
+ import {
23
+ generateSlopcameraImageGallery,
24
+ parseSlopcameraGalleryVary,
25
+ slopcameraGalleryKinds,
26
+ slopcameraGalleryLimits,
27
+ type SlopcameraGalleryAxisSpec,
28
+ } from "./image-gallery.js"
24
29
  import {
25
30
  executeSlopcameraOperation,
26
31
  slopcameraOperationCodes,
@@ -32,6 +37,8 @@ import type { HostResourceCoordinator } from "./host-resources.js"
32
37
  import { installSkill, type SkillScope, type SkillTarget } from "./skill-install.js"
33
38
  import { pathExists } from "./fs.js"
34
39
  import { SLOPCAMERA_VERSION } from "./version.js"
40
+ import { reportUsefulResult, type UsefulResultObserver } from "./support-completion.js"
41
+ import { runProductSupportCommand, showProductSupportInvitation, standaloneSupportEnvironment } from "./support.js"
35
42
 
36
43
  export const slopcameraCliVersion = SLOPCAMERA_VERSION
37
44
 
@@ -45,14 +52,17 @@ Usage:
45
52
  slopcamera diagram render <file> [--out-dir <directory>] [--config <file>] [--scale <number>]
46
53
  slopcamera image vectorize <image> --output <file.svg> [--json] [--duotone <#rgb,#rgb>]
47
54
  slopcamera image generate <prompt> --output <file.png|jpg|webp> [--model <provider/model>] [--json]
55
+ slopcamera image icon <subject> --output <file.svg> [--purpose <mark|illustration>]
56
+ [--model <provider/model>] [--ink <#rgb|#rrggbb>] [--rounds <1-${slopcameraIconMaximumRounds}>]
57
+ [--critique-model <provider/model>] [--keep-raster] [--json]
58
+ slopcamera image gallery <subject> --output-dir <directory> [--kind <${slopcameraGalleryKinds.join("|")}>]
59
+ [--count <1-${slopcameraGalleryLimits.candidates}>] [--vary <axis[=v1,v2][;axis...]>] [--candidates <file.json>]
60
+ [--model <provider/model>] [--cell <${slopcameraGalleryLimits.cellEdgeMin}-${slopcameraGalleryLimits.cellEdgeMax}>] [--tile|--no-tile] [--json]
48
61
  slopcamera code search [query] [--limit <number>]
49
62
  slopcamera code execute <operation> --input <JSON>
50
63
  slopcamera mcp --root <workspace>
51
- slopcamera canvas open <file.tldr|file.tldraw>
52
- slopcamera canvas status
53
- slopcamera canvas url
54
- slopcamera canvas install [--yes] [--download-only]
55
64
  slopcamera doctor
65
+ slopcamera support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
56
66
  slopcamera skill path
57
67
  slopcamera skill install [--target codex|claude|agents] [--scope user|project] [--force]
58
68
 
@@ -63,9 +73,8 @@ Render writes the same five replaceable artifacts on every run:
63
73
  <name>.light.png
64
74
  <name>.dark.png
65
75
 
66
- The .tldr file is editable tldraw interchange. It imports into tldraw Offline,
67
- which can save the newer app-owned .tldraw bundle. Rendering does not require
68
- tldraw Offline or the tldraw SDK.
76
+ The .tldr file is editable interchange for browser-based canvas tooling.
77
+ Rendering does not require a desktop application or a bundled UI runtime.
69
78
 
70
79
  Vectorize adaptively traces a raster with a checksum-pinned VTracer binary.
71
80
  It enforces bounded input, decode, time, path, and output budgets and emits a
@@ -77,6 +86,21 @@ Set AI_GATEWAY_API_KEY, or run through \`vercel env run -- …\` so
77
86
  VERCEL_OIDC_TOKEN is available. Slopcamera never stores or prints the token.
78
87
  PNG, JPEG, and WebP responses are signature-checked and published atomically.
79
88
 
89
+ Icon produces isometric line-art SVG: a style-locked Gateway raster is
90
+ normalized to canonical ink-on-transparent pixels, traced by the local
91
+ vectorizer, and (when --rounds exceeds 1) critiqued by a vision model whose
92
+ feedback revises the prompt for the next attempt. Only Slopcamera's own
93
+ generated output is uploaded for critique — never user media.
94
+
95
+ Gallery generates several bounded candidates in parallel and composes them
96
+ into one labelled contact sheet plus a receipt. Use it to review texture,
97
+ skybox, backdrop, sprite, or design alternatives, then promote a chosen
98
+ candidate file explicitly — nothing is applied automatically.
99
+
100
+ Optional support: after useful work, agents can read slopcamera support protocol --json.
101
+ Discovery uses stderr without claiming an invitation; HRANESS_SUPPORT_AUDIENCE=off disables it.
102
+ No feature requires payment. Imported CLI/SDK calls and probes stay quiet.
103
+
80
104
  Code mode searches and executes a fixed semantic registry. Execute accepts
81
105
  typed JSON for one exact owned operation code; it never evaluates source text.
82
106
 
@@ -151,6 +175,23 @@ function parseDuotone(value: string | undefined): readonly [string, string] | un
151
175
  return [colors[0]!, colors[1]!]
152
176
  }
153
177
 
178
+ function parseGalleryVary(value: string | undefined): readonly SlopcameraGalleryAxisSpec[] | undefined {
179
+ if (value === undefined) return undefined
180
+ return parseSlopcameraGalleryVary(value)
181
+ }
182
+
183
+ async function readGalleryCandidates(path: string): Promise<unknown> {
184
+ const text = await readFile(resolve(path), "utf8")
185
+ if (Buffer.byteLength(text, "utf8") > 64 * 1024) {
186
+ throw new Error("--candidates JSON must be no more than 65536 UTF-8 bytes")
187
+ }
188
+ try {
189
+ return JSON.parse(text)
190
+ } catch {
191
+ throw new Error("--candidates must be valid JSON")
192
+ }
193
+ }
194
+
154
195
  function printFindings(findings: Awaited<ReturnType<typeof checkDiagramFile>>["findings"]): void {
155
196
  if (findings.length === 0) {
156
197
  console.log("No diagram lint findings.")
@@ -191,24 +232,13 @@ const starter = {
191
232
  edges: [{ id: "source-result", from: "source", to: "result" }],
192
233
  }
193
234
 
194
- async function confirmInstall(): Promise<boolean> {
195
- if (!process.stdin.isTTY) {
196
- throw new Error("Pass --yes to download the 100–230 MB official tldraw Offline installer")
197
- }
198
- const prompt = createInterface({ input: process.stdin, output: process.stdout })
199
- try {
200
- const answer = await prompt.question(
201
- "Download, verify, and launch the official tldraw Offline installer? [y/N] ",
202
- )
203
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes"
204
- } finally {
205
- prompt.close()
206
- }
207
- }
208
-
209
235
  export interface SlopcameraCliDependencies {
236
+ readonly onUsefulResult?: UsefulResultObserver
237
+ readonly supportEnvironment?: Readonly<Record<string, string | undefined>>
238
+ readonly gallery?: typeof generateSlopcameraImageGallery
210
239
  readonly generate?: typeof generateSlopcameraImageFile
211
240
  readonly hostResourceCoordinator?: HostResourceCoordinator
241
+ readonly icon?: typeof generateSlopcameraIcon
212
242
  readonly log?: (value: string) => void
213
243
  readonly vectorize?: typeof vectorizeImage
214
244
  }
@@ -230,26 +260,22 @@ function canonicalArguments(args: readonly string[]): readonly string[] {
230
260
  throw new Error("Use slopcamera diagram init, check, or render")
231
261
  }
232
262
  if (surface === "image") {
233
- if (subcommand === "vectorize" || subcommand === "generate") {
263
+ if (
264
+ subcommand === "vectorize" ||
265
+ subcommand === "generate" ||
266
+ subcommand === "icon" ||
267
+ subcommand === "gallery"
268
+ ) {
234
269
  return [subcommand, ...rest]
235
270
  }
236
- throw new Error("Use slopcamera image vectorize or generate")
237
- }
238
- if (surface === "canvas") {
239
- if (subcommand === "open") return ["open", ...rest]
240
- if (subcommand === "status" || subcommand === "url" || subcommand === "install") {
241
- return ["desktop", subcommand, ...rest]
242
- }
243
- throw new Error("Use slopcamera canvas open, status, url, or install")
271
+ throw new Error("Use slopcamera image vectorize, generate, icon, or gallery")
244
272
  }
245
273
  if (
246
274
  surface === "init" ||
247
275
  surface === "check" ||
248
276
  surface === "render" ||
249
277
  surface === "vectorize" ||
250
- surface === "generate" ||
251
- surface === "open" ||
252
- surface === "desktop"
278
+ surface === "generate"
253
279
  ) {
254
280
  throw new Error(`The flat \`${surface}\` command moved to a namespaced Slopcamera surface.\n\n${help}`)
255
281
  }
@@ -260,6 +286,11 @@ export async function main(
260
286
  args: readonly string[],
261
287
  dependencies: SlopcameraCliDependencies = {},
262
288
  ): Promise<void> {
289
+ if (args[0] === "support") {
290
+ process.exitCode = await runProductSupportCommand(args.slice(1),
291
+ dependencies.supportEnvironment === undefined ? {} : { env: dependencies.supportEnvironment })
292
+ return
293
+ }
263
294
  const [command, ...rest] = canonicalArguments(args)
264
295
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
265
296
  console.log(help)
@@ -276,6 +307,7 @@ export async function main(
276
307
  if (await pathExists(filePath)) throw new Error(`Refusing to overwrite existing file: ${filePath}`)
277
308
  await writeFile(filePath, `${JSON.stringify(starter, null, 2)}\n`)
278
309
  console.log(`Created ${filePath}`)
310
+ reportUsefulResult(dependencies.onUsefulResult)
279
311
  return
280
312
  }
281
313
 
@@ -317,6 +349,7 @@ export async function main(
317
349
  )
318
350
  console.log(artifactSummary(result.artifacts))
319
351
  printFindings(result.findings)
352
+ reportUsefulResult(dependencies.onUsefulResult)
320
353
  return
321
354
  }
322
355
 
@@ -365,6 +398,7 @@ export async function main(
365
398
  + `${result.receipt.profile}/${result.receipt.representation}: ${result.outputPath}`,
366
399
  )
367
400
  }
401
+ reportUsefulResult(dependencies.onUsefulResult)
368
402
  return
369
403
  }
370
404
 
@@ -405,6 +439,173 @@ export async function main(
405
439
  `Generated ${result.mediaType} with ${result.model}: ${result.outputPath} (${result.bytes} bytes, request ${result.requestId})`,
406
440
  )
407
441
  }
442
+ reportUsefulResult(dependencies.onUsefulResult)
443
+ return
444
+ }
445
+
446
+ if (command === "icon") {
447
+ const parsed = parseArguments(
448
+ rest,
449
+ new Set(["model", "output", "ink", "purpose", "rounds", "critique-model"]),
450
+ )
451
+ const unknownFlags = [...parsed.flags].filter(
452
+ (flag) => flag !== "json" && flag !== "keep-raster",
453
+ )
454
+ if (unknownFlags.length > 0) {
455
+ throw new Error(`Unknown icon option: --${unknownFlags[0]}`)
456
+ }
457
+ if (parsed.positionals.length !== 1) {
458
+ throw new Error("slopcamera image icon accepts exactly one subject")
459
+ }
460
+ const output = requiredOption(parsed, "output")
461
+ if (!output.toLowerCase().endsWith(".svg")) {
462
+ throw new Error("--output must end in .svg")
463
+ }
464
+ const model = parsed.options.model ?? slopcameraImageModels[1]
465
+ if (
466
+ model.length > 256 ||
467
+ !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)
468
+ ) {
469
+ throw new Error(
470
+ "--model must be a bounded Vercel AI Gateway provider/model id",
471
+ )
472
+ }
473
+ const critiqueModel = parsed.options["critique-model"]
474
+ if (
475
+ critiqueModel !== undefined &&
476
+ (critiqueModel.length > 256 ||
477
+ !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(critiqueModel))
478
+ ) {
479
+ throw new Error(
480
+ "--critique-model must be a bounded Vercel AI Gateway provider/model id",
481
+ )
482
+ }
483
+ const ink = parsed.options.ink
484
+ if (ink !== undefined && !/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(ink)) {
485
+ throw new Error("--ink must be a #rgb or #rrggbb color")
486
+ }
487
+ const purpose = parsed.options.purpose
488
+ if (purpose !== undefined && purpose !== "mark" && purpose !== "illustration") {
489
+ throw new Error("--purpose must be mark or illustration")
490
+ }
491
+ const rounds = parsePositiveInteger(parsed.options.rounds, "rounds")
492
+ if (rounds !== undefined && rounds > slopcameraIconMaximumRounds) {
493
+ throw new Error(`--rounds must be at most ${slopcameraIconMaximumRounds}`)
494
+ }
495
+ const result = await withSlopcameraOperationHostAdmission(
496
+ "slopcamera.image.icon",
497
+ async (lease) => await (dependencies.icon ?? generateSlopcameraIcon)({
498
+ keepRaster: parsed.flags.has("keep-raster"),
499
+ model,
500
+ outputPath: output,
501
+ subject: requiredPositional(parsed, 0, "subject"),
502
+ inheritedFileDescriptors: [lease.inheritedFileDescriptor],
503
+ ...(critiqueModel === undefined ? {} : { critiqueModel }),
504
+ ...(ink === undefined ? {} : { ink }),
505
+ ...(purpose === undefined ? {} : { purpose }),
506
+ ...(rounds === undefined ? {} : { rounds }),
507
+ }),
508
+ hostAdmissionOptions(dependencies),
509
+ )
510
+ if (parsed.flags.has("json")) {
511
+ ;(dependencies.log ?? console.log)(JSON.stringify(result, null, 2))
512
+ } else {
513
+ const scored = result.attempts
514
+ .map((attempt) =>
515
+ attempt.score === undefined
516
+ ? `#${attempt.round}`
517
+ : `#${attempt.round}=${attempt.score}`,
518
+ )
519
+ .join(" ")
520
+ ;(dependencies.log ?? console.log)(
521
+ `Icon ${result.svgSha256.slice(0, 12)} round ${result.selectedRound}/${result.attempts.length} (${scored}): ${result.outputPath}`,
522
+ )
523
+ }
524
+ return
525
+ }
526
+
527
+ if (command === "gallery") {
528
+ const parsed = parseArguments(
529
+ rest,
530
+ new Set(["model", "output-dir", "kind", "count", "vary", "candidates", "cell", "timeout-ms"]),
531
+ )
532
+ const unknownFlags = [...parsed.flags].filter(
533
+ (flag) => flag !== "json" && flag !== "tile" && flag !== "no-tile",
534
+ )
535
+ if (unknownFlags.length > 0) {
536
+ throw new Error(`Unknown gallery option: --${unknownFlags[0]}`)
537
+ }
538
+ if (parsed.flags.has("tile") && parsed.flags.has("no-tile")) {
539
+ throw new Error("--tile and --no-tile are mutually exclusive")
540
+ }
541
+ if (parsed.positionals.length !== 1) {
542
+ throw new Error("slopcamera image gallery accepts exactly one subject")
543
+ }
544
+ const model = parsed.options.model ?? slopcameraImageModels[1]
545
+ if (
546
+ model.length > 256 ||
547
+ !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)
548
+ ) {
549
+ throw new Error(
550
+ "--model must be a bounded Vercel AI Gateway provider/model id",
551
+ )
552
+ }
553
+ const kind = parsed.options.kind
554
+ if (kind !== undefined && !slopcameraGalleryKinds.includes(kind as never)) {
555
+ throw new Error(`--kind must be one of: ${slopcameraGalleryKinds.join(", ")}`)
556
+ }
557
+ const count = parsePositiveInteger(parsed.options.count, "count")
558
+ if (count !== undefined && count > slopcameraGalleryLimits.candidates) {
559
+ throw new Error(`--count must be at most ${slopcameraGalleryLimits.candidates}`)
560
+ }
561
+ const cell = parsePositiveInteger(parsed.options.cell, "cell")
562
+ if (
563
+ cell !== undefined &&
564
+ (cell < slopcameraGalleryLimits.cellEdgeMin || cell > slopcameraGalleryLimits.cellEdgeMax)
565
+ ) {
566
+ throw new Error(
567
+ `--cell must be between ${slopcameraGalleryLimits.cellEdgeMin} and ${slopcameraGalleryLimits.cellEdgeMax}`,
568
+ )
569
+ }
570
+ const timeoutMs = parsePositiveInteger(parsed.options["timeout-ms"], "timeout-ms")
571
+ const vary = parseGalleryVary(parsed.options.vary)
572
+ const candidates = parsed.options.candidates === undefined
573
+ ? undefined
574
+ : await readGalleryCandidates(parsed.options.candidates)
575
+ if (candidates !== undefined && !Array.isArray(candidates)) {
576
+ throw new Error("--candidates JSON must be an array of {id, prompt|variant} entries")
577
+ }
578
+ const result = await withSlopcameraOperationHostAdmission(
579
+ "slopcamera.image.gallery",
580
+ async () => await (dependencies.gallery ?? generateSlopcameraImageGallery)({
581
+ subject: requiredPositional(parsed, 0, "subject"),
582
+ outputDir: requiredOption(parsed, "output-dir"),
583
+ model,
584
+ ...(kind === undefined ? {} : { kind }),
585
+ ...(count === undefined ? {} : { count }),
586
+ ...(vary === undefined ? {} : { vary }),
587
+ ...(candidates === undefined ? {} : { candidates }),
588
+ ...(cell === undefined ? {} : { cellEdge: cell }),
589
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
590
+ ...(parsed.flags.has("tile")
591
+ ? { tiled: true }
592
+ : parsed.flags.has("no-tile") ? { tiled: false } : {}),
593
+ }),
594
+ hostAdmissionOptions(dependencies),
595
+ )
596
+ if (parsed.flags.has("json")) {
597
+ ;(dependencies.log ?? console.log)(JSON.stringify(result, null, 2))
598
+ } else {
599
+ const rows = result.candidates.map(candidate =>
600
+ candidate.status === "generated"
601
+ ? `#${candidate.index} ${candidate.id}\t${candidate.path}`
602
+ : `#${candidate.index} ${candidate.id}\tfailed`)
603
+ ;(dependencies.log ?? console.log)(
604
+ `Gallery ${result.counts.generated}/${result.counts.requested} generated (${result.model}): ${result.gallery.path}\n`
605
+ + `${rows.join("\n")}\nreceipt ${result.receiptPath}`,
606
+ )
607
+ }
608
+ reportUsefulResult(dependencies.onUsefulResult)
408
609
  return
409
610
  }
410
611
 
@@ -460,6 +661,9 @@ export async function main(
460
661
  ;(dependencies.log ?? console.log)(
461
662
  JSON.stringify({ operation, result }, null, 2),
462
663
  )
664
+ if (operation === "slopcamera.diagram.render" || operation === "slopcamera.image.vectorize" || operation === "slopcamera.image.generate") {
665
+ reportUsefulResult(dependencies.onUsefulResult)
666
+ }
463
667
  return
464
668
  }
465
669
  throw new Error(
@@ -479,15 +683,7 @@ export async function main(
479
683
  return
480
684
  }
481
685
 
482
- if (command === "open") {
483
- const parsed = parseArguments(rest, new Set())
484
- await openInDesktop(requiredPositional(parsed, 0, "tldraw file"))
485
- console.log("Opened in tldraw Offline.")
486
- return
487
- }
488
-
489
686
  if (command === "doctor") {
490
- const status = await desktopStatus()
491
687
  console.log(`slopcamera ${slopcameraCliVersion}`)
492
688
  console.log(`Bun ${process.versions.bun ?? "not detected"}`)
493
689
  console.log("Headless diagram SVG/PNG/tldraw renderer ready")
@@ -496,67 +692,16 @@ export async function main(
496
692
  ? "Local raster-to-SVG vectorizer unavailable on Windows (fails closed with tool_platform)"
497
693
  : "Local raster-to-SVG vectorizer ready without authentication (VTracer downloads on first use)",
498
694
  )
499
- console.log("Root-relative MCP check/render server ready (trusted local workspace)")
695
+ console.log("Root-relative MCP check/render/scene server ready (trusted local workspace)")
500
696
  const gateway = slopcameraGatewayCredentialStatus()
501
697
  console.log(
502
698
  gateway.available
503
699
  ? `Vercel AI Gateway ready via ${gateway.source}`
504
700
  : "Vercel AI Gateway requires AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN",
505
701
  )
506
- console.log(
507
- status.installedPath === null
508
- ? "tldraw Offline not installed (optional)"
509
- : `tldraw Offline: ${status.installedPath}`,
510
- )
511
- console.log(
512
- status.server === null
513
- ? "tldraw Offline agent server not running (optional)"
514
- : `tldraw Offline agent server: localhost:${status.server.port}`,
515
- )
516
702
  return
517
703
  }
518
704
 
519
- if (command === "desktop") {
520
- const [subcommand, ...subcommandArgs] = rest
521
- if (subcommand === "status") {
522
- const status = await desktopStatus()
523
- console.log(JSON.stringify(status, null, 2))
524
- return
525
- }
526
- if (subcommand === "url") {
527
- const release = await getLatestDesktopRelease()
528
- const asset = selectDesktopAsset(release)
529
- console.log(
530
- JSON.stringify(
531
- {
532
- release: release.tag_name,
533
- releaseUrl: release.html_url,
534
- asset: asset.name,
535
- url: asset.browser_download_url,
536
- bytes: asset.size,
537
- sha256: asset.digest,
538
- },
539
- null,
540
- 2,
541
- ),
542
- )
543
- return
544
- }
545
- if (subcommand === "install") {
546
- const parsed = parseArguments(subcommandArgs, new Set())
547
- if (!parsed.flags.has("yes") && !(await confirmInstall())) {
548
- console.log("Cancelled.")
549
- return
550
- }
551
- const result = await installDesktop({ downloadOnly: parsed.flags.has("download-only") })
552
- console.log(
553
- `${parsed.flags.has("download-only") ? "Downloaded" : "Prepared"} tldraw Offline ${result.release}: ${result.filePath}`,
554
- )
555
- return
556
- }
557
- throw new Error("Use slopcamera canvas status, url, or install")
558
- }
559
-
560
705
  if (command === "skill") {
561
706
  const [subcommand, ...subcommandArgs] = rest
562
707
  if (subcommand === "path") {
@@ -593,7 +738,10 @@ export async function main(
593
738
 
594
739
  if (import.meta.main) {
595
740
  try {
596
- await main(process.argv.slice(2))
741
+ const env = standaloneSupportEnvironment()
742
+ let usefulResult = false
743
+ await main(process.argv.slice(2), { supportEnvironment: env, onUsefulResult: () => { usefulResult = true } })
744
+ if (usefulResult && (process.exitCode ?? 0) === 0) await showProductSupportInvitation({ env })
597
745
  } catch (error) {
598
746
  console.error(error instanceof Error ? error.message : String(error))
599
747
  process.exitCode = 1
@@ -19,6 +19,15 @@ const MAX_CANONICAL_JSON_BYTES = Number.MAX_SAFE_INTEGER
19
19
  const MAX_CANONICAL_JSON_DEPTH = Number.MAX_SAFE_INTEGER
20
20
  const MAX_CANONICAL_JSON_VALUES = Number.MAX_SAFE_INTEGER
21
21
 
22
+ // Cross-implementation parity (pinned byte- and digest-for-digest by
23
+ // canonical-parity.test.ts): on the shared domain — finite numbers other
24
+ // than -0, well-formed UTF-16 strings, dense arrays, and plain objects
25
+ // with enumerable string data properties — canonicalJson is identical to
26
+ // @hraness/oh's RFC 8785 canonicalizer. Slopcamera intentionally keeps a
27
+ // wider domain that oh rejects: -0 normalizes to 0, unpaired surrogates
28
+ // serialize through \uXXXX escapes, non-enumerable own properties and
29
+ // non-index array properties are ignored, and the iterative traversal
30
+ // accepts nesting far beyond oh's recursive stack range.
22
31
  export type CanonicalJsonValue =
23
32
  | JsonScalar
24
33
  | readonly CanonicalJsonValue[]
package/src/generate.ts CHANGED
@@ -116,7 +116,7 @@ function environment(
116
116
  return injected ?? process.env
117
117
  }
118
118
 
119
- function resolveGatewayCredential(
119
+ export function resolveSlopcameraGatewayCredential(
120
120
  injected: SlopcameraEnvironment | undefined,
121
121
  ): Readonly<{
122
122
  source: SlopcameraGatewayCredentialSource
@@ -588,7 +588,7 @@ async function performGeneration(
588
588
  }>> {
589
589
  const model = validateModel(input.model)
590
590
  const prompt = validatePrompt(input.prompt)
591
- const credential = resolveGatewayCredential(dependencies.environment)
591
+ const credential = resolveSlopcameraGatewayCredential(dependencies.environment)
592
592
  const timeout = combineSignals(input.signal, validateTimeout(input.timeoutMs))
593
593
  try {
594
594
  const generation = (async () => {