@hraness/slopcamera 3.2.8 → 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 (150) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-cgpkyjqa.js +3 -0
  59. package/dist/index-f0ptv8dm.js +7 -0
  60. package/dist/index-grk47tgs.js +3 -0
  61. package/dist/index-qry58nj2.js +10 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/package.json +7 -5
  69. package/skills/slopcamera/SKILL.md +6 -1
  70. package/skills/slopcamera/references/directed-scenes.md +22 -2
  71. package/skills/slopcamera/references/gateway-media.md +15 -1
  72. package/skills/slopcamera/references/image-galleries.md +166 -0
  73. package/skills/slopcamera/references/install.md +1 -1
  74. package/skills/slopcamera/references/parametric-design.md +14 -0
  75. package/skills/slopcamera/references/scene-building.md +32 -0
  76. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  77. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  78. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  79. package/src/capability-manifest.ts +361 -0
  80. package/src/cli.ts +222 -4
  81. package/src/code/canonical-json.ts +9 -0
  82. package/src/generate.ts +2 -2
  83. package/src/icon.ts +1111 -0
  84. package/src/image-gallery.ts +1049 -0
  85. package/src/index.ts +11 -0
  86. package/src/mcp/boundary.ts +11 -11
  87. package/src/mcp/index.ts +3 -0
  88. package/src/mcp/server.ts +2 -1
  89. package/src/mcp/tools.ts +1642 -130
  90. package/src/mcp/types.ts +19 -0
  91. package/src/operations.ts +412 -0
  92. package/src/portable-capability-manifest.ts +67 -0
  93. package/src/scene-gallery.ts +184 -0
  94. package/src/spatial-scene/asset-admission.ts +183 -0
  95. package/src/spatial-scene/audit-rendered.ts +692 -0
  96. package/src/spatial-scene/audit.ts +617 -0
  97. package/src/spatial-scene/behavior-audit.ts +252 -0
  98. package/src/spatial-scene/behavior-bake.ts +233 -0
  99. package/src/spatial-scene/behavior-fns.ts +585 -0
  100. package/src/spatial-scene/behavior-gallery.ts +109 -0
  101. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  102. package/src/spatial-scene/behavior-trace.ts +325 -0
  103. package/src/spatial-scene/behavior.ts +618 -0
  104. package/src/spatial-scene/build.ts +632 -0
  105. package/src/spatial-scene/camera-rig.ts +125 -0
  106. package/src/spatial-scene/camera-track.ts +4 -2
  107. package/src/spatial-scene/character.ts +124 -0
  108. package/src/spatial-scene/contracts.ts +75 -7
  109. package/src/spatial-scene/design-templates.ts +351 -0
  110. package/src/spatial-scene/design.ts +327 -0
  111. package/src/spatial-scene/direction-compile.ts +584 -0
  112. package/src/spatial-scene/direction.ts +122 -0
  113. package/src/spatial-scene/effects.ts +149 -0
  114. package/src/spatial-scene/evaluate.ts +109 -29
  115. package/src/spatial-scene/gallery.ts +277 -0
  116. package/src/spatial-scene/generate.ts +239 -0
  117. package/src/spatial-scene/geometry-native.ts +165 -0
  118. package/src/spatial-scene/geometry.ts +1216 -0
  119. package/src/spatial-scene/gltf.ts +600 -77
  120. package/src/spatial-scene/identity.ts +55 -2
  121. package/src/spatial-scene/index.ts +36 -0
  122. package/src/spatial-scene/inspect.ts +35 -6
  123. package/src/spatial-scene/material-lighting.ts +609 -0
  124. package/src/spatial-scene/math.ts +21 -4
  125. package/src/spatial-scene/motion-evidence.ts +88 -0
  126. package/src/spatial-scene/parametric.ts +765 -0
  127. package/src/spatial-scene/particle-preparation.ts +183 -0
  128. package/src/spatial-scene/particle.ts +195 -0
  129. package/src/spatial-scene/patch.ts +109 -15
  130. package/src/spatial-scene/performance.ts +1613 -0
  131. package/src/spatial-scene/probe.ts +137 -0
  132. package/src/spatial-scene/recipe-pack.ts +86 -0
  133. package/src/spatial-scene/render-effects.ts +240 -0
  134. package/src/spatial-scene/review.ts +639 -0
  135. package/src/spatial-scene/simulation-bake.ts +206 -0
  136. package/src/spatial-scene/simulation.ts +187 -0
  137. package/src/spatial-scene/solve.ts +366 -0
  138. package/src/spatial-scene/temporal-audit.ts +354 -0
  139. package/src/version.ts +1 -1
  140. package/dist/index-42zsesc1.js +0 -2257
  141. package/dist/index-7131pg1c.js +0 -1820
  142. package/dist/index-7308egqr.js +0 -48
  143. package/dist/index-8txs6fkn.js +0 -712
  144. package/dist/index-fava6pge.js +0 -192
  145. package/dist/index-h1k0fnjq.js +0 -2363
  146. package/dist/index-r7gdhmsp.js +0 -424
  147. package/dist/index-sh6xbav6.js +0 -1219
  148. package/dist/index-z1w83f81.js +0 -4
  149. package/dist/index-zfnddgay.js +0 -2029
  150. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,361 @@
