@hraness/slopcamera 3.2.8 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/apps/desktop/application/context.ts +6 -0
- package/apps/desktop/application/default-registry.ts +26 -0
- package/apps/desktop/application/operation.ts +14 -1
- package/apps/desktop/application/operations/index.ts +4 -0
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +514 -11
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +29 -1
- package/apps/desktop/cli/commands.ts +1188 -4
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +172 -6
- package/apps/desktop/cli/main.ts +3 -3
- package/apps/desktop/cli/portable-surface.ts +6 -1
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +271 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/semantic-builder.ts +24 -0
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +725 -273
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +25 -429
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-gs48afpt.js +7 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index-zhg36tf9.js +3 -0
- package/dist/index.js +1 -226
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +12 -0
- package/package.json +7 -5
- package/skills/slopcamera/SKILL.md +7 -1
- package/skills/slopcamera/references/directed-scenes.md +48 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +4 -0
- package/skills/slopcamera/references/native-studio.md +19 -0
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +172 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +222 -4
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -0
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1649 -132
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +232 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +108 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +617 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/version.ts +1 -1
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7131pg1c.js +0 -1820
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-fava6pge.js +0 -192
- package/dist/index-h1k0fnjq.js +0 -2363
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-zfnddgay.js +0 -2029
- 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 (
|
|
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
|
|
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
|
|
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 =
|
|
591
|
+
const credential = resolveSlopcameraGatewayCredential(dependencies.environment)
|
|
592
592
|
const timeout = combineSignals(input.signal, validateTimeout(input.timeoutMs))
|
|
593
593
|
try {
|
|
594
594
|
const generation = (async () => {
|