@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,334 @@
1
+ import { createBoundedJsonSnapshot } from "../../../src/code/json-snapshot";
2
+ import {
3
+ SPATIAL_REVIEW_LIMITS,
4
+ SPATIAL_REVIEW_PROMPT,
5
+ SpatialReviewModelOutputSchema,
6
+ SpatialReviewProviderError,
7
+ redactedSpatialReviewProviderError,
8
+ validateSpatialReviewProviderRequest,
9
+ type SpatialReviewModelOutput,
10
+ type SpatialReviewProvider,
11
+ type SpatialReviewProviderResult,
12
+ type ValidatedSpatialReviewProviderRequest,
13
+ } from "../../../src/spatial-scene/review";
14
+
15
+ import { canonicalJson } from "../core/canonical-json";
16
+ import type { ActiveGatewayCredential } from "./gateway-credential";
17
+ import type { GatewayMediaCatalogTransport } from "./gateway-media-catalog";
18
+ import {
19
+ createFixedSceneGatewayFetch,
20
+ sceneFrameDataUrl,
21
+ VERCEL_SCENE_GATEWAY_BASE_URL,
22
+ type SceneAiSdkLoader,
23
+ type SceneAiSdkModule,
24
+ } from "./gateway-scene-provider";
25
+
26
+ const MAXIMUM_CATALOG_BYTES = 8 * 1024 * 1024;
27
+ const CATALOG_MODEL_LIMIT = 1_000;
28
+ const DEFAULT_REVIEW_GATEWAY_TIMEOUT_MS = 120_000;
29
+ const GOOGLE_GEMINI_MODEL = /^google\/gemini-[a-z0-9][a-z0-9._-]{0,239}$/u;
30
+
31
+ type GatewayFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
32
+
33
+ interface AiSdkResult {
34
+ readonly output?: unknown;
35
+ readonly response?: unknown;
36
+ readonly usage?: unknown;
37
+ }
38
+
39
+ async function loadAiSdk(): Promise<SceneAiSdkModule> {
40
+ const [gatewayValue, aiValue]: readonly unknown[] = await Promise.all([
41
+ import("@ai-sdk/gateway-v4"),
42
+ import("ai-v7"),
43
+ ]);
44
+ if (
45
+ typeof gatewayValue !== "object"
46
+ || gatewayValue === null
47
+ || !("createGateway" in gatewayValue)
48
+ || typeof gatewayValue.createGateway !== "function"
49
+ || typeof aiValue !== "object"
50
+ || aiValue === null
51
+ || !("generateText" in aiValue)
52
+ || typeof aiValue.generateText !== "function"
53
+ || !("Output" in aiValue)
54
+ || typeof aiValue.Output !== "object"
55
+ || aiValue.Output === null
56
+ || !("object" in aiValue.Output)
57
+ || typeof aiValue.Output.object !== "function"
58
+ ) {
59
+ throw new Error("The AI SDK review provider is unavailable.");
60
+ }
61
+ return {
62
+ Output: aiValue.Output as SceneAiSdkModule["Output"],
63
+ createGateway: gatewayValue.createGateway as SceneAiSdkModule["createGateway"],
64
+ generateText: aiValue.generateText as SceneAiSdkModule["generateText"],
65
+ };
66
+ }
67
+
68
+ function isRecord(value: unknown): value is Record<string, unknown> {
69
+ return typeof value === "object" && value !== null && !Array.isArray(value);
70
+ }
71
+
72
+ function boundedModelId(value: unknown): string | null {
73
+ return typeof value === "string"
74
+ && value.length >= 1
75
+ && value.length <= SPATIAL_REVIEW_LIMITS.modelIdChars
76
+ && value.includes("/")
77
+ && !/\s/u.test(value)
78
+ && !value.startsWith("/")
79
+ && !value.endsWith("/")
80
+ && ![...value].some(character => {
81
+ const code = character.codePointAt(0) ?? 0;
82
+ return code <= 31 || code === 127;
83
+ })
84
+ ? value
85
+ : null;
86
+ }
87
+
88
+ function modalityList(value: unknown): readonly string[] {
89
+ return Array.isArray(value)
90
+ ? value.filter((item): item is string => typeof item === "string")
91
+ : [];
92
+ }
93
+
94
+ export interface SpatialReviewModelSelection {
95
+ readonly catalogSha256: string;
96
+ readonly modelId: string;
97
+ }
98
+
99
+ /**
100
+ * Scans the fresh raw catalog payload for vision-capable language models:
101
+ * `type: "language"` rows whose input modalities include "image" and output
102
+ * modalities include "text". The media catalog projection drops pure language
103
+ * models, so review validates the untransformed document. Selection is
104
+ * deterministic: google/gemini-* ids first, then lexicographic order.
105
+ */
106
+ export function selectSpatialReviewModel(payload: unknown): SpatialReviewModelSelection {
107
+ const snapshot = createBoundedJsonSnapshot(payload, MAXIMUM_CATALOG_BYTES, "Gateway model catalog", {
108
+ maximumDepth: 16, maximumValues: 200_000,
109
+ });
110
+ if (snapshot.sha256 === undefined) {
111
+ throw new SpatialReviewProviderError("gateway-unavailable");
112
+ }
113
+ const value = snapshot.value;
114
+ if (!isRecord(value) || !Array.isArray(value.data) || value.data.length > CATALOG_MODEL_LIMIT) {
115
+ throw new SpatialReviewProviderError("gateway-unavailable");
116
+ }
117
+ const candidates: string[] = [];
118
+ for (const row of value.data) {
119
+ if (!isRecord(row) || row.type !== "language") continue;
120
+ const modalities = isRecord(row.modalities) ? row.modalities : {};
121
+ const input = modalityList(modalities.input);
122
+ const output = modalityList(modalities.output);
123
+ if (!input.includes("image") || !output.includes("text")) continue;
124
+ const id = boundedModelId(row.id);
125
+ if (id === null) continue;
126
+ candidates.push(id);
127
+ }
128
+ if (candidates.length === 0) throw new SpatialReviewProviderError("model-unavailable");
129
+ candidates.sort((left, right) => left.localeCompare(right));
130
+ const preferred = candidates.filter(id => GOOGLE_GEMINI_MODEL.test(id));
131
+ return { catalogSha256: snapshot.sha256, modelId: (preferred[0] ?? candidates[0])! };
132
+ }
133
+
134
+ function userMessage(request: ValidatedSpatialReviewProviderRequest): Readonly<Record<string, unknown>> {
135
+ const content: Readonly<Record<string, unknown>>[] = [{
136
+ text: [
137
+ `Review ${request.frames.length} rendered beauty frame(s) for spatial scene ${request.scene.sceneId}.`,
138
+ `Scene sha256=${request.scene.sceneSha256}; camera ${request.scene.cameraId}; duration ${request.scene.durationUs} microseconds.`,
139
+ `Sampled times (microseconds): ${request.scene.timesUs.join(", ")}.`,
140
+ request.scene.entities.length === 0
141
+ ? "The scene declares no entities."
142
+ : `Declared entity catalog for attribution: ${canonicalJson(request.scene.entities)}.`,
143
+ "Attribute a finding to entityId only when it exactly matches one declared identifier.",
144
+ ].join(" "),
145
+ type: "text",
146
+ }];
147
+ for (const frame of request.frames) {
148
+ content.push({ text: `frameIndex ${frame.index} at ${frame.timeUs} microseconds follows.`, type: "text" });
149
+ content.push({
150
+ image: sceneFrameDataUrl("image/png", frame.bytes),
151
+ mediaType: "image/png",
152
+ type: "image",
153
+ });
154
+ }
155
+ return { content, role: "user" };
156
+ }
157
+
158
+ function record(value: unknown): Readonly<Record<string, unknown>> | null {
159
+ return isRecord(value) ? value : null;
160
+ }
161
+
162
+ function tokenCount(value: unknown, key: "inputTokens" | "outputTokens"): number {
163
+ const selected = record(value)?.[key];
164
+ return Number.isSafeInteger(selected) && (selected as number) >= 0
165
+ && (selected as number) <= SPATIAL_REVIEW_LIMITS.tokenCount
166
+ ? selected as number
167
+ : 0;
168
+ }
169
+
170
+ function resolvedModel(value: unknown): string | null {
171
+ const modelId = record(value)?.modelId;
172
+ return boundedModelId(modelId) ?? (typeof modelId === "string" && modelId.length >= 1 && modelId.length <= SPATIAL_REVIEW_LIMITS.modelIdChars ? modelId : null);
173
+ }
174
+
175
+ function providerResponseId(value: unknown): string | undefined {
176
+ const id = record(value)?.id;
177
+ return typeof id === "string" && id.length >= 1 && id.length <= SPATIAL_REVIEW_LIMITS.providerResponseIdChars
178
+ && ![...id].some(character => (character.codePointAt(0) ?? 0) <= 31)
179
+ ? id
180
+ : undefined;
181
+ }
182
+
183
+ function parseReviewProviderOutput(value: unknown): SpatialReviewModelOutput {
184
+ const parsed = SpatialReviewModelOutputSchema.safeParse(value);
185
+ if (!parsed.success) throw new SpatialReviewProviderError("invalid-response");
186
+ return parsed.data;
187
+ }
188
+
189
+ export interface GatewaySpatialReviewProviderOptions {
190
+ /** Lazy credential load: reads the environment only inside the dispatch. */
191
+ readonly credential: () => ActiveGatewayCredential;
192
+ readonly fetch?: GatewayFetch;
193
+ readonly loadAiSdk?: SceneAiSdkLoader;
194
+ readonly timeoutMs?: number;
195
+ /** Fresh catalog transport; the review dispatch performs one refresh. */
196
+ readonly transport: GatewayMediaCatalogTransport;
197
+ }
198
+
199
+ function boundedTimeout(value: number | undefined): number {
200
+ const timeoutMs = value ?? DEFAULT_REVIEW_GATEWAY_TIMEOUT_MS;
201
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) {
202
+ throw new SpatialReviewProviderError("invalid-request");
203
+ }
204
+ return timeoutMs;
205
+ }
206
+
207
+ function disableAiSdkWarningLogging(): void {
208
+ (
209
+ globalThis as typeof globalThis & {
210
+ AI_SDK_LOG_WARNINGS?: false;
211
+ }
212
+ ).AI_SDK_LOG_WARNINGS = false;
213
+ }
214
+
215
+ function assertReviewDispatchActive(signal: AbortSignal): void {
216
+ if (signal.aborted) throw new SpatialReviewProviderError("aborted");
217
+ }
218
+
219
+ /**
220
+ * One consent-gated critique call against the fixed Gateway origin. The
221
+ * credential is loaded lazily after the fresh catalog proves a vision-capable
222
+ * language model; `maxRetries` is pinned to zero and an ambiguous dispatch is
223
+ * never resubmitted.
224
+ */
225
+ export function createGatewaySpatialReviewProvider(
226
+ options: GatewaySpatialReviewProviderOptions,
227
+ ): SpatialReviewProvider {
228
+ const loader = options.loadAiSdk ?? loadAiSdk;
229
+ const gatewayFetch = createFixedSceneGatewayFetch(options.fetch ?? globalThis.fetch);
230
+ const timeoutMs = boundedTimeout(options.timeoutMs);
231
+ return {
232
+ critique: async (requestInput, signal): Promise<SpatialReviewProviderResult> => {
233
+ const request = validateSpatialReviewProviderRequest(requestInput);
234
+ let dispatched = false;
235
+ let timedOut = false;
236
+ const controller = new AbortController();
237
+ const abortFromCaller = (): void => controller.abort(signal?.reason);
238
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
239
+ if (signal?.aborted === true) {
240
+ signal?.removeEventListener("abort", abortFromCaller);
241
+ throw new SpatialReviewProviderError("aborted");
242
+ }
243
+ const timer = setTimeout(() => {
244
+ timedOut = true;
245
+ controller.abort();
246
+ }, timeoutMs);
247
+ const interrupted = new Promise<never>((_resolve, reject) => {
248
+ const rejectInterrupted = (): void => reject(
249
+ dispatched
250
+ ? new SpatialReviewProviderError(
251
+ timedOut ? "gateway-outcome-unknown" : "aborted",
252
+ "ambiguous",
253
+ )
254
+ : new SpatialReviewProviderError(
255
+ timedOut ? "gateway-unavailable" : "aborted",
256
+ ),
257
+ );
258
+ controller.signal.addEventListener("abort", rejectInterrupted, { once: true });
259
+ if (controller.signal.aborted) rejectInterrupted();
260
+ });
261
+ try {
262
+ const operation = (async (): Promise<SpatialReviewProviderResult> => {
263
+ const refresh = await options.transport.refresh(undefined, controller.signal);
264
+ assertReviewDispatchActive(controller.signal);
265
+ if (refresh.status !== "modified") {
266
+ throw new SpatialReviewProviderError("gateway-unavailable");
267
+ }
268
+ const selection = selectSpatialReviewModel(refresh.payload);
269
+ disableAiSdkWarningLogging();
270
+ const ai = await loader();
271
+ assertReviewDispatchActive(controller.signal);
272
+ let credential: ActiveGatewayCredential;
273
+ try {
274
+ credential = options.credential();
275
+ } catch {
276
+ throw new SpatialReviewProviderError("credential-missing");
277
+ }
278
+ return await credential.withApiKey(async (apiKey) => {
279
+ assertReviewDispatchActive(controller.signal);
280
+ const gateway = ai.createGateway({
281
+ apiKey,
282
+ baseURL: VERCEL_SCENE_GATEWAY_BASE_URL,
283
+ fetch: gatewayFetch,
284
+ });
285
+ const output = ai.Output.object({
286
+ description: "Bounded advisory beauty-pass findings for one spatial scene review.",
287
+ name: "slopcamera_spatial_review",
288
+ schema: SpatialReviewModelOutputSchema,
289
+ });
290
+ assertReviewDispatchActive(controller.signal);
291
+ dispatched = true;
292
+ const result: AiSdkResult = await ai.generateText({
293
+ abortSignal: controller.signal,
294
+ maxOutputTokens: SPATIAL_REVIEW_LIMITS.responseTokens,
295
+ maxRetries: 0,
296
+ messages: [userMessage(request)],
297
+ model: gateway.languageModel(selection.modelId),
298
+ output,
299
+ providerOptions: {
300
+ gateway: {
301
+ disallowPromptTraining: true,
302
+ tags: ["slopcamera", "scene-review", "v1"],
303
+ zeroDataRetention: true,
304
+ },
305
+ },
306
+ system: SPATIAL_REVIEW_PROMPT,
307
+ temperature: 0,
308
+ });
309
+ const responseId = providerResponseId(result.response);
310
+ return {
311
+ model: {
312
+ catalogSha256: selection.catalogSha256,
313
+ requestedModel: selection.modelId,
314
+ resolvedModel: resolvedModel(result.response),
315
+ ...(responseId === undefined ? {} : { providerResponseId: responseId }),
316
+ },
317
+ output: parseReviewProviderOutput(result.output),
318
+ usage: {
319
+ inputTokens: tokenCount(result.usage, "inputTokens"),
320
+ outputTokens: tokenCount(result.usage, "outputTokens"),
321
+ },
322
+ };
323
+ });
324
+ })();
325
+ return await Promise.race([operation, interrupted]);
326
+ } catch (error) {
327
+ throw redactedSpatialReviewProviderError(error, dispatched);
328
+ } finally {
329
+ clearTimeout(timer);
330
+ signal?.removeEventListener("abort", abortFromCaller);
331
+ }
332
+ },
333
+ };
334
+ }
@@ -8,15 +8,19 @@ const GLOBAL_HELP = `slopcamera — creative scenes and non-destructive media ed
8
8
  Usage: slopcamera <command> [options]
9
9
 
10
10
  Commands:
11
+ capabilities Discover the exact static capability modules and qualification status
11
12
  operations list|show Discover host-owned typed operations and policies
12
13
  diagram init|check|render Create, validate, or render portable diagram sources
13
- scene init|check|inspect|patch|evaluate|camera-track|plan|render
14
+ scene init|check|inspect|diff|patch|evaluate|audit|render-audit|solve|review|camera-track|generate|plan|render|direction|effects|behavior|temporal-audit
14
15
  Author and inspect editable directed 3D scene sources
16
+ scene design catalog|init|inspect|set|compile|gallery
17
+ Build and revise geometry from named parameters and constraints
15
18
  direct init|plan|start|generate|review|assemble
16
19
  Direct short Gateway clips with retained takes and budgets
17
20
  studio init|bundle|plan|probe|run|encode|asset|assemble|inspect|reconcile
18
21
  Author and render retained Blender, CAD and Manim productions
19
- image vectorize|generate Create a local SVG or generated image file
22
+ image vectorize|generate|gallery|icon
23
+ Create local SVGs, generated images, or review galleries
20
24
  html catalog|scaffold|render Author HTML scenes and export video with local audio
21
25
  workflows list|show|plan|run Plan or run a reviewed reusable workflow
22
26
  code init|check|plan|run Author, preflight, and run trusted TypeScript workflows
@@ -53,6 +57,9 @@ HRANESS_SUPPORT_AUDIENCE=off suppresses offers and incidental discovery.
53
57
  Run slopcamera help <command> for command-specific help.`;