1
+ import { z } from "zod"
2
+
3
+ import { canonicalJsonSha256 } from "./code/canonical-json.js"
4
+ import { createBoundedJsonValueSnapshot, deepFreezeJson } from "./code/json-snapshot.js"
5
+
6
+ export const SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS = Object.freeze({
7
+ bytes: 4 * 1024 * 1024,
8
+ commands: 256,
9
+ evidence: 64,
10
+ modules: 64,
11
+ operations: 512,
12
+ profiles: 128,
13
+ requirements: 32,
14
+ tools: 128,
15
+ workflows: 128,
16
+ })
17
+
18
+ const identifier = z.string().min(1).max(192).regex(/^[a-z][a-z0-9]*(?:[-_.:/@][a-z0-9]+)*$/u)
19
+ const commandName = z.string().min(1).max(128).regex(/^[a-z][a-z0-9-]*(?: [a-z][a-z0-9-]*){0,3}$/u)
20
+ const digest = z.string().regex(/^[a-f0-9]{64}$/u)
21
+ const positiveSafeInteger = z.number().int().safe().positive()
22
+ const nonnegativeSafeInteger = z.number().int().safe().nonnegative()
23
+
24
+ export const SlopcameraCapabilityEffectSchema = z.enum([
25
+ "pure",
26
+ "local-read",
27
+ "local-derived-write",
28
+ "project-mutation",
29
+ "paid-cloud",
30
+ "live-control",
31
+ ])
32
+
33
+ export const SlopcameraCapabilityResumeSchema = z.enum([
34
+ "deterministic",
35
+ "verified-receipt",
36
+ "recoverable-transaction",
37
+ "ambiguous-after-dispatch",
38
+ "non-resumable-live",
39
+ ])
40
+
41
+ export const SlopcameraCapabilityResourceSchema = z.enum([
42
+ "cpu",
43
+ "local-io",
44
+ "ffmpeg",
45
+ "vision",
46
+ "whisper",
47
+ "network",
48
+ "paid-call",
49
+ "project-render",
50
+ "project-publication",
51
+ "output-publication",
52
+ "capture-device",
53
+ "browser",
54
+ ])
55
+
56
+ export const SlopcameraCapabilityPreparationSchema = z.enum([
57
+ "project-state",
58
+ "recording-metadata",
59
+ "screen-capture",
60
+ "camera",
61
+ "microphone",
62
+ "system-audio",
63
+ "typed-text",
64
+ "window-metadata",
65
+ "local-media",
66
+ "provider-options",
67
+ ])
68
+
69
+ export const SlopcameraCapabilityOperationSchema = z.strictObject({
70
+ cache: z.enum(["none", "exact-run", "content-addressed"]),
71
+ cancellable: z.boolean(),
72
+ effect: SlopcameraCapabilityEffectSchema,
73
+ inputSchemaId: identifier,
74
+ key: z.string().min(3).max(256).regex(/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*@[1-9][0-9]*$/u),
75
+ kind: z.string().min(3).max(192).regex(/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*)+$/u),
76
+ lifecycle: z.enum(["pure", "local-artifact", "project-transaction", "paid-dispatch", "live-control"]),
77
+ maxDurationMs: positiveSafeInteger,
78
+ maxFanOut: nonnegativeSafeInteger,
79
+ maxInputBytes: nonnegativeSafeInteger,
80
+ maxOutputBytes: nonnegativeSafeInteger,
81
+ outputSchemaId: identifier,
82
+ preparation: z.array(SlopcameraCapabilityPreparationSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.requirements),
83
+ resources: z.array(z.strictObject({
84
+ amount: positiveSafeInteger,
85
+ resource: SlopcameraCapabilityResourceSchema,
86
+ })).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.requirements),
87
+ resume: SlopcameraCapabilityResumeSchema,
88
+ version: positiveSafeInteger,
89
+ }).superRefine((operation, context) => {
90
+ if (operation.key !== `${operation.kind}@${operation.version}`) {
91
+ context.addIssue({ code: "custom", path: ["key"], message: "Operation key must match kind and version." })
92
+ }
93
+ if (new Set(operation.preparation).size !== operation.preparation.length) {
94
+ context.addIssue({ code: "custom", path: ["preparation"], message: "Operation preparation entries must be unique." })
95
+ }
96
+ const resources = operation.resources.map(claim => claim.resource)
97
+ if (new Set(resources).size !== resources.length) {
98
+ context.addIssue({ code: "custom", path: ["resources"], message: "Operation resource claims must be unique by resource." })
99
+ }
100
+ })
101
+
102
+ export const SlopcameraCapabilityWorkflowSchema = z.strictObject({
103
+ id: identifier,
104
+ inputSchemaId: identifier,
105
+ key: z.string().min(3).max(256).regex(/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*@[1-9][0-9]*$/u),
106
+ version: positiveSafeInteger,
107
+ }).superRefine((workflow, context) => {
108
+ if (workflow.key !== `${workflow.id}@${workflow.version}`) {
109
+ context.addIssue({ code: "custom", path: ["key"], message: "Workflow key must match id and version." })
110
+ }
111
+ })
112
+
113
+ export const SlopcameraCapabilityToolSchema = z.strictObject({
114
+ destructive: z.boolean(),
115
+ idempotent: z.boolean(),
116
+ name: z.string().min(1).max(128).regex(/^[a-z][a-z0-9_]*$/u),
117
+ openWorld: z.boolean(),
118
+ readOnly: z.boolean(),
119
+ }).superRefine((tool, context) => {
120
+ if (tool.readOnly && tool.destructive) {
121
+ context.addIssue({ code: "custom", message: "A read-only tool cannot be destructive." })
122
+ }
123
+ })
124
+
125
+ export const SlopcameraCapabilityCommandSchema = z.strictObject({
126
+ effect: SlopcameraCapabilityEffectSchema,
127
+ name: commandName,
128
+ })
129
+
130
+ export const SlopcameraCapabilityQualificationSchema = z.strictObject({
131
+ evidence: z.array(z.string().min(1).max(512)).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.evidence),
132
+ level: z.enum(["unit", "fixture", "browser", "native", "provider"]),
133
+ status: z.enum(["qualified", "conditional", "not-qualified"]),
134
+ }).superRefine((qualification, context) => {
135
+ if (qualification.status !== "not-qualified" && qualification.evidence.length === 0) {
136
+ context.addIssue({ code: "custom", path: ["evidence"], message: "Qualified or conditional profiles require evidence references." })
137
+ }
138
+ })
139
+
140
+ export const SlopcameraCapabilityProfileSchema = z.strictObject({
141
+ id: identifier,
142
+ kind: z.enum(["renderer", "native-adapter", "provider", "authoring", "interchange"]),
143
+ qualification: SlopcameraCapabilityQualificationSchema,
144
+ requirements: z.array(z.strictObject({
145
+ description: z.string().min(1).max(256),
146
+ id: identifier,
147
+ optional: z.boolean(),
148
+ })).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.requirements),
149
+ })
150
+
151
+ export const SlopcameraCapabilityModuleDeclarationSchema = z.strictObject({
152
+ commands: z.array(SlopcameraCapabilityCommandSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.commands),
153
+ description: z.string().min(1).max(1_024),
154
+ moduleId: z.string().min(1).max(128).regex(/^slopcamera\.capability\.[a-z][a-z0-9-]*$/u),
155
+ operationKeys: z.array(z.string().min(3).max(256)).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.operations),
156
+ profiles: z.array(SlopcameraCapabilityProfileSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.profiles),
157
+ stability: z.enum(["stable", "experimental"]),
158
+ title: z.string().min(1).max(160),
159
+ toolNames: z.array(z.string().min(1).max(128)).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.tools),
160
+ trust: z.enum(["portable", "local-effects", "trusted-authoring", "native-adapter", "paid-provider"]),
161
+ version: positiveSafeInteger,
162
+ workflowKeys: z.array(z.string().min(3).max(256)).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.workflows),
163
+ }).superRefine((module, context) => {
164
+ if (module.commands.length + module.operationKeys.length + module.profiles.length + module.toolNames.length + module.workflowKeys.length === 0) {
165
+ context.addIssue({ code: "custom", message: "A capability module must expose at least one command, operation, profile, tool, or workflow." })
166
+ }
167
+ for (const [name, values] of [["operationKeys", module.operationKeys], ["toolNames", module.toolNames], ["workflowKeys", module.workflowKeys]] as const) {
168
+ if (new Set(values).size !== values.length) {
169
+ context.addIssue({ code: "custom", path: [name], message: `${name} must be unique within a capability module.` })
170
+ }
171
+ }
172
+ })
173
+
174
+ export const SlopcameraCapabilityModuleSchema = z.strictObject({
175
+ commands: z.array(SlopcameraCapabilityCommandSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.commands),
176
+ description: z.string().min(1).max(1_024),
177
+ moduleId: z.string().min(1).max(128).regex(/^slopcamera\.capability\.[a-z][a-z0-9-]*$/u),
178
+ operations: z.array(SlopcameraCapabilityOperationSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.operations),
179
+ profiles: z.array(SlopcameraCapabilityProfileSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.profiles),
180
+ stability: z.enum(["stable", "experimental"]),
181
+ title: z.string().min(1).max(160),
182
+ tools: z.array(SlopcameraCapabilityToolSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.tools),
183
+ trust: z.enum(["portable", "local-effects", "trusted-authoring", "native-adapter", "paid-provider"]),
184
+ version: positiveSafeInteger,
185
+ workflows: z.array(SlopcameraCapabilityWorkflowSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.workflows),
186
+ }).superRefine((module, context) => {
187
+ const identities = [
188
+ ["commands", module.commands.map(value => value.name)],
189
+ ["operations", module.operations.map(value => value.key)],
190
+ ["profiles", module.profiles.map(value => value.id)],
191
+ ["tools", module.tools.map(value => value.name)],
192
+ ["workflows", module.workflows.map(value => value.key)],
193
+ ] as const
194
+ if (identities.every(([, values]) => values.length === 0)) {
195
+ context.addIssue({ code: "custom", message: "A capability module must expose at least one command, operation, profile, tool, or workflow." })
196
+ }
197
+ for (const [name, values] of identities) {
198
+ if (new Set(values).size !== values.length) context.addIssue({ code: "custom", path: [name], message: `${name} must be unique within a capability module.` })
199
+ }
200
+ })
201
+
202
+ const manifestBodyBaseSchema = z.strictObject({
203
+ host: z.enum(["portable", "local"]),
204
+ kind: z.literal("slopcamera.capability-manifest"),
205
+ modules: z.array(SlopcameraCapabilityModuleSchema).min(1).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.modules),
206
+ schemaVersion: z.literal(1),
207
+ toolVersion: z.string().min(1).max(128),
208
+ })
209
+
210
+ type ManifestBody = z.infer<typeof manifestBodyBaseSchema>
211
+
212
+ function validateManifestOwnership(manifest: ManifestBody, context: z.RefinementCtx): void {
213
+ const ownership = new Map<string, string>()
214
+ for (const [moduleIndex, module] of manifest.modules.entries()) {
215
+ for (const identity of [
216
+ module.moduleId,
217
+ ...module.commands.map(value => `command:${value.name}`),
218
+ ...module.operations.map(value => `operation:${value.key}`),
219
+ ...module.profiles.map(value => `profile:${value.id}`),
220
+ ...module.tools.map(value => `tool:${value.name}`),
221
+ ...module.workflows.map(value => `workflow:${value.key}`),
222
+ ]) {
223
+ const owner = ownership.get(identity)
224
+ if (owner !== undefined) context.addIssue({ code: "custom", path: ["modules", moduleIndex], message: `${identity} is owned by both ${owner} and ${module.moduleId}.` })
225
+ ownership.set(identity, module.moduleId)
226
+ }
227
+ if (manifest.host === "portable" && module.trust !== "portable") {
228
+ context.addIssue({ code: "custom", path: ["modules", moduleIndex, "trust"], message: "Portable manifests may contain only portable capability modules." })
229
+ }
230
+ }
231
+ }
232
+
233
+ const manifestBodySchema = manifestBodyBaseSchema.superRefine(validateManifestOwnership)
234
+
235
+ export const SlopcameraCapabilityManifestSchema = manifestBodyBaseSchema.extend({
236
+ manifestSha256: digest,
237
+ }).superRefine(validateManifestOwnership)
238
+
239
+ const compilerInputSchema = z.strictObject({
240
+ host: z.enum(["portable", "local"]),
241
+ modules: z.array(SlopcameraCapabilityModuleDeclarationSchema).min(1).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.modules),
242
+ operations: z.array(SlopcameraCapabilityOperationSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.operations),
243
+ tools: z.array(SlopcameraCapabilityToolSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.tools),
244
+ toolVersion: z.string().min(1).max(128),
245
+ workflows: z.array(SlopcameraCapabilityWorkflowSchema).max(SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.workflows),
246
+ })
247
+
248
+ export type SlopcameraCapabilityOperation = Readonly<z.infer<typeof SlopcameraCapabilityOperationSchema>>
249
+ export type SlopcameraCapabilityWorkflow = Readonly<z.infer<typeof SlopcameraCapabilityWorkflowSchema>>
250
+ export type SlopcameraCapabilityTool = Readonly<z.infer<typeof SlopcameraCapabilityToolSchema>>
251
+ export type SlopcameraCapabilityModuleDeclaration = Readonly<z.infer<typeof SlopcameraCapabilityModuleDeclarationSchema>>
252
+ export type SlopcameraCapabilityManifest = Readonly<z.infer<typeof SlopcameraCapabilityManifestSchema>>
253
+
254
+ function compareBy<Key extends string>(key: Key) {
255
+ return <Value extends Readonly<Record<Key, string>>>(left: Value, right: Value): number => left[key].localeCompare(right[key])
256
+ }
257
+
258
+ function indexed<Value>(values: readonly Value[], key: (value: Value) => string, label: string): ReadonlyMap<string, Value> {
259
+ const result = new Map<string, Value>()
260
+ for (const value of values) {
261
+ const identity = key(value)
262
+ if (result.has(identity)) throw new TypeError(`Duplicate ${label}: ${identity}.`)
263
+ result.set(identity, value)
264
+ }
265
+ return result
266
+ }
267
+
268
+ function assertExactOwnership(label: string, available: ReadonlyMap<string, unknown>, owners: ReadonlyMap<string, string>): void {
269
+ for (const key of available.keys()) {
270
+ if (!owners.has(key)) throw new TypeError(`Unowned ${label}: ${key}.`)
271
+ }
272
+ for (const key of owners.keys()) {
273
+ if (!available.has(key)) throw new TypeError(`Unknown ${label} declared by a capability module: ${key}.`)
274
+ }
275
+ }
276
+
277
+ export function defineSlopcameraCapabilityModule(input: unknown): SlopcameraCapabilityModuleDeclaration {
278
+ return deepFreezeJson(SlopcameraCapabilityModuleDeclarationSchema.parse(
279
+ createBoundedJsonValueSnapshot(input, SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.bytes, "capability module").value,
280
+ ))
281
+ }
282
+
283
+ export function compileSlopcameraCapabilityManifest(input: unknown): SlopcameraCapabilityManifest {
284
+ const parsed = compilerInputSchema.parse(
285
+ createBoundedJsonValueSnapshot(input, SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.bytes, "capability manifest compiler input").value,
286
+ )
287
+ const operations = indexed(parsed.operations, value => value.key, "operation inventory key")
288
+ const workflows = indexed(parsed.workflows, value => value.key, "workflow inventory key")
289
+ const tools = indexed(parsed.tools, value => value.name, "tool inventory name")
290
+ const operationOwners = new Map<string, string>()
291
+ const workflowOwners = new Map<string, string>()
292
+ const toolOwners = new Map<string, string>()
293
+ const moduleIds = new Set<string>()
294
+ const commandOwners = new Map<string, string>()
295
+ const profileOwners = new Map<string, string>()
296
+
297
+ const claim = (owners: Map<string, string>, key: string, moduleId: string, label: string): void => {
298
+ const owner = owners.get(key)
299
+ if (owner !== undefined) throw new TypeError(`${label} ${key} is owned by both ${owner} and ${moduleId}.`)
300
+ owners.set(key, moduleId)
301
+ }
302
+
303
+ const modules = [...parsed.modules].sort(compareBy("moduleId")).map(module => {
304
+ if (moduleIds.has(module.moduleId)) throw new TypeError(`Duplicate capability module: ${module.moduleId}.`)
305
+ moduleIds.add(module.moduleId)
306
+ for (const command of module.commands) claim(commandOwners, command.name, module.moduleId, "Command")
307
+ for (const profile of module.profiles) claim(profileOwners, profile.id, module.moduleId, "Profile")
308
+ const resolvedOperations = module.operationKeys.map(key => {
309
+ claim(operationOwners, key, module.moduleId, "Operation")
310
+ const value = operations.get(key)
311
+ if (value === undefined) throw new TypeError(`Unknown operation declared by ${module.moduleId}: ${key}.`)
312
+ return value
313
+ }).sort(compareBy("key"))
314
+ const resolvedWorkflows = module.workflowKeys.map(key => {
315
+ claim(workflowOwners, key, module.moduleId, "Workflow")
316
+ const value = workflows.get(key)
317
+ if (value === undefined) throw new TypeError(`Unknown workflow declared by ${module.moduleId}: ${key}.`)
318
+ return value
319
+ }).sort(compareBy("key"))
320
+ const resolvedTools = module.toolNames.map(name => {
321
+ claim(toolOwners, name, module.moduleId, "Tool")
322
+ const value = tools.get(name)
323
+ if (value === undefined) throw new TypeError(`Unknown tool declared by ${module.moduleId}: ${name}.`)
324
+ return value
325
+ }).sort(compareBy("name"))
326
+ const { operationKeys: _operationKeys, toolNames: _toolNames, workflowKeys: _workflowKeys, ...metadata } = module
327
+ return {
328
+ ...metadata,
329
+ commands: [...module.commands].sort(compareBy("name")),
330
+ operations: resolvedOperations,
331
+ profiles: [...module.profiles].sort(compareBy("id")),
332
+ tools: resolvedTools,
333
+ workflows: resolvedWorkflows,
334
+ }
335
+ })
336
+
337
+ assertExactOwnership("operation", operations, operationOwners)
338
+ assertExactOwnership("workflow", workflows, workflowOwners)
339
+ assertExactOwnership("tool", tools, toolOwners)
340
+ const body = manifestBodySchema.parse({
341
+ host: parsed.host,
342
+ kind: "slopcamera.capability-manifest",
343
+ modules,
344
+ schemaVersion: 1,
345
+ toolVersion: parsed.toolVersion,
346
+ })
347
+ return deepFreezeJson(SlopcameraCapabilityManifestSchema.parse({
348
+ ...body,
349
+ manifestSha256: canonicalJsonSha256(body),
350
+ }))
351
+ }
352
+
353
+ export function parseSlopcameraCapabilityManifest(input: unknown): SlopcameraCapabilityManifest {
354
+ const manifest = SlopcameraCapabilityManifestSchema.parse(
355
+ createBoundedJsonValueSnapshot(input, SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS.bytes, "capability manifest").value,
356
+ )
357
+ const { manifestSha256, ...body } = manifest
358
+ const actual = canonicalJsonSha256(body)
359
+ if (actual !== manifestSha256) throw new TypeError(`Capability manifest digest mismatch: expected ${manifestSha256}, received ${actual}.`)
360
+ return deepFreezeJson(manifest)
361
+ }
package/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
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
5
  import {
6
6
  artifactSummary,
@@ -15,6 +15,17 @@ import {
15
15
  slopcameraImageModels,
16
16
  type SlopcameraImageModel,
17
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"
18
29
  import {
19
30
  executeSlopcameraOperation,
20
31
  slopcameraOperationCodes,
@@ -41,6 +52,12 @@ Usage:
41
52
  slopcamera diagram render <file> [--out-dir <directory>] [--config <file>] [--scale <number>]
42
53
  slopcamera image vectorize <image> --output <file.svg> [--json] [--duotone <#rgb,#rgb>]
43
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]
44
61
  slopcamera code search [query] [--limit <number>]
45
62
  slopcamera code execute <operation> --input <JSON>
46
63
  slopcamera mcp --root <workspace>
@@ -69,6 +86,17 @@ Set AI_GATEWAY_API_KEY, or run through \`vercel env run -- …\` so
69
86
  VERCEL_OIDC_TOKEN is available. Slopcamera never stores or prints the token.
70
87
  PNG, JPEG, and WebP responses are signature-checked and published atomically.
71
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
+
72
100
  Optional support: after useful work, agents can read slopcamera support protocol --json.
73
101
  Discovery uses stderr without claiming an invitation; HRANESS_SUPPORT_AUDIENCE=off disables it.
74
102
  No feature requires payment. Imported CLI/SDK calls and probes stay quiet.
@@ -147,6 +175,23 @@ function parseDuotone(value: string | undefined): readonly [string, string] | un
147
175
  return [colors[0]!, colors[1]!]
148
176
  }
149
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
+
150
195
  function printFindings(findings: Awaited<ReturnType<typeof checkDiagramFile>>["findings"]): void {
151
196
  if (findings.length === 0) {
152
197
  console.log("No diagram lint findings.")
@@ -190,8 +235,10 @@ const starter = {
190
235
  export interface SlopcameraCliDependencies {
191
236
  readonly onUsefulResult?: UsefulResultObserver
192
237
  readonly supportEnvironment?: Readonly<Record<string, string | undefined>>
238
+ readonly gallery?: typeof generateSlopcameraImageGallery
193
239
  readonly generate?: typeof generateSlopcameraImageFile
194
240
  readonly hostResourceCoordinator?: HostResourceCoordinator
241
+ readonly icon?: typeof generateSlopcameraIcon
195
242
  readonly log?: (value: string) => void
196
243
  readonly vectorize?: typeof vectorizeImage
197
244
  }
@@ -213,10 +260,15 @@ function canonicalArguments(args: readonly string[]): readonly string[] {
213
260
  throw new Error("Use slopcamera diagram init, check, or render")
214
261
  }
215
262
  if (surface === "image") {
216
- if (subcommand === "vectorize" || subcommand === "generate") {
263
+ if (
264
+ subcommand === "vectorize" ||
265
+ subcommand === "generate" ||
266
+ subcommand === "icon" ||
267
+ subcommand === "gallery"
268
+ ) {
217
269
  return [subcommand, ...rest]
218
270
  }
219
- throw new Error("Use slopcamera image vectorize or generate")
271
+ throw new Error("Use slopcamera image vectorize, generate, icon, or gallery")
220
272
  }
221
273
  if (
222
274
  surface === "init" ||
@@ -391,6 +443,172 @@ export async function main(
391
443
  return
392
444
  }
393
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)
609
+ return
610
+ }
611
+
394
612
  if (command === "code") {
395
613
  const [subcommand, ...subcommandArgs] = rest
396
614
  if (subcommand === "search") {
@@ -474,7 +692,7 @@ export async function main(
474
692
  ? "Local raster-to-SVG vectorizer unavailable on Windows (fails closed with tool_platform)"
475
693
  : "Local raster-to-SVG vectorizer ready without authentication (VTracer downloads on first use)",
476
694
  )
477
- 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)")
478
696
  const gateway = slopcameraGatewayCredentialStatus()
479
697
  console.log(
480
698
  gateway.available
@@ -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 () => {