54
58
 
55
59
  const HELP: Readonly<Record<string, string>> = {
60
+ capabilities: `Usage: slopcamera capabilities [--json]
61
+
62
+ Print the exact statically assembled capability manifest for this CLI build. Each module owns named operation, workflow, MCP-tool, command, and runtime-profile identities. The manifest includes trust classes, resource/effect policies, runtime requirements, qualification status, and a canonical SHA-256. It reads no workspace source, loads no plugin, probes no runtime, and grants no authority. Use doctor separately to inspect this machine's currently available executables.`,
56
63
  studio: `Usage:
57
64
  slopcamera studio init <new-directory> [--template blender-product|blender-character|blender-shaded-street|blender-cloth|blender-fluid|cadquery-bracket|manim-lesson] [--json]
58
65
  slopcamera studio bundle <source.json> [--source-root <directory>] [--json]
@@ -119,27 +126,120 @@ project for slopcamera project render run. Review and assembly work without clou
119
126
  This workflow retains image and prompt continuity; it has no provider neural checkpoint
120
127
  or real-time session. Use slopcamera ai models list --type video for live model discovery.`,
121
128
  scene: `Usage:
129
+ slopcamera scene design catalog [--json]
130
+ slopcamera scene design init <directory> --template <id> [--json]
131
+ slopcamera scene design inspect <design.json> [--json]
132
+ slopcamera scene design set <design.json> --parameters <values.json> --output <new-design.json> [--json]
133
+ slopcamera scene design compile <design.json> [--scene <base.scene.json>] --output-dir <new-directory> [--json]
134
+ slopcamera scene design gallery <design.json> --variants <variants.json> [--scene <base.scene.json>] --output-dir <new-directory> [--json]
122
135
  slopcamera scene init <scene.json> [--json]
123
136
  slopcamera scene check <scene.json> [--json]
124
137
  slopcamera scene inspect <scene.json> [--json]
125
138
  slopcamera scene patch <scene.json> --patch <patch.json> --output <new-scene.json> [--json]
126
139
  slopcamera scene evaluate <scene.json> --camera <camera-id> --time-us <integer> [--json]
140
+ slopcamera scene audit <scene.json> --camera <camera-id> [--times-us <csv>] [--asset-bounds <bounds-or-admission.json>] [--json]
141
+ slopcamera scene render-audit <scene.json> --camera <camera-id> [--times-us <csv>] [--json]
142
+ slopcamera scene solve <scene.json> --goals <goals.json> [--output <patch.json>] [--asset-bounds <bounds-or-admission.json>] [--json]
143
+ slopcamera scene review <scene.json> --camera <camera-id> [--times-us <csv>] --allow-cloud-upload [--json]
127
144
  slopcamera scene camera-track <scene.json> --request <sampling.json> --output <new-track.json> [--json]
145
+ slopcamera scene generate --module <generator.ts> --generator-id <id>
146
+ [--parameters <params.json>] [--seed <n>] [--into <scene.json>] --output <new-scene.json> [--json]
128
147
  slopcamera scene plan|render <scene.json> --request <request.json> [--assets <bindings.json>] [--profile <profile>] [--json]
148
+ slopcamera scene direction check <direction.json> --scene <scene.json> [--json]
149
+ slopcamera scene direction plan <direction.json> --scene <scene.json> [--camera <camera-id>] [--output <out.json>] [--json]
150
+ slopcamera scene direction gallery <direction.json> --scene <scene.json> --axis <axis> [--camera <camera-id>] [--output <out.json>] [--json]
151
+ slopcamera scene effects check <effects.json> --scene <scene.json> [--json]
152
+ slopcamera scene effects plan <draft.json> --scene <scene.json> [--output <effects.json>] [--json]
153
+ slopcamera scene effects bake <plan.json> --scene <scene.json> --output <bake.json> [--json]
154
+ slopcamera scene behavior check <behavior.json> --scene <scene.json> [--json]
155
+ slopcamera scene behavior bake <behavior.json> --scene <scene.json> [--channel-map <map.json>] --output <bake.json> [--json]
156
+ slopcamera scene behavior gallery <behavior.json> --scene <scene.json> [--channel-map <map.json>] [--output <gallery.json>] [--json]
157
+ slopcamera scene behavior audit <bake.json> [--output <audit.json>] [--json]
158
+ slopcamera scene temporal-audit <scene.json> --camera <camera-id> [--times-us <csv>] [--contacts <contacts.json>] [--cut-before-us <csv>] [--json]
129
159
  slopcamera scene project snapshot <project-id> [--json]
130
160
  slopcamera scene project prepare-render <project-id> --input <request.json> --output <prepared-render.json> [--profile <profile>] [--json]
131
161
  slopcamera scene project migrate|patch|restore|add-shot|add-candidate|select-candidate|reconcile
132
162
  <project-id> --input <request.json> [--json]
133
163
  slopcamera scene world import --input <import.json> --source-root <directory>
134
164
  --output-root <directory-below-artifacts/slopcamera/generated> [--json]
165
+ slopcamera scene asset admit <model.glb> --output <manifest.json>
166
+ [--source-root <directory>] [--asset-id <id>]
167
+ [--meters-per-unit <n>] [--source-up x|y|z] [--json]
168
+
169
+ Designs retain named numeric controls, derived values, dimensional constraints and
170
+ geometry stages. Inspect resolves dependencies and checks budgets without creating
171
+ meshes. Set applies a bounded JSON object of parameter values as a new revision.
172
+ Compile creates a new directory containing design.json, scene.json, exact GLB/facts
173
+ payloads, a compilation receipt and render.json when a camera is available. Pass
174
+ the starter's base.scene.json to retain its cameras and lighting. Gallery compiles
175
+ one to six labelled parameter variants into independent bundles; it does not render.
176
+ Render each bundle through scene render <bundle/scene.json> --request <bundle/render.json>.
177
+ Compilation is local and executes no authored code. Existing output paths conflict;
178
+ interrupted bundles remain without a completion receipt. Use scene design catalog
179
+ for the current architectural starters and docs/parametric-design.md for the contract.
135
180
 
136
181
  Hardware profiles: three-webgl2-hardware-v1 and three-spark-webgl2-hardware-v1.
137
182
  An explicit profile must agree with the request; omitting it preserves the request.
138
183
  Saved-world import works offline and preserves imported provenance receipts.
184
+ Asset admit copies one local GLB into content-addressed storage beside --output,
185
+ derives bounds and a mesh entity, and emits manifests plus ready-to-apply patch
186
+ operations without overwriting existing paths.
139
187
  Scene sources retain stable entities, cameras, asset manifests and animation channels.
140
188
  Inspect reports editable controls and known bounds without decoding assets. Patch requires
141
189
  the exact expected scene digest in its patch document and writes a new source without
142
190
  overwriting either revision. Evaluate samples absolute time without launching a renderer.
191
+ Audit samples bounded geometry against one camera over time and reports deterministic
192
+ frustum findings; --asset-bounds supplies scene-space enclosures as a bounds map,
193
+ admission documents, {manifest, facts} pairs, or arrays of those.
194
+ Render-audit renders the real object-ID pass in the bound browser runtime and counts
195
+ per-entity pixels at each sampled time, reporting splats and camera-bound view masks
196
+ honestly as unsupported or non-attributable rather than estimating them.
197
+ Solve turns declarative relation goals (onTopOf, nextTo, facing, align, at, groundSnap)
198
+ into concrete transforms and a ready-to-apply patch document. Goals name scene entities;
199
+ relations may target entities, cameras, or caller-supplied base entries, and goal targets
200
+ see each other's solved transforms in dependency order. The emitted patch carries a
201
+ placeholder expectedSceneSha256 — copy the digest from scene inspect before scene patch.
202
+ Goal entities must be authored, unparented, world-placed, and free of transform
203
+ overrides or transform channels; asset-derived bounds come from --asset-bounds.
204
+ Review renders up to four bounded beauty frames locally, uploads only those exact
205
+ frames to a vision-capable Gateway language model after explicit --allow-cloud-upload,
206
+ and returns a structured advisory report. It makes exactly one Gateway call, records
207
+ frame digests, model and attempt identity, and rubric version, and never modifies or
208
+ auto-applies scene changes. The report is model-generated and unverified.
209
+ Generate runs a trusted TypeScript generator at authoring time, stamps retained
210
+ output with derived entity identity, and records source, closure, parameters, seed and
211
+ runtime digests; it never reruns source during inspect, evaluate or render.
212
+ Direction commands compile an authored direction document into deterministic,
213
+ unverified proposals — performance actions, camera rigs, shots and looks bound to the
214
+ actual scene digest. Check reports stale digests, unresolved references and intervals
215
+ outside scene duration without compiling; plan emits the compilation (optionally to
216
+ --output); gallery compiles a bounded set of single-axis variants as separate
217
+ content-addressed candidates and never selects one. Direction gallery axes:
218
+ performance, camera, lighting, materials, effects, sequence.
219
+ Effects commands assemble and validate slopcamera.spatial-render-effects documents.
220
+ Check verifies scene-digest bindings, entity and asset closure, and nested plan,
221
+ particle and simulation digests before any host work; plan binds the caller's
222
+ renderPlan, particleSystems and simulationBakes draft to the actual scene digest and
223
+ emits a {document, documentSha256} binding; bake executes the bounded reference
224
+ rigid-body integrator for one admitted simulation plan and publishes the canonical
225
+ bake document. Native secondary-motion and hinge constraints remain unqualified.
226
+ Behavior commands admit and execute slopcamera.spatial-behavior documents —
227
+ inert ALGAL organism closures checked under the bake-safe profile (pure
228
+ input/const/fn/repeat/each/organism cells only; effect-bearing cells reject
229
+ before execution). Check reports manifest digests, wiring, entity and channel
230
+ findings; bake runs the entry organism through the pinned ALGAL runtime with
231
+ the closed pure fn catalog and publishes a canonical bake document — emitted
232
+ semantic records, unverified performance directive proposals, and a receipt
233
+ binding closure, scene, seed, run, and catalog digests; gallery bakes the
234
+ document under deterministic seed variants as separate content-addressed
235
+ candidates and never selects one. --channel-map binds emitted channels to
236
+ clip runs and attach/release pairs; unmapped channels surface as unresolved
237
+ intents rather than fabricated bindings.
238
+ Temporal-audit evaluates the scene at bounded sample times under one camera and
239
+ reports deterministic findings: visibility flicker, transform discontinuity,
240
+ foot-slide against declared contact windows, camera acceleration/jerk/angular
241
+ velocity, exposure and focus jumps, and per-sample resource spikes. Without
242
+ --times-us it samples the scene duration at up to 17 evenly spaced times.
143
243
  Project operations use the exact full project ID and a versioned whole-project basis.
144
244
  Discover each request with slopcamera operations show spatial.project.<action> --json.
145
245
  Migration retains the frozen media/edit pair in V2 authority. Legacy project commands
@@ -161,11 +261,26 @@ operations separately publish equivalent derivatives by content hash for workflo
161
261
  slopcamera image generate <prompt> --output <file.webp> [--model <model>]
162
262
  [--idempotency-key <key>] [--json]
163
263
  slopcamera image generate --model <gateway-model> --prompt <prompt>
264
+ slopcamera image gallery <subject> --output-dir <directory> [--model <model>]
265
+ [--kind <image|texture|skybox|backdrop|sprite>] [--count <n>]
266
+ [--vary <axis[=v1,v2][;axis...]>] [--candidates <file.json>]
267
+ [--cell <n>] [--tile|--no-tile] [--json]
268
+ slopcamera image icon <subject> --output <file.svg>
269
+ [--purpose <mark|illustration>] [--model <model>] [--ink <#rgb>]
270
+ [--rounds <1-4>] [--critique-model <model>] [--keep-raster] [--json]
164
271
 
165
272
  Explicit --output file commands delegate to @hraness/slopcamera. Vectorization is local,
166
273
  bounded, checksum-pinned, and emits inert SVG. File generation uses Vercel AI Gateway with the
167
274
  caller's environment credential. The --prompt spelling without --output is an alias for the desktop
168
- content-addressed \`ai image generate\` lane and returns project-composable content hash references.`,
275
+ content-addressed \`ai image generate\` lane and returns project-composable content hash references.
276
+ Icon combines a style-locked Gateway raster, local ink extraction and VTracer tracing into a
277
+ canonical isometric line-art SVG; --rounds above 1 adds a vision-model critique that revises the
278
+ prompt between attempts. Gallery fans one subject out to bounded parallel candidates — driven by
279
+ variation axes or an explicit candidate list — keeps every candidate with its provenance, and
280
+ composes a labelled contact sheet plus receipt for agent review. Texture cells repeat the
281
+ candidate 2×2 so seams are inspectable (--tile/--no-tile overrides). It never replaces existing
282
+ outputs and never promotes a candidate into authored source; review the sheet and select
283
+ explicitly.`,
169
284
  html: `Usage:
170
285
  slopcamera html catalog [--json]
171
286
  slopcamera html scaffold <${HTML_OVERLAY_SCAFFOLD_KIND_HELP}> --output <file.html>
@@ -254,6 +369,13 @@ run journal.`,
254
369
  [--max-output-tokens <n>] [--temperature <n>] [--stop <text> ...]
255
370
  [--provider-options <json-file>] [--timeout <time>]
256
371
  [--allow-cloud-upload] [--json]
372
+ slopcamera ai image gallery <subject> --model <id> --output-dir <directory>
373
+ [--kind <image|texture|skybox|backdrop|sprite>] [--count <n>]
374
+ [--vary <axis[=v1,v2][;axis...]>] [--candidates <json-file>]
375
+ [--cell <n>] [--tile|--no-tile] [--preview probe]
376
+ [--timeout <time>] [--json]
377
+ slopcamera ai scene gallery <scene.json> --variants <file.json> --output-dir <directory>
378
+ [--camera <camera-id>] [--time-us <integer>] [--cell <n>] [--json]
257
379
  slopcamera ai video generate --model <id> [--prompt <text> | --prompt-file <path>]
258
380
  [--image <path-or-https-url>] [--frame first=<path-or-https-url>]
259
381
  [--frame last=<path-or-https-url>]
@@ -295,6 +417,25 @@ image cannot accompany a first frame.
295
417
  provider-options inspect emits exactly the secret-free {sha256,namespaces} reference that workflow
296
418
  nodes store in their exact request; it never prints or persists the raw option values.
297
419
 
420
+ ai image gallery runs one tracked paid job per candidate with bounded parallelism, then composes a
421
+ labelled contact sheet and receipt into --output-dir. Each candidate keeps its durable artifact,
422
+ prompt, digest, and job record so an agent can review the sheet once and promote a chosen candidate
423
+ explicitly; generated bytes never replace authored source. --vary takes automatic variation axes,
424
+ --candidates takes an explicit JSON list, and the two are mutually exclusive. Texture cells render
425
+ as a 2×2 tile repeat so seams are visible at a glance; --no-tile keeps a flat cell and --tile opts
426
+ other kinds in. --preview probe renders each settled candidate inside the fixed probe scene —
427
+ textures applied to lit geometry, skyboxes as the environment, backdrops as a scene surface — so the
428
+ sheet shows rendered stills. The receipt keeps the generated candidate path/digest and the rendered
429
+ cellImage path/digest as separate fields; promotion always targets the candidate, never the still.
430
+
431
+ ai scene gallery reads an authored scene plus a bounded variants file of typed scene patches, then
432
+ renders each derived scene through the qualified scene renderer and composes the beauty stills into
433
+ the same labelled contact sheet. Every variant is published as a new scene document under
434
+ --output-dir/variants/<id>/ with its asset payloads staged alongside, so the derived scene is
435
+ inspectable and re-renderable; the authored scene is never modified. The receipt keeps the base
436
+ scene digest, each patch digest, each derived scene digest, and each rendered still digest so an
437
+ agent can review once and promote a chosen variant explicitly.
438
+
298
439
  Reference media and transcription audio never upload implicitly. The appropriate explicit
299
440
  --allow-cloud-* acknowledgement is required after local files pass type and byte bounds. Image
300
441
  and video references may instead use a public credential-free HTTPS URL when the live catalog
@@ -461,6 +602,23 @@ Face track IDs describe local geometry continuity inside one immutable analysis.
461
602
  slopcamera project edit <project> overlay remove <id> [--json]
462
603
  slopcamera project render <plan|run> <project> [--width <px>] [--height <px>] [--fps <n>]
463
604
  [--output <renders/path.mp4>] [--dry-run] [--allow-unverified-sync] [--json]
605
+ slopcamera project cinema init <project> [--force] [--json]
606
+ slopcamera project cinema check <project> [--json]
607
+ slopcamera project cinema plan <project> [--allow-placeholders] [--json]
608
+ slopcamera project cinema audit <project> [--json]
609
+ slopcamera project cinema gallery <project> --axis <audio|looks|mixed|pacing|structure|transitions>
610
+ [--output <cinema/path.json>] [--json]
611
+ slopcamera project cinema animatic <project> [--output <renders/path.mp4>] [--dry-run] [--json]
612
+ slopcamera project cinema run <project> [--output <renders/path.mp4>] [--allow-placeholders] [--dry-run] [--json]
613
+
614
+ Cinema commands drive the content-addressed cinema sidecar (cinema/current.json), which binds exact
615
+ project structure and edit-plan digests. Any project or edit change makes the sidecar stale until it is
616
+ re-authorized. Animatics always allow deterministic placeholder frames for missing spatial artifacts;
617
+ final runs require materialized media unless --allow-placeholders is explicit. Cinema never alters the
618
+ project render plan or its identity. Audit reports layout-derived temporal evidence — pacing floors,
619
+ transition overruns, cue concurrency and uniform pacing — with deterministic shot metrics. Gallery
620
+ compiles a bounded set of single-axis candidate plans under cinema/galleries/, each re-validated and
621
+ re-hashed through the canonical composition digest; it never selects a candidate.
464
622
 
465
623
  Imported media starts unverified. Align its audio before relying on synchronization. Structural edits are project-time operations and affect every placement. Camera moves address any placed video stream; metadata-driven screen zooms still require a recording-backed source placement. Face analysis and geometry tracks remain local. --select largest follows the largest currently visible prepared-layer face per frame. --require-all-selected makes a missing explicit/all-selected face invoke the chosen gap policy.`,
466
624
  render: `Usage: slopcamera render <plan|run> <recording> [--display <id|primary>] [--output <path>] [--dry-run] [--keep-inactivity] [--json]
@@ -475,7 +633,7 @@ export function commandHelp(topic: readonly string[]): string {
475
633
 
476
634
  export function completions(words: readonly string[]): readonly string[] {
477
635
  const topLevel = [
478
- "operations", "diagram", "direct", "studio", "image", "html", "workflows", "code", "runs", "doctor", "ai", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
636
+ "capabilities", "operations", "diagram", "direct", "studio", "image", "html", "workflows", "code", "runs", "doctor", "ai", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
479
637
  ];
480
638
  if (words.length <= 1) return topLevel;
481
639
  const command = words[0];
@@ -485,13 +643,21 @@ export function completions(words: readonly string[]): readonly string[] {
485
643
  if (command === "studio") return words[2] === "assets" || words[1] === "assets" ? ["search", "describe", "plan", "import"] : ["init", "bundle", "plan", "probe", "run", "encode", "asset", "assemble", "inspect", "reconcile", "assets"];
486
644
  if (command === "operations") return ["list", "show"];
487
645
  if (command === "diagram") return ["check", "render"];
488
- if (command === "image") return ["vectorize"];
646
+ if (command === "image") return ["vectorize", "generate", "gallery", "icon"];
489
647
  if (command === "workflows") return ["list", "show", "plan", "run"];
490
648
  if (command === "code") return ["init", "check", "plan", "run"];
491
649
  if (command === "runs") return ["list", "show", "resume", "approve", "cancel"];
492
650
  if (command === "recordings") return ["list"];
493
651
  if (command === "projects") return ["list", "create"];
494
- if (command === "project") return ["inspect", "add", "edit", "render"];
652
+ if (command === "project") return words[1] === "cinema"
653
+ ? ["init", "check", "plan", "audit", "gallery", "animatic", "run"]
654
+ : ["inspect", "add", "edit", "render", "cinema"];
655
+ if (command === "scene") {
656
+ if (words[1] === "direction") return ["check", "plan", "gallery"];
657
+ if (words[1] === "effects") return ["check", "plan", "bake"];
658
+ if (words[1] === "behavior") return ["check", "bake", "gallery", "audit"];
659
+ return ["init", "check", "inspect", "diff", "patch", "evaluate", "audit", "render-audit", "solve", "review", "camera-track", "generate", "plan", "render", "direction", "effects", "behavior", "temporal-audit"];
660
+ }
495
661
  if (command === "edit") return ["init", "show", "trim", "cut", "speed", "zoom", "overlay", "cursor", "clicks", "keystrokes", "typed-text"];
496
662
  if (command === "analyze") return ["faces", "inactivity", "zooms", "music", "scenes", "speech"];
497
663
  if (command === "faces") return ["list"];
@@ -46,7 +46,7 @@ export async function main(
46
46
  const portableExitCode = await runPortableSurface(unifiedArgv, portableDependencies);
47
47
  if (portableExitCode !== undefined) return portableExitCode;
48
48
  const earlyCommand = parseCliArgs(unifiedArgv);
49
- if (earlyCommand.kind === "help" || earlyCommand.kind === "version" || earlyCommand.kind === "complete") {
49
+ if (earlyCommand.kind === "help" || earlyCommand.kind === "version" || earlyCommand.kind === "complete" || earlyCommand.kind === "capabilities") {
50
50
  const intro = rootHelpIntro(unifiedArgv, {
51
51
  isTTY: process.stdout.isTTY === true, term: process.env.TERM, columns: process.stdout.columns,
52
52
  });
@@ -54,8 +54,8 @@ export async function main(
54
54
  return await runCli(unifiedArgv, { io: processIo });
55
55
  }
56
56
  const paths = await resolveRepositoryPaths(processIo.cwd(), processIo.env);
57
- if (earlyCommand.kind === "html-render" || earlyCommand.kind === "spatial-world" || earlyCommand.kind === "directing" || earlyCommand.kind === "studio"
58
- || earlyCommand.kind === "spatial-scene" && earlyCommand.action === "camera-track") {
57
+ if (earlyCommand.kind === "html-render" || earlyCommand.kind === "spatial-world" || earlyCommand.kind === "spatial-asset" || earlyCommand.kind === "spatial-design" || earlyCommand.kind === "directing" || earlyCommand.kind === "studio"
58
+ || earlyCommand.kind === "spatial-scene" && (earlyCommand.action === "camera-track" || earlyCommand.action === "render-audit" || earlyCommand.action === "review")) {
59
59
  const controller = new AbortController();
60
60
  const cancel = () => controller.abort(new CliError("cancelled", "Command interrupted."));
61
61
  process.on("SIGINT", cancel);
@@ -235,7 +235,12 @@ export async function runPortableSurface(
235
235
  )
236
236
  || (
237
237
  argv[0] === "image"
238
- && (argv[1] === "vectorize" || argv[1] === "generate")
238
+ && (
239
+ argv[1] === "vectorize" ||
240
+ argv[1] === "generate" ||
241
+ argv[1] === "icon" ||
242
+ argv[1] === "gallery"
243
+ )
239
244
  );
240
245
  if (!delegatesToHeadless) return undefined;
241
246