@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.
- 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 +268 -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-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -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/package.json +7 -5
- package/skills/slopcamera/SKILL.md +6 -1
- package/skills/slopcamera/references/directed-scenes.md +22 -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/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +32 -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 +1642 -130
- 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 +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -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 +618 -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
package/dist/index-7131pg1c.js
DELETED
|
@@ -1,1820 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
bundledSkillPath,
|
|
4
|
-
installSkill,
|
|
5
|
-
pathExists
|
|
6
|
-
} from "./index-7308egqr.js";
|
|
7
|
-
import {
|
|
8
|
-
SlopcameraCodeError,
|
|
9
|
-
boundedCanonicalJsonSha256,
|
|
10
|
-
canonicalJson,
|
|
11
|
-
compareUtf16Strings,
|
|
12
|
-
createBoundedJsonValueSnapshot,
|
|
13
|
-
deepFreezeJson
|
|
14
|
-
} from "./index-8txs6fkn.js";
|
|
15
|
-
import {
|
|
16
|
-
SlopcameraWorkflowError,
|
|
17
|
-
defineSlopcameraWorkflow,
|
|
18
|
-
runSlopcameraWorkflow
|
|
19
|
-
} from "./index-fava6pge.js";
|
|
20
|
-
import {
|
|
21
|
-
DiagramValidationError,
|
|
22
|
-
SlopcameraOperationError,
|
|
23
|
-
StackLayoutError,
|
|
24
|
-
builtInIcons,
|
|
25
|
-
executeSlopcameraOperation,
|
|
26
|
-
lintDiagram,
|
|
27
|
-
parseDiagramSource,
|
|
28
|
-
parseDiagramSpec,
|
|
29
|
-
parseSlopcameraOperationInput,
|
|
30
|
-
renderPng,
|
|
31
|
-
renderSvg,
|
|
32
|
-
resolveDiagramSource,
|
|
33
|
-
resolveEdge,
|
|
34
|
-
resolveStackLayout,
|
|
35
|
-
sanitizeIcon,
|
|
36
|
-
searchSlopcameraOperations,
|
|
37
|
-
serializeTldr,
|
|
38
|
-
slopcameraOperationCodes,
|
|
39
|
-
slopcameraOperationRegistry,
|
|
40
|
-
stackLayoutDefaults,
|
|
41
|
-
withSlopcameraOperationHostAdmission
|
|
42
|
-
} from "./index-h1k0fnjq.js";
|
|
43
|
-
import {
|
|
44
|
-
VectorizeError,
|
|
45
|
-
vectorizeHardLimits,
|
|
46
|
-
vectorizeImage
|
|
47
|
-
} from "./index-zfnddgay.js";
|
|
48
|
-
import {
|
|
49
|
-
SlopcameraCloudError,
|
|
50
|
-
generateSlopcameraImage,
|
|
51
|
-
generateSlopcameraImageFile,
|
|
52
|
-
slopcameraGatewayCredentialStatus
|
|
53
|
-
} from "./index-r7gdhmsp.js";
|
|
54
|
-
import {
|
|
55
|
-
createDefaultHostResourceCoordinator
|
|
56
|
-
} from "./index-sh6xbav6.js";
|
|
57
|
-
|
|
58
|
-
// src/artifacts.ts
|
|
59
|
-
import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
60
|
-
import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
|
|
61
|
-
|
|
62
|
-
// src/config.ts
|
|
63
|
-
import { readFile } from "fs/promises";
|
|
64
|
-
import { dirname, extname, isAbsolute, resolve } from "path";
|
|
65
|
-
import { pathToFileURL } from "url";
|
|
66
|
-
var configNames = [
|
|
67
|
-
{ current: "slopcamera.config.ts", retired: "diagram.config.ts" },
|
|
68
|
-
{ current: "slopcamera.config.mjs", retired: "diagram.config.mjs" },
|
|
69
|
-
{ current: "slopcamera.config.js", retired: "diagram.config.js" },
|
|
70
|
-
{ current: "slopcamera.config.json", retired: "diagram.config.json" }
|
|
71
|
-
];
|
|
72
|
-
function isRecord(value) {
|
|
73
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
-
}
|
|
75
|
-
function parseFont(value, at) {
|
|
76
|
-
if (!isRecord(value) || typeof value.family !== "string" || value.family.trim() === "") {
|
|
77
|
-
throw new Error(`${at} must have a non-empty family`);
|
|
78
|
-
}
|
|
79
|
-
if (value.files !== undefined && !Array.isArray(value.files)) {
|
|
80
|
-
throw new Error(`${at}.files must be an array`);
|
|
81
|
-
}
|
|
82
|
-
if (value.monoFamily !== undefined && (typeof value.monoFamily !== "string" || value.monoFamily.trim() === "")) {
|
|
83
|
-
throw new Error(`${at}.monoFamily must be a non-empty string when present`);
|
|
84
|
-
}
|
|
85
|
-
const files = (value.files ?? []).map((file, index) => {
|
|
86
|
-
if (!isRecord(file) || typeof file.path !== "string" || file.path.trim() === "") {
|
|
87
|
-
throw new Error(`${at}.files[${index}].path must be a non-empty string`);
|
|
88
|
-
}
|
|
89
|
-
if (file.weight !== undefined && (typeof file.weight !== "number" || !Number.isFinite(file.weight))) {
|
|
90
|
-
throw new Error(`${at}.files[${index}].weight must be a finite number`);
|
|
91
|
-
}
|
|
92
|
-
if (file.style !== undefined && file.style !== "normal" && file.style !== "italic") {
|
|
93
|
-
throw new Error(`${at}.files[${index}].style must be normal or italic`);
|
|
94
|
-
}
|
|
95
|
-
if (file.embed !== undefined && typeof file.embed !== "boolean") {
|
|
96
|
-
throw new Error(`${at}.files[${index}].embed must be a boolean`);
|
|
97
|
-
}
|
|
98
|
-
const style = file.style;
|
|
99
|
-
return {
|
|
100
|
-
path: file.path,
|
|
101
|
-
...file.weight === undefined ? {} : { weight: file.weight },
|
|
102
|
-
...style === undefined ? {} : { style },
|
|
103
|
-
...file.embed === undefined ? {} : { embed: file.embed }
|
|
104
|
-
};
|
|
105
|
-
});
|
|
106
|
-
return {
|
|
107
|
-
family: value.family,
|
|
108
|
-
...value.monoFamily === undefined ? {} : { monoFamily: value.monoFamily },
|
|
109
|
-
...files.length === 0 ? {} : { files }
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function parseIcons(value, at) {
|
|
113
|
-
if (!isRecord(value))
|
|
114
|
-
throw new Error(`${at} must be an object`);
|
|
115
|
-
return Object.fromEntries(Object.entries(value).map(([name, icon]) => {
|
|
116
|
-
if (!isRecord(icon) || typeof icon.viewBox !== "string" || typeof icon.body !== "string") {
|
|
117
|
-
throw new Error(`${at}.${name} must have string viewBox and body fields`);
|
|
118
|
-
}
|
|
119
|
-
return [name, sanitizeIcon({ viewBox: icon.viewBox, body: icon.body })];
|
|
120
|
-
}));
|
|
121
|
-
}
|
|
122
|
-
function parseTheme(value, at) {
|
|
123
|
-
if (!isRecord(value))
|
|
124
|
-
throw new Error(`${at} must be an object`);
|
|
125
|
-
const scalarKeys = ["background", "foreground", "muted", "stroke"];
|
|
126
|
-
for (const key of scalarKeys) {
|
|
127
|
-
if (value[key] !== undefined && typeof value[key] !== "string") {
|
|
128
|
-
throw new Error(`${at}.${key} must be a CSS color string`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (value.tones !== undefined && !isRecord(value.tones)) {
|
|
132
|
-
throw new Error(`${at}.tones must be an object`);
|
|
133
|
-
}
|
|
134
|
-
return value;
|
|
135
|
-
}
|
|
136
|
-
function parseConfig(value) {
|
|
137
|
-
if (!isRecord(value))
|
|
138
|
-
throw new Error("Slopcamera config must export an object");
|
|
139
|
-
const font = value.font === undefined ? undefined : parseFont(value.font, "font");
|
|
140
|
-
const icons = value.icons === undefined ? undefined : parseIcons(value.icons, "icons");
|
|
141
|
-
let theme;
|
|
142
|
-
if (value.theme !== undefined) {
|
|
143
|
-
if (!isRecord(value.theme))
|
|
144
|
-
throw new Error("theme must be an object");
|
|
145
|
-
theme = {
|
|
146
|
-
...value.theme.light === undefined ? {} : { light: parseTheme(value.theme.light, "theme.light") },
|
|
147
|
-
...value.theme.dark === undefined ? {} : { dark: parseTheme(value.theme.dark, "theme.dark") }
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
...font === undefined ? {} : { font },
|
|
152
|
-
...icons === undefined ? {} : { icons: { ...builtInIcons, ...icons } },
|
|
153
|
-
...theme === undefined ? {} : { theme }
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
async function discoverConfig(directory) {
|
|
157
|
-
let current = null;
|
|
158
|
-
for (const names of configNames) {
|
|
159
|
-
const candidate = resolve(directory, names.current);
|
|
160
|
-
if (await pathExists(candidate)) {
|
|
161
|
-
current = candidate;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (current !== null)
|
|
166
|
-
return current;
|
|
167
|
-
for (const names of configNames) {
|
|
168
|
-
const candidate = resolve(directory, names.retired);
|
|
169
|
-
if (await pathExists(candidate)) {
|
|
170
|
-
const replacement = resolve(directory, names.current);
|
|
171
|
-
throw new Error(`Legacy Slopcamera config found at ${candidate}. Rename it to ${replacement}; Slopcamera does not auto-load diagram.config.*.`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
async function loadDiagramConfig(options) {
|
|
177
|
-
const filePath = options.explicitPath === undefined ? await discoverConfig(options.searchDirectory) : resolve(options.explicitPath);
|
|
178
|
-
if (filePath === null) {
|
|
179
|
-
return {
|
|
180
|
-
filePath: null,
|
|
181
|
-
baseDirectory: options.searchDirectory,
|
|
182
|
-
value: { icons: builtInIcons }
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (!await pathExists(filePath))
|
|
186
|
-
throw new Error(`Config does not exist: ${filePath}`);
|
|
187
|
-
const raw = extname(filePath) === ".json" ? JSON.parse(await readFile(filePath, "utf8")) : (await import(`${pathToFileURL(filePath).href}?v=${Date.now()}`)).default;
|
|
188
|
-
const value = parseConfig(raw);
|
|
189
|
-
const baseDirectory = dirname(filePath);
|
|
190
|
-
const font = value.font === undefined ? undefined : {
|
|
191
|
-
...value.font,
|
|
192
|
-
...value.font.files === undefined ? {} : {
|
|
193
|
-
files: value.font.files.map((file) => ({
|
|
194
|
-
...file,
|
|
195
|
-
path: isAbsolute(file.path) ? file.path : resolve(baseDirectory, file.path)
|
|
196
|
-
}))
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
return {
|
|
200
|
-
filePath,
|
|
201
|
-
baseDirectory,
|
|
202
|
-
value: {
|
|
203
|
-
...value,
|
|
204
|
-
icons: { ...builtInIcons, ...value.icons },
|
|
205
|
-
...font === undefined ? {} : { font }
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// src/artifacts.ts
|
|
211
|
-
async function atomicWrite(filePath, data) {
|
|
212
|
-
const temporary = `${filePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
213
|
-
try {
|
|
214
|
-
await writeFile(temporary, data);
|
|
215
|
-
await rename(temporary, filePath);
|
|
216
|
-
} catch (error) {
|
|
217
|
-
await rm(temporary, { force: true });
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
async function readDiagramFile(filePath) {
|
|
222
|
-
const absolutePath = resolve2(filePath);
|
|
223
|
-
let parsed;
|
|
224
|
-
try {
|
|
225
|
-
parsed = JSON.parse(await readFile2(absolutePath, "utf8"));
|
|
226
|
-
} catch (error) {
|
|
227
|
-
throw new Error(`Could not read diagram JSON at ${absolutePath}`, { cause: error });
|
|
228
|
-
}
|
|
229
|
-
return { absolutePath, spec: parseDiagramSpec(parsed) };
|
|
230
|
-
}
|
|
231
|
-
async function checkDiagramFile(options) {
|
|
232
|
-
const { absolutePath, spec } = await readDiagramFile(options.filePath);
|
|
233
|
-
const config = await loadDiagramConfig({
|
|
234
|
-
...options.configPath === undefined ? {} : { explicitPath: options.configPath },
|
|
235
|
-
searchDirectory: dirname2(absolutePath)
|
|
236
|
-
});
|
|
237
|
-
for (const shape of spec.shapes) {
|
|
238
|
-
if ((shape.type === "rect" || shape.type === "ellipse") && shape.icon !== undefined && config.value.icons?.[shape.icon] === undefined) {
|
|
239
|
-
throw new Error(`Unknown icon "${shape.icon}" on shape ${shape.id}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return { findings: lintDiagram(spec), configPath: config.filePath };
|
|
243
|
-
}
|
|
244
|
-
async function renderDiagramFile(options) {
|
|
245
|
-
const { absolutePath, spec } = await readDiagramFile(options.filePath);
|
|
246
|
-
const outDirectory = resolve2(options.outDirectory ?? dirname2(absolutePath));
|
|
247
|
-
const config = await loadDiagramConfig({
|
|
248
|
-
...options.configPath === undefined ? {} : { explicitPath: options.configPath },
|
|
249
|
-
searchDirectory: dirname2(absolutePath)
|
|
250
|
-
});
|
|
251
|
-
const scale = options.scale ?? 2;
|
|
252
|
-
if (!Number.isFinite(scale) || scale <= 0 || scale > 8) {
|
|
253
|
-
throw new Error("PNG scale must be greater than zero and no more than 8");
|
|
254
|
-
}
|
|
255
|
-
const [light, dark] = await Promise.all([
|
|
256
|
-
renderSvg(spec, "light", config.value),
|
|
257
|
-
renderSvg(spec, "dark", config.value)
|
|
258
|
-
]);
|
|
259
|
-
const [lightPng, darkPng] = [renderPng(light, config.value, scale), renderPng(dark, config.value, scale)];
|
|
260
|
-
const artifacts = {
|
|
261
|
-
spec: absolutePath,
|
|
262
|
-
tldr: join(outDirectory, `${spec.name}.tldr`),
|
|
263
|
-
lightSvg: join(outDirectory, `${spec.name}.light.svg`),
|
|
264
|
-
darkSvg: join(outDirectory, `${spec.name}.dark.svg`),
|
|
265
|
-
lightPng: join(outDirectory, `${spec.name}.light.png`),
|
|
266
|
-
darkPng: join(outDirectory, `${spec.name}.dark.png`)
|
|
267
|
-
};
|
|
268
|
-
await mkdir(outDirectory, { recursive: true });
|
|
269
|
-
await Promise.all([
|
|
270
|
-
atomicWrite(artifacts.tldr, serializeTldr(spec, config.value)),
|
|
271
|
-
atomicWrite(artifacts.lightSvg, light.svg),
|
|
272
|
-
atomicWrite(artifacts.darkSvg, dark.svg),
|
|
273
|
-
atomicWrite(artifacts.lightPng, lightPng),
|
|
274
|
-
atomicWrite(artifacts.darkPng, darkPng)
|
|
275
|
-
]);
|
|
276
|
-
return { artifacts, findings: lintDiagram(spec), configPath: config.filePath };
|
|
277
|
-
}
|
|
278
|
-
function artifactSummary(artifacts) {
|
|
279
|
-
return [
|
|
280
|
-
`Rendered ${basename(artifacts.spec)}`,
|
|
281
|
-
` ${artifacts.tldr}`,
|
|
282
|
-
` ${artifacts.lightSvg}`,
|
|
283
|
-
` ${artifacts.darkSvg}`,
|
|
284
|
-
` ${artifacts.lightPng}`,
|
|
285
|
-
` ${artifacts.darkPng}`
|
|
286
|
-
].join(`
|
|
287
|
-
`);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// src/mcp/tools.ts
|
|
291
|
-
import { rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
292
|
-
import { dirname as dirname4, join as join3 } from "path";
|
|
293
|
-
|
|
294
|
-
// src/mcp/boundary.ts
|
|
295
|
-
import { open, mkdir as mkdir2, realpath, stat } from "fs/promises";
|
|
296
|
-
import {
|
|
297
|
-
basename as basename2,
|
|
298
|
-
dirname as dirname3,
|
|
299
|
-
isAbsolute as isAbsolute2,
|
|
300
|
-
join as join2,
|
|
301
|
-
relative,
|
|
302
|
-
resolve as resolve3,
|
|
303
|
-
win32
|
|
304
|
-
} from "path";
|
|
305
|
-
var mcpSourceByteLimit = 1024 * 1024;
|
|
306
|
-
|
|
307
|
-
class WorkspaceBoundaryError extends Error {
|
|
308
|
-
code;
|
|
309
|
-
constructor(code, message) {
|
|
310
|
-
super(message);
|
|
311
|
-
this.name = "WorkspaceBoundaryError";
|
|
312
|
-
this.code = code;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function filesystemCode(error) {
|
|
316
|
-
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
|
|
317
|
-
return error.code;
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
function normalizeRelativePath(value, options) {
|
|
322
|
-
if (value.length === 0 || value.includes("\x00")) {
|
|
323
|
-
throw new WorkspaceBoundaryError("INVALID_PATH", "Path must be a non-empty root-relative path.");
|
|
324
|
-
}
|
|
325
|
-
if (isAbsolute2(value) || win32.isAbsolute(value) || /^[A-Za-z]:/.test(value)) {
|
|
326
|
-
throw new WorkspaceBoundaryError("INVALID_PATH", "Absolute paths are not allowed.");
|
|
327
|
-
}
|
|
328
|
-
const segments = value.split(/[\\/]/).filter((segment) => segment !== "" && segment !== ".");
|
|
329
|
-
if (segments.includes("..")) {
|
|
330
|
-
throw new WorkspaceBoundaryError("INVALID_PATH", "Parent-directory traversal is not allowed.");
|
|
331
|
-
}
|
|
332
|
-
if (segments.length === 0) {
|
|
333
|
-
if (!options.allowRoot) {
|
|
334
|
-
throw new WorkspaceBoundaryError("INVALID_PATH", "Path must identify a file below the root.");
|
|
335
|
-
}
|
|
336
|
-
return { native: ".", portable: "." };
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
native: segments.join("/"),
|
|
340
|
-
portable: segments.join("/")
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function isConfined(rootDirectory, target) {
|
|
344
|
-
const fromRoot = relative(rootDirectory, target);
|
|
345
|
-
return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute2(fromRoot);
|
|
346
|
-
}
|
|
347
|
-
async function readUtf8WithCap(filePath) {
|
|
348
|
-
let handle;
|
|
349
|
-
try {
|
|
350
|
-
handle = await open(filePath, "r");
|
|
351
|
-
const metadata = await handle.stat();
|
|
352
|
-
if (!metadata.isFile()) {
|
|
353
|
-
throw new WorkspaceBoundaryError("SOURCE_NOT_FILE", "Diagram source must be a regular file.");
|
|
354
|
-
}
|
|
355
|
-
if (metadata.size > mcpSourceByteLimit) {
|
|
356
|
-
throw new WorkspaceBoundaryError("SOURCE_TOO_LARGE", `Diagram source exceeds the ${mcpSourceByteLimit}-byte limit.`);
|
|
357
|
-
}
|
|
358
|
-
const buffer = Buffer.allocUnsafe(mcpSourceByteLimit + 1);
|
|
359
|
-
let bytesRead = 0;
|
|
360
|
-
while (bytesRead <= mcpSourceByteLimit) {
|
|
361
|
-
const next = await handle.read(buffer, bytesRead, mcpSourceByteLimit + 1 - bytesRead, null);
|
|
362
|
-
if (next.bytesRead === 0)
|
|
363
|
-
break;
|
|
364
|
-
bytesRead += next.bytesRead;
|
|
365
|
-
}
|
|
366
|
-
if (bytesRead > mcpSourceByteLimit) {
|
|
367
|
-
throw new WorkspaceBoundaryError("SOURCE_TOO_LARGE", `Diagram source exceeds the ${mcpSourceByteLimit}-byte limit.`);
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
|
|
371
|
-
} catch {
|
|
372
|
-
throw new WorkspaceBoundaryError("SOURCE_ENCODING", "Diagram source must contain valid UTF-8.");
|
|
373
|
-
}
|
|
374
|
-
} catch (error) {
|
|
375
|
-
if (error instanceof WorkspaceBoundaryError)
|
|
376
|
-
throw error;
|
|
377
|
-
const code = filesystemCode(error);
|
|
378
|
-
if (code === "ENOENT") {
|
|
379
|
-
throw new WorkspaceBoundaryError("SOURCE_NOT_FOUND", "Diagram source does not exist.");
|
|
380
|
-
}
|
|
381
|
-
throw new WorkspaceBoundaryError("FILESYSTEM_ERROR", "Diagram source could not be read.");
|
|
382
|
-
} finally {
|
|
383
|
-
await handle?.close();
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
class WorkspaceBoundary {
|
|
388
|
-
rootDirectory;
|
|
389
|
-
constructor(rootDirectory) {
|
|
390
|
-
this.rootDirectory = rootDirectory;
|
|
391
|
-
}
|
|
392
|
-
static async create(rootDirectory) {
|
|
393
|
-
let resolvedRoot;
|
|
394
|
-
try {
|
|
395
|
-
resolvedRoot = await realpath(resolve3(rootDirectory));
|
|
396
|
-
if (!(await stat(resolvedRoot)).isDirectory()) {
|
|
397
|
-
throw new WorkspaceBoundaryError("OUTPUT_NOT_DIRECTORY", "MCP root must be a directory.");
|
|
398
|
-
}
|
|
399
|
-
} catch (error) {
|
|
400
|
-
if (error instanceof WorkspaceBoundaryError)
|
|
401
|
-
throw error;
|
|
402
|
-
throw new WorkspaceBoundaryError("FILESYSTEM_ERROR", "MCP root could not be opened.");
|
|
403
|
-
}
|
|
404
|
-
return new WorkspaceBoundary(resolvedRoot);
|
|
405
|
-
}
|
|
406
|
-
assertConfined(target) {
|
|
407
|
-
if (!isConfined(this.rootDirectory, target)) {
|
|
408
|
-
throw new WorkspaceBoundaryError("PATH_OUTSIDE_ROOT", "Path resolves outside the MCP root.");
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
toRelativePath(absolutePath) {
|
|
412
|
-
this.assertConfined(absolutePath);
|
|
413
|
-
const fromRoot = relative(this.rootDirectory, absolutePath);
|
|
414
|
-
return fromRoot === "" ? "." : fromRoot.split("\\").join("/");
|
|
415
|
-
}
|
|
416
|
-
async readSource(value) {
|
|
417
|
-
const normalized = normalizeRelativePath(value, { allowRoot: false });
|
|
418
|
-
const lexicalPath = resolve3(this.rootDirectory, normalized.native);
|
|
419
|
-
this.assertConfined(lexicalPath);
|
|
420
|
-
let canonicalPath;
|
|
421
|
-
try {
|
|
422
|
-
canonicalPath = await realpath(lexicalPath);
|
|
423
|
-
} catch (error) {
|
|
424
|
-
if (filesystemCode(error) === "ENOENT") {
|
|
425
|
-
throw new WorkspaceBoundaryError("SOURCE_NOT_FOUND", "Diagram source does not exist.");
|
|
426
|
-
}
|
|
427
|
-
throw new WorkspaceBoundaryError("FILESYSTEM_ERROR", "Diagram source could not be resolved.");
|
|
428
|
-
}
|
|
429
|
-
this.assertConfined(canonicalPath);
|
|
430
|
-
return {
|
|
431
|
-
absolutePath: canonicalPath,
|
|
432
|
-
relativePath: this.toRelativePath(canonicalPath),
|
|
433
|
-
text: await readUtf8WithCap(canonicalPath)
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
async resolveInputFile(value, maximumBytes) {
|
|
437
|
-
const normalized = normalizeRelativePath(value, { allowRoot: false });
|
|
438
|
-
const lexicalPath = resolve3(this.rootDirectory, normalized.native);
|
|
439
|
-
this.assertConfined(lexicalPath);
|
|
440
|
-
let canonicalPath;
|
|
441
|
-
try {
|
|
442
|
-
canonicalPath = await realpath(lexicalPath);
|
|
443
|
-
this.assertConfined(canonicalPath);
|
|
444
|
-
const metadata = await stat(canonicalPath);
|
|
445
|
-
if (!metadata.isFile()) {
|
|
446
|
-
throw new WorkspaceBoundaryError("SOURCE_NOT_FILE", "Input must be a regular file.");
|
|
447
|
-
}
|
|
448
|
-
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || metadata.size > maximumBytes) {
|
|
449
|
-
throw new WorkspaceBoundaryError("SOURCE_TOO_LARGE", `Input exceeds the ${maximumBytes}-byte limit.`);
|
|
450
|
-
}
|
|
451
|
-
} catch (error) {
|
|
452
|
-
if (error instanceof WorkspaceBoundaryError)
|
|
453
|
-
throw error;
|
|
454
|
-
if (filesystemCode(error) === "ENOENT") {
|
|
455
|
-
throw new WorkspaceBoundaryError("SOURCE_NOT_FOUND", "Input does not exist.");
|
|
456
|
-
}
|
|
457
|
-
throw new WorkspaceBoundaryError("FILESYSTEM_ERROR", "Input could not be resolved.");
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
absolutePath: canonicalPath,
|
|
461
|
-
relativePath: this.toRelativePath(canonicalPath)
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
async prepareOutputFile(value) {
|
|
465
|
-
const normalized = normalizeRelativePath(value, { allowRoot: false });
|
|
466
|
-
const fileName = basename2(normalized.native);
|
|
467
|
-
if (fileName === "." || fileName === ".." || fileName.length === 0) {
|
|
468
|
-
throw new WorkspaceBoundaryError("INVALID_PATH", "Output path must identify a file below the root.");
|
|
469
|
-
}
|
|
470
|
-
const directory = await this.prepareOutputDirectory(dirname3(normalized.native));
|
|
471
|
-
const absolutePath = join2(directory.absolutePath, fileName);
|
|
472
|
-
this.assertConfined(absolutePath);
|
|
473
|
-
return {
|
|
474
|
-
absolutePath,
|
|
475
|
-
relativePath: this.toRelativePath(absolutePath)
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
async prepareOutputDirectory(value) {
|
|
479
|
-
const normalized = normalizeRelativePath(value, { allowRoot: true });
|
|
480
|
-
const lexicalPath = resolve3(this.rootDirectory, normalized.native);
|
|
481
|
-
this.assertConfined(lexicalPath);
|
|
482
|
-
let ancestor = lexicalPath;
|
|
483
|
-
for (;; ) {
|
|
484
|
-
try {
|
|
485
|
-
const canonicalAncestor = await realpath(ancestor);
|
|
486
|
-
this.assertConfined(canonicalAncestor);
|
|
487
|
-
break;
|
|
488
|
-
} catch (error) {
|
|
489
|
-
if (error instanceof WorkspaceBoundaryError)
|
|
490
|
-
throw error;
|
|
491
|
-
if (filesystemCode(error) !== "ENOENT") {
|
|
492
|
-
throw new WorkspaceBoundaryError("FILESYSTEM_ERROR", "Output directory could not be resolved.");
|
|
493
|
-
}
|
|
494
|
-
const parent = dirname3(ancestor);
|
|
495
|
-
if (parent === ancestor) {
|
|
496
|
-
throw new WorkspaceBoundaryError("PATH_OUTSIDE_ROOT", "Output directory resolves outside the MCP root.");
|
|
497
|
-
}
|
|
498
|
-
ancestor = parent;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
try {
|
|
502
|
-
await mkdir2(lexicalPath, { recursive: true });
|
|
503
|
-
const canonicalPath = await realpath(lexicalPath);
|
|
504
|
-
this.assertConfined(canonicalPath);
|
|
505
|
-
if (!(await stat(canonicalPath)).isDirectory()) {
|
|
506
|
-
throw new WorkspaceBoundaryError("OUTPUT_NOT_DIRECTORY", "Output path must be a directory.");
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
absolutePath: canonicalPath,
|
|
510
|
-
relativePath: this.toRelativePath(canonicalPath)
|
|
511
|
-
};
|
|
512
|
-
} catch (error) {
|
|
513
|
-
if (error instanceof WorkspaceBoundaryError)
|
|
514
|
-
throw error;
|
|
515
|
-
throw new WorkspaceBoundaryError("FILESYSTEM_ERROR", "Output directory could not be created.");
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// src/mcp/tools.ts
|
|
521
|
-
var mcpMaximumScale = 4;
|
|
522
|
-
var mcpMaximumRenderedPixels = 16777216;
|
|
523
|
-
var mcpMaximumShapes = 64;
|
|
524
|
-
var mcpMaximumEdges = 128;
|
|
525
|
-
var mcpMaximumReturnedFindings = 40;
|
|
526
|
-
var defaultScale = 2;
|
|
527
|
-
var maximumShapeIdsPerFinding = 12;
|
|
528
|
-
var builtInConfig = Object.freeze({ icons: builtInIcons });
|
|
529
|
-
var findingSchema = {
|
|
530
|
-
type: "object",
|
|
531
|
-
additionalProperties: false,
|
|
532
|
-
required: ["code", "message", "shapeIds"],
|
|
533
|
-
properties: {
|
|
534
|
-
code: { type: "string" },
|
|
535
|
-
message: { type: "string" },
|
|
536
|
-
shapeIds: { type: "array", items: { type: "string" } }
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
function deepFreeze(value) {
|
|
540
|
-
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
541
|
-
return value;
|
|
542
|
-
}
|
|
543
|
-
for (const nested of Object.values(value))
|
|
544
|
-
deepFreeze(nested);
|
|
545
|
-
return Object.freeze(value);
|
|
546
|
-
}
|
|
547
|
-
var slopcameraMcpTools = deepFreeze([
|
|
548
|
-
{
|
|
549
|
-
name: "check_diagram",
|
|
550
|
-
title: "Check diagram",
|
|
551
|
-
description: "Parse and lint one root-relative Slopcamera diagram source without changing files. Uses only built-in icons and themes.",
|
|
552
|
-
inputSchema: {
|
|
553
|
-
type: "object",
|
|
554
|
-
additionalProperties: false,
|
|
555
|
-
required: ["path"],
|
|
556
|
-
properties: {
|
|
557
|
-
path: {
|
|
558
|
-
type: "string",
|
|
559
|
-
description: "Root-relative path to a diagram JSON source (1 MiB maximum)."
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
},
|
|
563
|
-
outputSchema: {
|
|
564
|
-
type: "object",
|
|
565
|
-
additionalProperties: false,
|
|
566
|
-
required: ["ok", "source", "findings", "summary"],
|
|
567
|
-
properties: {
|
|
568
|
-
ok: { const: true },
|
|
569
|
-
source: { type: "string" },
|
|
570
|
-
findings: { type: "array", items: findingSchema },
|
|
571
|
-
summary: {
|
|
572
|
-
type: "object",
|
|
573
|
-
additionalProperties: false,
|
|
574
|
-
required: [
|
|
575
|
-
"shapeCount",
|
|
576
|
-
"edgeCount",
|
|
577
|
-
"findingCount",
|
|
578
|
-
"returnedFindingCount",
|
|
579
|
-
"findingsTruncated"
|
|
580
|
-
],
|
|
581
|
-
properties: {
|
|
582
|
-
shapeCount: { type: "integer", minimum: 0 },
|
|
583
|
-
edgeCount: { type: "integer", minimum: 0 },
|
|
584
|
-
findingCount: { type: "integer", minimum: 0 },
|
|
585
|
-
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
586
|
-
findingsTruncated: { type: "boolean" }
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
},
|
|
591
|
-
annotations: {
|
|
592
|
-
title: "Check diagram",
|
|
593
|
-
readOnlyHint: true,
|
|
594
|
-
destructiveHint: false,
|
|
595
|
-
idempotentHint: true,
|
|
596
|
-
openWorldHint: false
|
|
597
|
-
}
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
name: "render_diagram",
|
|
601
|
-
title: "Render diagram",
|
|
602
|
-
description: "Render one root-relative Slopcamera diagram source with built-in icons and themes, overwriting its paired .tldr, light/dark SVG, and light/dark PNG artifacts.",
|
|
603
|
-
inputSchema: {
|
|
604
|
-
type: "object",
|
|
605
|
-
additionalProperties: false,
|
|
606
|
-
required: ["path"],
|
|
607
|
-
properties: {
|
|
608
|
-
path: {
|
|
609
|
-
type: "string",
|
|
610
|
-
description: "Root-relative path to a diagram JSON source (1 MiB maximum)."
|
|
611
|
-
},
|
|
612
|
-
out_dir: {
|
|
613
|
-
type: "string",
|
|
614
|
-
description: "Optional root-relative output directory. Defaults to the source directory."
|
|
615
|
-
},
|
|
616
|
-
scale: {
|
|
617
|
-
type: "number",
|
|
618
|
-
exclusiveMinimum: 0,
|
|
619
|
-
maximum: mcpMaximumScale,
|
|
620
|
-
default: defaultScale,
|
|
621
|
-
description: "PNG scale. The scaled canvas may contain at most 16,777,216 pixels."
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
},
|
|
625
|
-
outputSchema: {
|
|
626
|
-
type: "object",
|
|
627
|
-
additionalProperties: false,
|
|
628
|
-
required: ["ok", "source", "scale", "findings", "artifacts", "summary"],
|
|
629
|
-
properties: {
|
|
630
|
-
ok: { const: true },
|
|
631
|
-
source: { type: "string" },
|
|
632
|
-
scale: { type: "number" },
|
|
633
|
-
findings: { type: "array", items: findingSchema },
|
|
634
|
-
artifacts: {
|
|
635
|
-
type: "object",
|
|
636
|
-
additionalProperties: false,
|
|
637
|
-
required: ["tldr", "lightSvg", "darkSvg", "lightPng", "darkPng"],
|
|
638
|
-
properties: {
|
|
639
|
-
tldr: { type: "string" },
|
|
640
|
-
lightSvg: { type: "string" },
|
|
641
|
-
darkSvg: { type: "string" },
|
|
642
|
-
lightPng: { type: "string" },
|
|
643
|
-
darkPng: { type: "string" }
|
|
644
|
-
}
|
|
645
|
-
},
|
|
646
|
-
summary: {
|
|
647
|
-
type: "object",
|
|
648
|
-
additionalProperties: false,
|
|
649
|
-
required: [
|
|
650
|
-
"shapeCount",
|
|
651
|
-
"edgeCount",
|
|
652
|
-
"findingCount",
|
|
653
|
-
"returnedFindingCount",
|
|
654
|
-
"findingsTruncated"
|
|
655
|
-
],
|
|
656
|
-
properties: {
|
|
657
|
-
shapeCount: { type: "integer", minimum: 0 },
|
|
658
|
-
edgeCount: { type: "integer", minimum: 0 },
|
|
659
|
-
findingCount: { type: "integer", minimum: 0 },
|
|
660
|
-
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
661
|
-
findingsTruncated: { type: "boolean" }
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
},
|
|
666
|
-
annotations: {
|
|
667
|
-
title: "Render diagram",
|
|
668
|
-
readOnlyHint: false,
|
|
669
|
-
destructiveHint: true,
|
|
670
|
-
idempotentHint: true,
|
|
671
|
-
openWorldHint: false
|
|
672
|
-
}
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
name: "search_slopcamera",
|
|
676
|
-
title: "Search Slopcamera operations",
|
|
677
|
-
description: "Search the fixed semantic Slopcamera operation registry by bounded text. This never executes code or changes files.",
|
|
678
|
-
inputSchema: {
|
|
679
|
-
type: "object",
|
|
680
|
-
additionalProperties: false,
|
|
681
|
-
properties: {
|
|
682
|
-
query: {
|
|
683
|
-
type: "string",
|
|
684
|
-
maxLength: 200,
|
|
685
|
-
description: "Optional terms matched against operation codes and descriptions."
|
|
686
|
-
},
|
|
687
|
-
limit: {
|
|
688
|
-
type: "integer",
|
|
689
|
-
minimum: 1,
|
|
690
|
-
maximum: 20,
|
|
691
|
-
default: 4
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
},
|
|
695
|
-
outputSchema: {
|
|
696
|
-
type: "object",
|
|
697
|
-
additionalProperties: false,
|
|
698
|
-
required: ["ok", "operations"],
|
|
699
|
-
properties: {
|
|
700
|
-
ok: { const: true },
|
|
701
|
-
operations: {
|
|
702
|
-
type: "array",
|
|
703
|
-
maxItems: 20,
|
|
704
|
-
items: {
|
|
705
|
-
type: "object",
|
|
706
|
-
required: [
|
|
707
|
-
"code",
|
|
708
|
-
"title",
|
|
709
|
-
"description",
|
|
710
|
-
"execution",
|
|
711
|
-
"authentication",
|
|
712
|
-
"inputSchema"
|
|
713
|
-
]
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
},
|
|
718
|
-
annotations: {
|
|
719
|
-
title: "Search Slopcamera operations",
|
|
720
|
-
readOnlyHint: true,
|
|
721
|
-
destructiveHint: false,
|
|
722
|
-
idempotentHint: true,
|
|
723
|
-
openWorldHint: false
|
|
724
|
-
}
|
|
725
|
-
},
|
|
726
|
-
{
|
|
727
|
-
name: "execute_slopcamera",
|
|
728
|
-
title: "Execute Slopcamera operation",
|
|
729
|
-
description: "Execute one exact operation code with typed JSON input. Never accepts or evaluates source code. Local paths remain confined to the configured workspace root.",
|
|
730
|
-
inputSchema: {
|
|
731
|
-
type: "object",
|
|
732
|
-
additionalProperties: false,
|
|
733
|
-
required: ["operation", "input"],
|
|
734
|
-
properties: {
|
|
735
|
-
operation: {
|
|
736
|
-
type: "string",
|
|
737
|
-
enum: slopcameraOperationCodes
|
|
738
|
-
},
|
|
739
|
-
input: {
|
|
740
|
-
type: "object",
|
|
741
|
-
description: "Typed input matching the selected operation's registry schema."
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
outputSchema: {
|
|
746
|
-
type: "object",
|
|
747
|
-
additionalProperties: false,
|
|
748
|
-
required: ["ok", "operation", "result"],
|
|
749
|
-
properties: {
|
|
750
|
-
ok: { const: true },
|
|
751
|
-
operation: { type: "string", enum: slopcameraOperationCodes },
|
|
752
|
-
result: { type: "object" }
|
|
753
|
-
}
|
|
754
|
-
},
|
|
755
|
-
annotations: {
|
|
756
|
-
title: "Execute Slopcamera operation",
|
|
757
|
-
readOnlyHint: false,
|
|
758
|
-
destructiveHint: true,
|
|
759
|
-
idempotentHint: false,
|
|
760
|
-
openWorldHint: true
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
]);
|
|
764
|
-
|
|
765
|
-
class ToolFailure extends Error {
|
|
766
|
-
code;
|
|
767
|
-
issues;
|
|
768
|
-
constructor(code, message, issues) {
|
|
769
|
-
super(message);
|
|
770
|
-
this.name = "ToolFailure";
|
|
771
|
-
this.code = code;
|
|
772
|
-
if (issues !== undefined)
|
|
773
|
-
this.issues = issues;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
function isRecord2(value) {
|
|
777
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
778
|
-
}
|
|
779
|
-
function safeFragment(value, maximumLength = 160) {
|
|
780
|
-
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, maximumLength);
|
|
781
|
-
}
|
|
782
|
-
function safeIssues(issues) {
|
|
783
|
-
return issues.slice(0, 24).map((issue) => safeFragment(issue, 240));
|
|
784
|
-
}
|
|
785
|
-
function rejectUnknownKeys(value, allowed) {
|
|
786
|
-
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
787
|
-
if (unknown.length > 0) {
|
|
788
|
-
throw new ToolFailure("INVALID_ARGUMENTS", `Unsupported argument: ${safeFragment(unknown[0] ?? "unknown")}.`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
function parsePath(value) {
|
|
792
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
793
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "path must be a non-empty root-relative string.");
|
|
794
|
-
}
|
|
795
|
-
if (!value.toLowerCase().endsWith(".diagram.json")) {
|
|
796
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "path must end in .diagram.json.");
|
|
797
|
-
}
|
|
798
|
-
return value;
|
|
799
|
-
}
|
|
800
|
-
function parseCheckArguments(value) {
|
|
801
|
-
if (!isRecord2(value)) {
|
|
802
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.");
|
|
803
|
-
}
|
|
804
|
-
rejectUnknownKeys(value, new Set(["path"]));
|
|
805
|
-
return { path: parsePath(value.path) };
|
|
806
|
-
}
|
|
807
|
-
function parseRenderArguments(value) {
|
|
808
|
-
if (!isRecord2(value)) {
|
|
809
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.");
|
|
810
|
-
}
|
|
811
|
-
rejectUnknownKeys(value, new Set(["path", "out_dir", "scale"]));
|
|
812
|
-
const outDirectory = value.out_dir;
|
|
813
|
-
if (outDirectory !== undefined && (typeof outDirectory !== "string" || outDirectory.length === 0)) {
|
|
814
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "out_dir must be a non-empty root-relative string when present.");
|
|
815
|
-
}
|
|
816
|
-
const scale = value.scale ?? defaultScale;
|
|
817
|
-
if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > mcpMaximumScale) {
|
|
818
|
-
throw new ToolFailure("RENDER_LIMIT", `scale must be greater than zero and no more than ${mcpMaximumScale}.`);
|
|
819
|
-
}
|
|
820
|
-
return {
|
|
821
|
-
path: parsePath(value.path),
|
|
822
|
-
...outDirectory === undefined ? {} : { outDirectory },
|
|
823
|
-
scale
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
function parseSearchArguments(value) {
|
|
827
|
-
if (!isRecord2(value)) {
|
|
828
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.");
|
|
829
|
-
}
|
|
830
|
-
rejectUnknownKeys(value, new Set(["query", "limit"]));
|
|
831
|
-
const query = value.query ?? "";
|
|
832
|
-
const limit = value.limit ?? slopcameraOperationCodes.length;
|
|
833
|
-
if (typeof query !== "string" || query.length > 200 || /[\u0000-\u001f\u007f]/u.test(query) || !Number.isInteger(limit) || limit < 1 || limit > 20) {
|
|
834
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "query must be a bounded string and limit must be an integer from 1 through 20.");
|
|
835
|
-
}
|
|
836
|
-
return { query, limit };
|
|
837
|
-
}
|
|
838
|
-
function parseExecuteArguments(value) {
|
|
839
|
-
if (!isRecord2(value)) {
|
|
840
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.");
|
|
841
|
-
}
|
|
842
|
-
rejectUnknownKeys(value, new Set(["operation", "input"]));
|
|
843
|
-
if (typeof value.operation !== "string" || !slopcameraOperationCodes.includes(value.operation) || !isRecord2(value.input)) {
|
|
844
|
-
throw new ToolFailure("INVALID_ARGUMENTS", "operation must be an exact Slopcamera operation code and input must be an object.");
|
|
845
|
-
}
|
|
846
|
-
return {
|
|
847
|
-
operation: value.operation,
|
|
848
|
-
input: value.input
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
function assertBuiltInIcons(spec) {
|
|
852
|
-
for (const shape of spec.shapes) {
|
|
853
|
-
if ((shape.type === "rect" || shape.type === "ellipse") && shape.icon !== undefined && !Object.hasOwn(builtInIcons, shape.icon)) {
|
|
854
|
-
throw new ToolFailure("UNKNOWN_ICON", `Shape ${safeFragment(shape.id)} requests unavailable built-in icon ${safeFragment(shape.icon)}.`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function assertComplexityLimits(spec) {
|
|
859
|
-
const edgeCount = spec.edges?.length ?? 0;
|
|
860
|
-
if (spec.shapes.length > mcpMaximumShapes || edgeCount > mcpMaximumEdges) {
|
|
861
|
-
throw new ToolFailure("COMPLEXITY_LIMIT", `Diagram may contain at most ${mcpMaximumShapes} shapes and ${mcpMaximumEdges} edges in MCP mode.`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
function assertRawComplexityLimits(value) {
|
|
865
|
-
if (!isRecord2(value))
|
|
866
|
-
return;
|
|
867
|
-
const shapeCount = Array.isArray(value.shapes) ? value.shapes.length : 0;
|
|
868
|
-
const edgeCount = Array.isArray(value.edges) ? value.edges.length : 0;
|
|
869
|
-
if (shapeCount > mcpMaximumShapes || edgeCount > mcpMaximumEdges) {
|
|
870
|
-
throw new ToolFailure("COMPLEXITY_LIMIT", `Diagram may contain at most ${mcpMaximumShapes} shapes and ${mcpMaximumEdges} edges in MCP mode.`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
function assertRenderLimits(spec, scale) {
|
|
874
|
-
const scaledWidth = spec.canvas.width * scale;
|
|
875
|
-
const scaledHeight = spec.canvas.height * scale;
|
|
876
|
-
const pixels = Math.ceil(scaledWidth) * Math.ceil(scaledHeight);
|
|
877
|
-
if (!Number.isFinite(pixels) || scaledWidth < 1 || scaledHeight < 1 || pixels > mcpMaximumRenderedPixels) {
|
|
878
|
-
throw new ToolFailure("RENDER_LIMIT", `Scaled canvas must be at least 1 pixel on each axis and no more than ${mcpMaximumRenderedPixels.toLocaleString("en-US")} pixels total.`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
function publicFinding(finding) {
|
|
882
|
-
return {
|
|
883
|
-
code: safeFragment(finding.code, 64),
|
|
884
|
-
message: safeFragment(finding.message, 240),
|
|
885
|
-
shapeIds: finding.shapeIds.slice(0, maximumShapeIdsPerFinding).map((shapeId) => safeFragment(shapeId, 120))
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
function publicFindings(findings) {
|
|
889
|
-
return findings.slice(0, mcpMaximumReturnedFindings).map(publicFinding);
|
|
890
|
-
}
|
|
891
|
-
function diagramSummary(spec, findingCount, returnedFindingCount) {
|
|
892
|
-
return {
|
|
893
|
-
shapeCount: spec.shapes.length,
|
|
894
|
-
edgeCount: spec.edges?.length ?? 0,
|
|
895
|
-
findingCount,
|
|
896
|
-
returnedFindingCount,
|
|
897
|
-
findingsTruncated: returnedFindingCount < findingCount
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
function successResult(text, structuredContent) {
|
|
901
|
-
return {
|
|
902
|
-
content: [{ type: "text", text }],
|
|
903
|
-
structuredContent
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
function failureResult(error) {
|
|
907
|
-
let code = "INTERNAL_ERROR";
|
|
908
|
-
let message = "The tool failed safely.";
|
|
909
|
-
let issues;
|
|
910
|
-
if (error instanceof ToolFailure) {
|
|
911
|
-
code = error.code;
|
|
912
|
-
message = safeFragment(error.message, 320);
|
|
913
|
-
issues = error.issues;
|
|
914
|
-
} else if (error instanceof WorkspaceBoundaryError) {
|
|
915
|
-
code = error.code;
|
|
916
|
-
message = safeFragment(error.message, 320);
|
|
917
|
-
} else if (error instanceof SlopcameraCloudError) {
|
|
918
|
-
code = error.code;
|
|
919
|
-
message = safeFragment(error.message.replace(/^\[[A-Z_]+\]\s*/u, ""), 320);
|
|
920
|
-
} else if (error instanceof SlopcameraOperationError) {
|
|
921
|
-
code = error.code;
|
|
922
|
-
message = safeFragment(error.message.replace(/^\[[A-Z_]+\]\s*/u, ""), 320);
|
|
923
|
-
} else if (error instanceof VectorizeError) {
|
|
924
|
-
code = `VECTORIZE_${error.code.toUpperCase()}`;
|
|
925
|
-
message = "Local vectorization failed safely.";
|
|
926
|
-
} else if (error instanceof DiagramValidationError) {
|
|
927
|
-
code = "INVALID_DIAGRAM";
|
|
928
|
-
message = "Diagram source did not pass validation.";
|
|
929
|
-
issues = safeIssues(error.issues);
|
|
930
|
-
} else if (typeof error === "object" && error !== null && "issues" in error && Array.isArray(error.issues) && error.issues.every((issue) => typeof issue === "string")) {
|
|
931
|
-
code = "INVALID_LAYOUT";
|
|
932
|
-
message = "Diagram layout could not be resolved.";
|
|
933
|
-
issues = safeIssues(error.issues);
|
|
934
|
-
}
|
|
935
|
-
const issueText = issues === undefined || issues.length === 0 ? "" : `
|
|
936
|
-
${issues.map((issue) => `- ${issue}`).join(`
|
|
937
|
-
`)}`;
|
|
938
|
-
return {
|
|
939
|
-
content: [{ type: "text", text: `[${code}] ${message}${issueText}` }],
|
|
940
|
-
isError: true
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
function portableDirectory(filePath) {
|
|
944
|
-
const separator = filePath.lastIndexOf("/");
|
|
945
|
-
return separator === -1 ? "." : filePath.slice(0, separator);
|
|
946
|
-
}
|
|
947
|
-
async function atomicOverwrite(filePath, data) {
|
|
948
|
-
const temporaryPath = join3(dirname4(filePath), `.${crypto.randomUUID()}.slopcamera-mcp.tmp`);
|
|
949
|
-
try {
|
|
950
|
-
await writeFile2(temporaryPath, data, { flag: "wx" });
|
|
951
|
-
try {
|
|
952
|
-
await rename2(temporaryPath, filePath);
|
|
953
|
-
} catch (error) {
|
|
954
|
-
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
955
|
-
if (code !== "EEXIST" && code !== "EPERM")
|
|
956
|
-
throw error;
|
|
957
|
-
await rm2(filePath, { force: true });
|
|
958
|
-
await rename2(temporaryPath, filePath);
|
|
959
|
-
}
|
|
960
|
-
} finally {
|
|
961
|
-
await rm2(temporaryPath, { force: true });
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
async function loadDiagram(boundary, path) {
|
|
965
|
-
const source = await boundary.readSource(path);
|
|
966
|
-
let parsed;
|
|
967
|
-
try {
|
|
968
|
-
parsed = JSON.parse(source.text);
|
|
969
|
-
} catch {
|
|
970
|
-
throw new ToolFailure("INVALID_JSON", "Diagram source is not valid JSON.");
|
|
971
|
-
}
|
|
972
|
-
assertRawComplexityLimits(parsed);
|
|
973
|
-
const spec = parseDiagramSpec(parsed);
|
|
974
|
-
assertComplexityLimits(spec);
|
|
975
|
-
assertBuiltInIcons(spec);
|
|
976
|
-
return { source, spec };
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
class SlopcameraMcpToolRuntime {
|
|
980
|
-
boundary;
|
|
981
|
-
generateDependencies;
|
|
982
|
-
hostResourceCoordinator;
|
|
983
|
-
renderQueue = Promise.resolve();
|
|
984
|
-
constructor(boundary, generateDependencies, hostResourceCoordinator) {
|
|
985
|
-
this.boundary = boundary;
|
|
986
|
-
this.generateDependencies = generateDependencies;
|
|
987
|
-
this.hostResourceCoordinator = hostResourceCoordinator;
|
|
988
|
-
}
|
|
989
|
-
static async create(rootDirectory, generateDependencies = {}, hostResourceCoordinator) {
|
|
990
|
-
return new SlopcameraMcpToolRuntime(await WorkspaceBoundary.create(rootDirectory), generateDependencies, hostResourceCoordinator ?? createDefaultHostResourceCoordinator());
|
|
991
|
-
}
|
|
992
|
-
async withHostAdmission(operation, callback) {
|
|
993
|
-
return await withSlopcameraOperationHostAdmission(operation, callback, {
|
|
994
|
-
hostResourceCoordinator: this.hostResourceCoordinator
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
enqueueRender(operation) {
|
|
998
|
-
const result = this.renderQueue.then(operation, operation);
|
|
999
|
-
this.renderQueue = result.then(() => {
|
|
1000
|
-
return;
|
|
1001
|
-
}, () => {
|
|
1002
|
-
return;
|
|
1003
|
-
});
|
|
1004
|
-
return result;
|
|
1005
|
-
}
|
|
1006
|
-
async call(name, argumentsValue) {
|
|
1007
|
-
try {
|
|
1008
|
-
if (name === "check_diagram") {
|
|
1009
|
-
const options = parseCheckArguments(argumentsValue);
|
|
1010
|
-
return await this.withHostAdmission("slopcamera.diagram.check", async () => await this.check(options));
|
|
1011
|
-
}
|
|
1012
|
-
if (name === "render_diagram") {
|
|
1013
|
-
const options = parseRenderArguments(argumentsValue);
|
|
1014
|
-
return await this.enqueueRender(async () => await this.withHostAdmission("slopcamera.diagram.render", async () => await this.render(options)));
|
|
1015
|
-
}
|
|
1016
|
-
if (name === "search_slopcamera") {
|
|
1017
|
-
const options = parseSearchArguments(argumentsValue);
|
|
1018
|
-
const operations = searchSlopcameraOperations(options.query, options.limit);
|
|
1019
|
-
return successResult(`Found ${operations.length} Slopcamera operation${operations.length === 1 ? "" : "s"}.`, { ok: true, operations });
|
|
1020
|
-
}
|
|
1021
|
-
if (name === "execute_slopcamera") {
|
|
1022
|
-
const options = parseExecuteArguments(argumentsValue);
|
|
1023
|
-
return await this.execute(options);
|
|
1024
|
-
}
|
|
1025
|
-
throw new ToolFailure("UNKNOWN_TOOL", "Requested tool is not available.");
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
return failureResult(error);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
wrapSemanticResult(operation, result) {
|
|
1031
|
-
if (result.isError === true)
|
|
1032
|
-
return result;
|
|
1033
|
-
return {
|
|
1034
|
-
content: result.content,
|
|
1035
|
-
structuredContent: {
|
|
1036
|
-
ok: true,
|
|
1037
|
-
operation,
|
|
1038
|
-
result: result.structuredContent ?? {}
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
async execute(options) {
|
|
1043
|
-
if (options.operation === "slopcamera.diagram.check") {
|
|
1044
|
-
const input = parseSlopcameraOperationInput(options.operation, options.input);
|
|
1045
|
-
return this.wrapSemanticResult(options.operation, await this.withHostAdmission(options.operation, async () => await this.check({ path: input.path })));
|
|
1046
|
-
}
|
|
1047
|
-
if (options.operation === "slopcamera.diagram.render") {
|
|
1048
|
-
const input = parseSlopcameraOperationInput(options.operation, options.input);
|
|
1049
|
-
return this.enqueueRender(async () => await this.withHostAdmission(options.operation, async () => this.wrapSemanticResult(options.operation, await this.render({
|
|
1050
|
-
path: input.path,
|
|
1051
|
-
...input.outDirectory === undefined ? {} : { outDirectory: input.outDirectory },
|
|
1052
|
-
scale: input.scale ?? defaultScale
|
|
1053
|
-
}))));
|
|
1054
|
-
}
|
|
1055
|
-
if (options.operation === "slopcamera.image.vectorize") {
|
|
1056
|
-
const input = parseSlopcameraOperationInput(options.operation, options.input);
|
|
1057
|
-
return this.enqueueRender(async () => await this.withHostAdmission(options.operation, async (lease) => {
|
|
1058
|
-
const source = await this.boundary.resolveInputFile(input.inputPath, vectorizeHardLimits.maxInputBytes);
|
|
1059
|
-
const output = await this.boundary.prepareOutputFile(input.outputPath);
|
|
1060
|
-
const result = await vectorizeImage(source.absolutePath, {
|
|
1061
|
-
outputPath: output.absolutePath,
|
|
1062
|
-
...input.duotone === undefined ? {} : { duotone: input.duotone },
|
|
1063
|
-
...input.alphaCutoff === undefined ? {} : { alphaCutoff: input.alphaCutoff },
|
|
1064
|
-
...input.timeoutMs === undefined ? {} : { limits: { maxDurationMs: input.timeoutMs } },
|
|
1065
|
-
inheritedFileDescriptors: [lease.inheritedFileDescriptor]
|
|
1066
|
-
});
|
|
1067
|
-
return successResult(`Executed ${options.operation}: ${output.relativePath}`, {
|
|
1068
|
-
ok: true,
|
|
1069
|
-
operation: options.operation,
|
|
1070
|
-
result: {
|
|
1071
|
-
inputPath: source.relativePath,
|
|
1072
|
-
outputPath: output.relativePath,
|
|
1073
|
-
receipt: result.receipt
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1076
|
-
}));
|
|
1077
|
-
}
|
|
1078
|
-
return await this.withHostAdmission(options.operation, async () => {
|
|
1079
|
-
const input = parseSlopcameraOperationInput(options.operation, options.input);
|
|
1080
|
-
const output = await this.boundary.prepareOutputFile(input.outputPath);
|
|
1081
|
-
const generated = await generateSlopcameraImageFile({ ...input, outputPath: output.absolutePath }, this.generateDependencies);
|
|
1082
|
-
return successResult(`Executed ${options.operation}: ${output.relativePath} (request ${safeFragment(generated.requestId, 256)}).`, {
|
|
1083
|
-
ok: true,
|
|
1084
|
-
operation: options.operation,
|
|
1085
|
-
result: {
|
|
1086
|
-
bytes: generated.bytes,
|
|
1087
|
-
mediaType: generated.mediaType,
|
|
1088
|
-
model: generated.model,
|
|
1089
|
-
outputPath: output.relativePath,
|
|
1090
|
-
provider: generated.provider,
|
|
1091
|
-
requestId: generated.requestId,
|
|
1092
|
-
sha256: generated.sha256,
|
|
1093
|
-
warnings: generated.warnings
|
|
1094
|
-
}
|
|
1095
|
-
});
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
async check(options) {
|
|
1099
|
-
const { source, spec } = await loadDiagram(this.boundary, options.path);
|
|
1100
|
-
const allFindings = lintDiagram(spec);
|
|
1101
|
-
const findings = publicFindings(allFindings);
|
|
1102
|
-
const summary = diagramSummary(spec, allFindings.length, findings.length);
|
|
1103
|
-
const text = allFindings.length === 0 ? `Checked ${source.relativePath}: no findings.` : `Checked ${source.relativePath}: ${allFindings.length} finding${allFindings.length === 1 ? "" : "s"}; ${findings.length} returned in structured content${findings.length < allFindings.length ? " (truncated)" : ""}.`;
|
|
1104
|
-
return successResult(text, {
|
|
1105
|
-
ok: true,
|
|
1106
|
-
source: source.relativePath,
|
|
1107
|
-
findings,
|
|
1108
|
-
summary
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
async render(options) {
|
|
1112
|
-
const { source, spec } = await loadDiagram(this.boundary, options.path);
|
|
1113
|
-
assertRenderLimits(spec, options.scale);
|
|
1114
|
-
const outputDirectory = await this.boundary.prepareOutputDirectory(options.outDirectory ?? portableDirectory(source.relativePath));
|
|
1115
|
-
const tldr = serializeTldr(spec, builtInConfig);
|
|
1116
|
-
const [light, dark] = await Promise.all([
|
|
1117
|
-
renderSvg(spec, "light", builtInConfig),
|
|
1118
|
-
renderSvg(spec, "dark", builtInConfig)
|
|
1119
|
-
]);
|
|
1120
|
-
const lightPng = renderPng(light, builtInConfig, options.scale);
|
|
1121
|
-
const darkPng = renderPng(dark, builtInConfig, options.scale);
|
|
1122
|
-
const absoluteArtifacts = {
|
|
1123
|
-
spec: source.absolutePath,
|
|
1124
|
-
tldr: join3(outputDirectory.absolutePath, `${spec.name}.tldr`),
|
|
1125
|
-
lightSvg: join3(outputDirectory.absolutePath, `${spec.name}.light.svg`),
|
|
1126
|
-
darkSvg: join3(outputDirectory.absolutePath, `${spec.name}.dark.svg`),
|
|
1127
|
-
lightPng: join3(outputDirectory.absolutePath, `${spec.name}.light.png`),
|
|
1128
|
-
darkPng: join3(outputDirectory.absolutePath, `${spec.name}.dark.png`)
|
|
1129
|
-
};
|
|
1130
|
-
await Promise.all([
|
|
1131
|
-
atomicOverwrite(absoluteArtifacts.tldr, tldr),
|
|
1132
|
-
atomicOverwrite(absoluteArtifacts.lightSvg, light.svg),
|
|
1133
|
-
atomicOverwrite(absoluteArtifacts.darkSvg, dark.svg),
|
|
1134
|
-
atomicOverwrite(absoluteArtifacts.lightPng, lightPng),
|
|
1135
|
-
atomicOverwrite(absoluteArtifacts.darkPng, darkPng)
|
|
1136
|
-
]);
|
|
1137
|
-
const artifacts = {
|
|
1138
|
-
tldr: this.boundary.toRelativePath(absoluteArtifacts.tldr),
|
|
1139
|
-
lightSvg: this.boundary.toRelativePath(absoluteArtifacts.lightSvg),
|
|
1140
|
-
darkSvg: this.boundary.toRelativePath(absoluteArtifacts.darkSvg),
|
|
1141
|
-
lightPng: this.boundary.toRelativePath(absoluteArtifacts.lightPng),
|
|
1142
|
-
darkPng: this.boundary.toRelativePath(absoluteArtifacts.darkPng)
|
|
1143
|
-
};
|
|
1144
|
-
const allFindings = lintDiagram(spec);
|
|
1145
|
-
const findings = publicFindings(allFindings);
|
|
1146
|
-
const summary = diagramSummary(spec, allFindings.length, findings.length);
|
|
1147
|
-
const text = [
|
|
1148
|
-
`Rendered ${source.relativePath} with built-in assets:`,
|
|
1149
|
-
...Object.values(artifacts).map((artifact) => `- ${artifact}`)
|
|
1150
|
-
].join(`
|
|
1151
|
-
`);
|
|
1152
|
-
return successResult(text, {
|
|
1153
|
-
ok: true,
|
|
1154
|
-
source: source.relativePath,
|
|
1155
|
-
scale: options.scale,
|
|
1156
|
-
findings,
|
|
1157
|
-
artifacts,
|
|
1158
|
-
summary
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// src/version.ts
|
|
1164
|
-
var SLOPCAMERA_VERSION = "3.2.8";
|
|
1165
|
-
|
|
1166
|
-
// src/mcp/server.ts
|
|
1167
|
-
var slopcameraMcpProtocolVersion = "2025-11-25";
|
|
1168
|
-
var slopcameraMcpServerName = "hraness-slopcamera";
|
|
1169
|
-
var maximumMessageBytes = 1024 * 1024;
|
|
1170
|
-
function isRecord3(value) {
|
|
1171
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1172
|
-
}
|
|
1173
|
-
function isJsonRpcId(value) {
|
|
1174
|
-
return typeof value === "string" || typeof value === "number" && Number.isSafeInteger(value);
|
|
1175
|
-
}
|
|
1176
|
-
function isInitializeParams(value) {
|
|
1177
|
-
return isRecord3(value) && typeof value.protocolVersion === "string" && isRecord3(value.capabilities) && isRecord3(value.clientInfo) && typeof value.clientInfo.name === "string" && typeof value.clientInfo.version === "string";
|
|
1178
|
-
}
|
|
1179
|
-
function parseRequest(value) {
|
|
1180
|
-
if (!isRecord3(value) || value.jsonrpc !== "2.0" || typeof value.method !== "string" || value.method.length === 0 || "id" in value && !isJsonRpcId(value.id)) {
|
|
1181
|
-
throw new Error("invalid request");
|
|
1182
|
-
}
|
|
1183
|
-
return {
|
|
1184
|
-
jsonrpc: "2.0",
|
|
1185
|
-
..."id" in value ? { id: value.id } : {},
|
|
1186
|
-
method: value.method,
|
|
1187
|
-
..."params" in value ? { params: value.params } : {}
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
function success(id, result) {
|
|
1191
|
-
return { jsonrpc: "2.0", id, result };
|
|
1192
|
-
}
|
|
1193
|
-
function failure(id, code, message) {
|
|
1194
|
-
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
1195
|
-
}
|
|
1196
|
-
function parseToolCall(params) {
|
|
1197
|
-
if (!isRecord3(params) || typeof params.name !== "string" || params.arguments !== undefined && !isRecord3(params.arguments)) {
|
|
1198
|
-
throw new Error("invalid params");
|
|
1199
|
-
}
|
|
1200
|
-
const unknownKeys = Object.keys(params).filter((key) => key !== "name" && key !== "arguments");
|
|
1201
|
-
if (unknownKeys.length > 0)
|
|
1202
|
-
throw new Error("invalid params");
|
|
1203
|
-
return {
|
|
1204
|
-
name: params.name,
|
|
1205
|
-
argumentsValue: params.arguments ?? {}
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
class SlopcameraMcpSession {
|
|
1210
|
-
runtime;
|
|
1211
|
-
serverVersion;
|
|
1212
|
-
state = "new";
|
|
1213
|
-
constructor(runtime, serverVersion) {
|
|
1214
|
-
this.runtime = runtime;
|
|
1215
|
-
this.serverVersion = serverVersion;
|
|
1216
|
-
}
|
|
1217
|
-
async handle(value) {
|
|
1218
|
-
let request;
|
|
1219
|
-
try {
|
|
1220
|
-
request = parseRequest(value);
|
|
1221
|
-
} catch {
|
|
1222
|
-
return failure(null, -32600, "Invalid Request");
|
|
1223
|
-
}
|
|
1224
|
-
const notification = request.id === undefined;
|
|
1225
|
-
if (request.method === "notifications/initialized") {
|
|
1226
|
-
if (!notification) {
|
|
1227
|
-
return failure(request.id, -32600, "Invalid Request");
|
|
1228
|
-
}
|
|
1229
|
-
if (this.state === "initializing")
|
|
1230
|
-
this.state = "ready";
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
|
-
if (notification)
|
|
1234
|
-
return null;
|
|
1235
|
-
const id = request.id;
|
|
1236
|
-
if (request.method === "initialize") {
|
|
1237
|
-
if (this.state !== "new" || !isInitializeParams(request.params)) {
|
|
1238
|
-
return failure(id, -32602, "Invalid initialize parameters");
|
|
1239
|
-
}
|
|
1240
|
-
this.state = "initializing";
|
|
1241
|
-
return success(id, {
|
|
1242
|
-
protocolVersion: slopcameraMcpProtocolVersion,
|
|
1243
|
-
capabilities: {
|
|
1244
|
-
tools: { listChanged: false }
|
|
1245
|
-
},
|
|
1246
|
-
serverInfo: {
|
|
1247
|
-
name: slopcameraMcpServerName,
|
|
1248
|
-
version: this.serverVersion
|
|
1249
|
-
},
|
|
1250
|
-
instructions: "Use check_diagram/render_diagram or search_slopcamera followed by execute_slopcamera with an exact registry code and typed JSON. Local paths are root-relative; source code is never accepted or evaluated."
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
if (this.state !== "ready") {
|
|
1254
|
-
return failure(id, -32002, "Server is not initialized");
|
|
1255
|
-
}
|
|
1256
|
-
if (request.method === "ping")
|
|
1257
|
-
return success(id, {});
|
|
1258
|
-
if (request.method === "tools/list") {
|
|
1259
|
-
if (request.params !== undefined && (!isRecord3(request.params) || Object.keys(request.params).length > 0)) {
|
|
1260
|
-
return failure(id, -32602, "Invalid tools/list parameters");
|
|
1261
|
-
}
|
|
1262
|
-
return success(id, { tools: slopcameraMcpTools });
|
|
1263
|
-
}
|
|
1264
|
-
if (request.method === "tools/call") {
|
|
1265
|
-
try {
|
|
1266
|
-
const toolCall = parseToolCall(request.params);
|
|
1267
|
-
if (!slopcameraMcpTools.some((tool) => tool.name === toolCall.name)) {
|
|
1268
|
-
return failure(id, -32602, "Unknown tool");
|
|
1269
|
-
}
|
|
1270
|
-
return success(id, await this.runtime.call(toolCall.name, toolCall.argumentsValue));
|
|
1271
|
-
} catch {
|
|
1272
|
-
return failure(id, -32602, "Invalid tools/call parameters");
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
return failure(id, -32601, "Method not found");
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
async function defaultWriteLine(line) {
|
|
1279
|
-
await new Promise((resolve4, reject) => {
|
|
1280
|
-
process.stdout.write(`${line}
|
|
1281
|
-
`, (error) => {
|
|
1282
|
-
if (error === null || error === undefined)
|
|
1283
|
-
resolve4();
|
|
1284
|
-
else
|
|
1285
|
-
reject(error);
|
|
1286
|
-
});
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
function defaultInput() {
|
|
1290
|
-
return process.stdin;
|
|
1291
|
-
}
|
|
1292
|
-
async function emitResponse(writeLine, response) {
|
|
1293
|
-
await writeLine(JSON.stringify(response));
|
|
1294
|
-
}
|
|
1295
|
-
async function processLine(line, session, writeLine) {
|
|
1296
|
-
if (line.byteLength === 0)
|
|
1297
|
-
return;
|
|
1298
|
-
let value;
|
|
1299
|
-
try {
|
|
1300
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(line);
|
|
1301
|
-
if (text.trim() === "")
|
|
1302
|
-
return;
|
|
1303
|
-
value = JSON.parse(text);
|
|
1304
|
-
} catch {
|
|
1305
|
-
await emitResponse(writeLine, failure(null, -32700, "Parse error"));
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (Array.isArray(value)) {
|
|
1309
|
-
await emitResponse(writeLine, failure(null, -32600, "Invalid Request"));
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
const response = await session.handle(value);
|
|
1313
|
-
if (response !== null)
|
|
1314
|
-
await emitResponse(writeLine, response);
|
|
1315
|
-
}
|
|
1316
|
-
async function runMcpServer(options = {}) {
|
|
1317
|
-
const runtime = await SlopcameraMcpToolRuntime.create(options.rootDirectory ?? process.cwd(), options.generateDependencies);
|
|
1318
|
-
const session = new SlopcameraMcpSession(runtime, options.serverVersion ?? SLOPCAMERA_VERSION);
|
|
1319
|
-
const writeLine = options.writeLine ?? defaultWriteLine;
|
|
1320
|
-
let buffered = Buffer.alloc(0);
|
|
1321
|
-
for await (const chunk of options.input ?? defaultInput()) {
|
|
1322
|
-
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
|
|
1323
|
-
buffered = Buffer.concat([buffered, bytes]);
|
|
1324
|
-
if (buffered.byteLength > maximumMessageBytes && !buffered.includes(10)) {
|
|
1325
|
-
buffered = Buffer.alloc(0);
|
|
1326
|
-
await emitResponse(writeLine, failure(null, -32700, "Parse error"));
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
for (;; ) {
|
|
1330
|
-
const newline = buffered.indexOf(10);
|
|
1331
|
-
if (newline === -1)
|
|
1332
|
-
break;
|
|
1333
|
-
let line = buffered.subarray(0, newline);
|
|
1334
|
-
buffered = buffered.subarray(newline + 1);
|
|
1335
|
-
if (line.at(-1) === 13)
|
|
1336
|
-
line = line.subarray(0, -1);
|
|
1337
|
-
if (line.byteLength > maximumMessageBytes) {
|
|
1338
|
-
await emitResponse(writeLine, failure(null, -32700, "Parse error"));
|
|
1339
|
-
} else {
|
|
1340
|
-
await processLine(line, session, writeLine);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
if (buffered.byteLength > 0) {
|
|
1345
|
-
if (buffered.byteLength > maximumMessageBytes) {
|
|
1346
|
-
await emitResponse(writeLine, failure(null, -32700, "Parse error"));
|
|
1347
|
-
} else {
|
|
1348
|
-
await processLine(buffered, session, writeLine);
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
// src/studio/contracts.ts
|
|
1353
|
-
import { z as z2 } from "zod";
|
|
1354
|
-
|
|
1355
|
-
// src/studio/shared.ts
|
|
1356
|
-
import { z } from "zod";
|
|
1357
|
-
var STUDIO_LIMITS = Object.freeze({
|
|
1358
|
-
documentBytes: 32 * 1024 * 1024,
|
|
1359
|
-
documentDepth: 32,
|
|
1360
|
-
documentValues: 500000,
|
|
1361
|
-
sourceFiles: 512,
|
|
1362
|
-
sourceBytes: 4 * 1024 ** 3,
|
|
1363
|
-
outputSpecifications: 32,
|
|
1364
|
-
outputFiles: 25000,
|
|
1365
|
-
outputBytes: 64 * 1024 ** 3,
|
|
1366
|
-
timeoutSeconds: 6 * 60 * 60,
|
|
1367
|
-
frames: 25000,
|
|
1368
|
-
frameIndexExclusive: 1e6,
|
|
1369
|
-
dimension: 8192,
|
|
1370
|
-
pixels: 33554432,
|
|
1371
|
-
parameterBytes: 256 * 1024,
|
|
1372
|
-
parameterDepth: 16,
|
|
1373
|
-
parameterValues: 20000
|
|
1374
|
-
});
|
|
1375
|
-
function studioDocument(schema, name) {
|
|
1376
|
-
return z.preprocess((value) => value === undefined ? undefined : createBoundedJsonValueSnapshot(value, STUDIO_LIMITS.documentBytes, name, { maximumDepth: STUDIO_LIMITS.documentDepth, maximumValues: STUDIO_LIMITS.documentValues }).value, schema);
|
|
1377
|
-
}
|
|
1378
|
-
function parseStudioValue(schema, input) {
|
|
1379
|
-
try {
|
|
1380
|
-
return deepFreezeJson(schema.parse(input));
|
|
1381
|
-
} catch (error) {
|
|
1382
|
-
if (error instanceof SlopcameraCodeError)
|
|
1383
|
-
throw error;
|
|
1384
|
-
throw new SlopcameraCodeError("invalid-data", error instanceof z.ZodError ? error.issues[0]?.message ?? "Invalid studio document." : "Invalid studio document.");
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
function studioHash(domain, value) {
|
|
1388
|
-
return boundedCanonicalJsonSha256({ domain, value }, { maximumBytes: STUDIO_LIMITS.documentBytes, maximumDepth: STUDIO_LIMITS.documentDepth + 2, maximumValues: STUDIO_LIMITS.documentValues + 4 });
|
|
1389
|
-
}
|
|
1390
|
-
var studioCompare = compareUtf16Strings;
|
|
1391
|
-
function studioRequire(condition, message) {
|
|
1392
|
-
if (!condition)
|
|
1393
|
-
throw new SlopcameraCodeError("invalid-data", message);
|
|
1394
|
-
}
|
|
1395
|
-
function pathKey(path) {
|
|
1396
|
-
return path.toLowerCase();
|
|
1397
|
-
}
|
|
1398
|
-
function assertDistinctPaths(paths) {
|
|
1399
|
-
const names = new Set(paths.map(pathKey));
|
|
1400
|
-
studioRequire(names.size === paths.length, "Studio paths collide.");
|
|
1401
|
-
for (const name of names) {
|
|
1402
|
-
for (let offset = name.indexOf("/");offset !== -1; offset = name.indexOf("/", offset + 1)) {
|
|
1403
|
-
studioRequire(!names.has(name.slice(0, offset)), "Studio paths use a file as an ancestor.");
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// src/studio/contracts.ts
|
|
1409
|
-
var StudioDigestSchema = z2.string().regex(/^[a-f0-9]{64}$/u);
|
|
1410
|
-
var StudioEngineSchema = z2.enum(["blender", "manim", "cadquery"]);
|
|
1411
|
-
var StudioPathSchema = z2.string().min(1).max(1024).refine((path) => path.normalize("NFC") === path && !path.startsWith("/") && !/[\\:\u0000-\u001f\u007f]/u.test(path) && path.split("/").every((part) => part !== "" && part !== "." && part !== ".." && !/[. ]$/u.test(part)), "Studio paths must be normalized, contained POSIX-relative names.");
|
|
1412
|
-
var identifier = z2.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u);
|
|
1413
|
-
var stableId = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/u);
|
|
1414
|
-
var sourceFile = z2.strictObject({ path: StudioPathSchema, sha256: StudioDigestSchema, bytes: z2.number().int().safe().nonnegative().max(STUDIO_LIMITS.sourceBytes) });
|
|
1415
|
-
var sourceBundleShape = z2.strictObject({
|
|
1416
|
-
kind: z2.literal("slopcamera.studio-source-bundle"),
|
|
1417
|
-
schemaVersion: z2.literal(1),
|
|
1418
|
-
engine: StudioEngineSchema,
|
|
1419
|
-
entrypoint: z2.discriminatedUnion("kind", [z2.strictObject({ kind: z2.literal("python"), path: StudioPathSchema.refine((path) => path.endsWith(".py")) }), z2.strictObject({ kind: z2.literal("blend"), path: StudioPathSchema.refine((path) => path.endsWith(".blend")) })]),
|
|
1420
|
-
files: z2.array(sourceFile).min(1).max(STUDIO_LIMITS.sourceFiles)
|
|
1421
|
-
}).superRefine((value, context) => {
|
|
1422
|
-
const issue = (message) => context.addIssue({ code: "custom", message });
|
|
1423
|
-
try {
|
|
1424
|
-
assertDistinctPaths(value.files.map((file) => file.path));
|
|
1425
|
-
} catch {
|
|
1426
|
-
issue("Source paths must be distinct and cannot have file ancestors.");
|
|
1427
|
-
}
|
|
1428
|
-
if (value.files.reduce((total, file) => total + file.bytes, 0) > STUDIO_LIMITS.sourceBytes)
|
|
1429
|
-
issue("Source bundle exceeds four GiB.");
|
|
1430
|
-
if (!value.files.some((file) => file.path === value.entrypoint.path && file.bytes > 0))
|
|
1431
|
-
issue("Entrypoint must name a declared nonempty source file.");
|
|
1432
|
-
if (value.entrypoint.kind === "blend" && value.engine !== "blender")
|
|
1433
|
-
issue("Blend entrypoints require Blender.");
|
|
1434
|
-
});
|
|
1435
|
-
var sourceBundle = sourceBundleShape.transform((value) => ({ ...value, files: [...value.files].sort((a, b) => studioCompare(a.path, b.path)) })).pipe(sourceBundleShape);
|
|
1436
|
-
var StudioSourceBundleSchema = studioDocument(sourceBundle, "studio source bundle");
|
|
1437
|
-
var parseStudioSourceBundle = (input) => parseStudioValue(StudioSourceBundleSchema, input);
|
|
1438
|
-
var StudioSourceSpaceSchema = z2.strictObject({ units: z2.enum(["meters", "millimeters", "centimeters"]), upAxis: z2.enum(["x", "y", "z"]), handedness: z2.enum(["right", "left"]) });
|
|
1439
|
-
var StudioOutputRoleSchema = z2.enum(["native-source", "model", "beauty", "auxiliary", "simulation-cache", "audio"]);
|
|
1440
|
-
var StudioOutputFormatSchema = z2.enum(["py", "blend", "usd", "usda", "usdc", "glb", "step", "png", "exr", "mp4", "mov", "webm", "wav", "flac", "mp3", "cache"]);
|
|
1441
|
-
var raster = z2.strictObject({
|
|
1442
|
-
kind: z2.literal("raster"),
|
|
1443
|
-
colorSpace: z2.enum(["srgb", "linear-rec709", "data", "unspecified"]),
|
|
1444
|
-
alpha: z2.enum(["opaque", "straight", "premultiplied", "none"]),
|
|
1445
|
-
dataType: z2.enum(["uint8", "uint16", "float16", "float32"]),
|
|
1446
|
-
channels: z2.array(z2.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u)).min(1).max(32),
|
|
1447
|
-
semantic: z2.enum(["color", "depth", "normal", "object-id", "mask", "custom"]),
|
|
1448
|
-
unit: z2.enum(["unitless", "meters", "millimeters", "centimeters"])
|
|
1449
|
-
}).superRefine((value, context) => {
|
|
1450
|
-
if (new Set(value.channels).size !== value.channels.length)
|
|
1451
|
-
context.addIssue({ code: "custom", message: "Raster channels must be unique." });
|
|
1452
|
-
if (value.semantic !== "color" && value.colorSpace !== "data")
|
|
1453
|
-
context.addIssue({ code: "custom", message: "Non-color raster passes must declare data color space." });
|
|
1454
|
-
if (value.semantic === "color" && value.colorSpace === "data")
|
|
1455
|
-
context.addIssue({ code: "custom", message: "Color output cannot declare data color space." });
|
|
1456
|
-
if (value.semantic !== "depth" && value.unit !== "unitless")
|
|
1457
|
-
context.addIssue({ code: "custom", message: "Only depth passes declare distance units." });
|
|
1458
|
-
});
|
|
1459
|
-
var StudioOutputInterpretationSchema = z2.discriminatedUnion("kind", [
|
|
1460
|
-
raster,
|
|
1461
|
-
z2.strictObject({ kind: z2.literal("model"), sourceSpace: StudioSourceSpaceSchema }),
|
|
1462
|
-
z2.strictObject({ kind: z2.literal("native-source") }),
|
|
1463
|
-
z2.strictObject({ kind: z2.literal("cache"), semantics: z2.literal("opaque-native") }),
|
|
1464
|
-
z2.strictObject({ kind: z2.literal("audio"), sampleRate: z2.number().int().min(8000).max(384000), channels: z2.number().int().min(1).max(32) })
|
|
1465
|
-
]);
|
|
1466
|
-
var outputCommon = { id: stableId, role: StudioOutputRoleSchema, format: StudioOutputFormatSchema, interpretation: StudioOutputInterpretationSchema };
|
|
1467
|
-
var pattern = z2.string().max(1024).refine((value) => value.split("%06d").length === 2 && !value.replace("%06d", "").includes("%") && StudioPathSchema.safeParse(value.replace("%06d", "000000")).success, "Sequence paths require exactly one %06d placeholder in a safe relative path.");
|
|
1468
|
-
function compatibleOutput(value) {
|
|
1469
|
-
if (value.role === "native-source")
|
|
1470
|
-
return ["py", "blend"].includes(value.format) && value.interpretation.kind === "native-source";
|
|
1471
|
-
if (value.role === "model")
|
|
1472
|
-
return ["blend", "usd", "usda", "usdc", "glb", "step"].includes(value.format) && value.interpretation.kind === "model";
|
|
1473
|
-
if (value.role === "simulation-cache")
|
|
1474
|
-
return value.format === "cache" && value.interpretation.kind === "cache";
|
|
1475
|
-
if (value.role === "audio")
|
|
1476
|
-
return ["wav", "flac", "mp3"].includes(value.format) && value.interpretation.kind === "audio";
|
|
1477
|
-
return ["png", "exr", "mp4", "mov", "webm"].includes(value.format) && value.interpretation.kind === "raster" && (value.role !== "beauty" || value.interpretation.semantic === "color");
|
|
1478
|
-
}
|
|
1479
|
-
var StudioOutputSpecSchema = z2.discriminatedUnion("kind", [
|
|
1480
|
-
z2.strictObject({ ...outputCommon, kind: z2.literal("file"), path: StudioPathSchema }),
|
|
1481
|
-
z2.strictObject({ ...outputCommon, kind: z2.literal("sequence"), pathPattern: pattern }),
|
|
1482
|
-
z2.strictObject({ ...outputCommon, kind: z2.literal("directory"), path: StudioPathSchema })
|
|
1483
|
-
]).superRefine((value, context) => {
|
|
1484
|
-
const issue = (message) => context.addIssue({ code: "custom", message });
|
|
1485
|
-
if (!compatibleOutput(value))
|
|
1486
|
-
issue("Output role, format, and interpretation disagree.");
|
|
1487
|
-
if (value.kind === "directory" !== (value.format === "cache"))
|
|
1488
|
-
issue("Only native cache outputs use directory declarations.");
|
|
1489
|
-
if (value.kind === "sequence" && !["png", "exr"].includes(value.format))
|
|
1490
|
-
issue("Numbered sequences support PNG or EXR only.");
|
|
1491
|
-
if (value.kind !== "directory") {
|
|
1492
|
-
const path = value.kind === "sequence" ? value.pathPattern : value.path;
|
|
1493
|
-
if (!path.endsWith(`.${value.format}`) && !(value.format === "step" && path.endsWith(".stp")))
|
|
1494
|
-
issue("Output extension must agree with its declared format.");
|
|
1495
|
-
}
|
|
1496
|
-
if (value.interpretation.kind === "raster") {
|
|
1497
|
-
if (value.format === "png" && !["uint8", "uint16"].includes(value.interpretation.dataType))
|
|
1498
|
-
issue("PNG declares integer sample data.");
|
|
1499
|
-
if (value.format === "exr" && !["float16", "float32"].includes(value.interpretation.dataType))
|
|
1500
|
-
issue("The admitted EXR profile declares floating point sample data.");
|
|
1501
|
-
}
|
|
1502
|
-
});
|
|
1503
|
-
var frameRateShape = z2.strictObject({ numerator: z2.number().int().min(1).max(1e6), denominator: z2.number().int().min(1).max(1e6) }).refine((value) => value.numerator / value.denominator <= 240, "Studio cadence exceeds 240 fps.");
|
|
1504
|
-
var frameRate = frameRateShape.transform((value) => {
|
|
1505
|
-
let { numerator: a, denominator: b } = value;
|
|
1506
|
-
while (b !== 0) {
|
|
1507
|
-
const remainder = a % b;
|
|
1508
|
-
a = b;
|
|
1509
|
-
b = remainder;
|
|
1510
|
-
}
|
|
1511
|
-
return { numerator: value.numerator / a, denominator: value.denominator / a };
|
|
1512
|
-
}).pipe(frameRateShape);
|
|
1513
|
-
var StudioRenderSchema = z2.strictObject({
|
|
1514
|
-
width: z2.number().int().min(1).max(STUDIO_LIMITS.dimension),
|
|
1515
|
-
height: z2.number().int().min(1).max(STUDIO_LIMITS.dimension),
|
|
1516
|
-
frameRate,
|
|
1517
|
-
startFrame: z2.number().int().nonnegative().max(STUDIO_LIMITS.frameIndexExclusive - 1),
|
|
1518
|
-
endFrameExclusive: z2.number().int().positive().max(STUDIO_LIMITS.frameIndexExclusive)
|
|
1519
|
-
}).refine((value) => value.endFrameExclusive > value.startFrame && value.endFrameExclusive - value.startFrame <= STUDIO_LIMITS.frames && value.width * value.height <= STUDIO_LIMITS.pixels, "Render requires a bounded, nonempty half-open frame interval and pixel area.");
|
|
1520
|
-
var StudioEngineOptionsSchema = z2.discriminatedUnion("engine", [
|
|
1521
|
-
z2.strictObject({ engine: z2.literal("blender"), renderer: z2.enum(["cycles", "eevee"]), device: z2.enum(["cpu", "gpu"]), samples: z2.number().int().min(1).max(4096), transparent: z2.boolean(), viewTransform: z2.enum(["AgX", "Standard"]), denoise: z2.boolean(), seed: z2.number().int().min(0).max(4294967295) }),
|
|
1522
|
-
z2.strictObject({ engine: z2.literal("manim"), scene: identifier, renderer: z2.literal("cairo"), transparent: z2.boolean() }),
|
|
1523
|
-
z2.strictObject({ engine: z2.literal("cadquery"), exportVariable: identifier, tolerance: z2.number().finite().positive().max(1), angularTolerance: z2.number().finite().positive().max(Math.PI) })
|
|
1524
|
-
]);
|
|
1525
|
-
var parameters = z2.preprocess((value) => value === undefined ? undefined : createBoundedJsonValueSnapshot(value, STUDIO_LIMITS.parameterBytes, "studio parameters", { maximumDepth: STUDIO_LIMITS.parameterDepth, maximumValues: STUDIO_LIMITS.parameterValues }).value, z2.record(z2.string(), z2.unknown()));
|
|
1526
|
-
var StudioExecutionProfileSchema = z2.strictObject({ trust: z2.literal("trusted-current-user"), isolation: z2.literal("none"), hermetic: z2.literal(false) });
|
|
1527
|
-
var jobShape = z2.strictObject({
|
|
1528
|
-
kind: z2.literal("slopcamera.studio-job"),
|
|
1529
|
-
schemaVersion: z2.literal(1),
|
|
1530
|
-
jobId: z2.string().regex(/^studio_[a-zA-Z0-9][a-zA-Z0-9_-]{0,120}$/u),
|
|
1531
|
-
bundleSha256: StudioDigestSchema,
|
|
1532
|
-
stage: z2.enum(["build", "bake", "render"]),
|
|
1533
|
-
parameters,
|
|
1534
|
-
engine: StudioEngineOptionsSchema,
|
|
1535
|
-
render: StudioRenderSchema.optional(),
|
|
1536
|
-
outputs: z2.array(StudioOutputSpecSchema).min(1).max(STUDIO_LIMITS.outputSpecifications),
|
|
1537
|
-
limits: z2.strictObject({ timeoutSeconds: z2.number().int().min(1).max(STUDIO_LIMITS.timeoutSeconds), maximumOutputBytes: z2.number().int().safe().min(1).max(STUDIO_LIMITS.outputBytes), maximumOutputFiles: z2.number().int().min(1).max(STUDIO_LIMITS.outputFiles) }),
|
|
1538
|
-
execution: StudioExecutionProfileSchema
|
|
1539
|
-
}).superRefine((value, context) => {
|
|
1540
|
-
if (value.render === undefined && (value.stage === "render" && value.engine.engine !== "cadquery" || value.outputs.some((output) => output.kind === "sequence" || output.interpretation.kind === "raster")))
|
|
1541
|
-
context.addIssue({ code: "custom", message: "Raster outputs, render stage, and numbered sequences require explicit dimensions and a frame interval." });
|
|
1542
|
-
if (new Set(value.outputs.map((output) => output.id)).size !== value.outputs.length)
|
|
1543
|
-
context.addIssue({ code: "custom", message: "Output IDs must be unique." });
|
|
1544
|
-
const count = value.outputs.reduce((sum, output) => sum + (output.kind === "sequence" && value.render !== undefined ? value.render.endFrameExclusive - value.render.startFrame : 1), 0);
|
|
1545
|
-
if (count > value.limits.maximumOutputFiles)
|
|
1546
|
-
context.addIssue({ code: "custom", message: "Declared outputs exceed the job's output-file limit." });
|
|
1547
|
-
});
|
|
1548
|
-
var job = jobShape.transform((value) => ({ ...value, outputs: [...value.outputs].sort((a, b) => studioCompare(a.id, b.id)) })).pipe(jobShape);
|
|
1549
|
-
var StudioJobSchema = studioDocument(job, "studio job");
|
|
1550
|
-
var parseStudioJob = (input) => parseStudioValue(StudioJobSchema, input);
|
|
1551
|
-
var StudioCapabilityNameSchema = z2.enum(["python-authoring", "blend-authoring", "build", "bake", "render", "gpu-render", "image-sequence", "beauty-video", "model-export", "auxiliary-passes", "native-cache", "audio-output"]);
|
|
1552
|
-
var capability = z2.strictObject({ name: StudioCapabilityNameSchema, support: z2.enum(["available", "unavailable", "unverified"]), evidence: z2.enum(["probe", "qualification"]), receiptSha256: StudioDigestSchema.optional() }).refine((value) => value.evidence !== "qualification" || value.receiptSha256 !== undefined, "Qualification evidence requires a retained receipt digest.");
|
|
1553
|
-
var runtimeShape = z2.strictObject({
|
|
1554
|
-
kind: z2.literal("slopcamera.studio-runtime"),
|
|
1555
|
-
schemaVersion: z2.literal(1),
|
|
1556
|
-
engine: StudioEngineSchema,
|
|
1557
|
-
tool: z2.strictObject({ name: z2.string().min(1).max(128), version: z2.string().min(1).max(512), executableSha256: StudioDigestSchema }),
|
|
1558
|
-
driverSha256: StudioDigestSchema,
|
|
1559
|
-
environment: z2.strictObject({ fingerprintSha256: StudioDigestSchema, evidence: z2.literal("observed-package-environment"), hermetic: z2.literal(false) }),
|
|
1560
|
-
capabilities: z2.array(capability).max(12)
|
|
1561
|
-
}).refine((value) => new Set(value.capabilities.map((item) => item.name)).size === value.capabilities.length, "Runtime capability names must be unique.");
|
|
1562
|
-
var runtime = runtimeShape.transform((value) => ({ ...value, capabilities: [...value.capabilities].sort((a, b) => studioCompare(a.name, b.name)) })).pipe(runtimeShape);
|
|
1563
|
-
var StudioRuntimeIdentitySchema = studioDocument(runtime, "studio runtime identity");
|
|
1564
|
-
var parseStudioRuntimeIdentity = (input) => parseStudioValue(StudioRuntimeIdentitySchema, input);
|
|
1565
|
-
var StudioOutputArtifactSchema = z2.strictObject({
|
|
1566
|
-
outputId: stableId,
|
|
1567
|
-
path: StudioPathSchema,
|
|
1568
|
-
sha256: StudioDigestSchema,
|
|
1569
|
-
bytes: z2.number().int().safe().positive().max(STUDIO_LIMITS.outputBytes),
|
|
1570
|
-
role: StudioOutputRoleSchema,
|
|
1571
|
-
format: StudioOutputFormatSchema,
|
|
1572
|
-
frame: z2.number().int().nonnegative().max(STUDIO_LIMITS.frameIndexExclusive - 1).optional()
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
// src/studio/plan.ts
|
|
1576
|
-
import { z as z3 } from "zod";
|
|
1577
|
-
var studioSourceBundleSha256 = (input) => studioHash("slopcamera.studio-source-bundle/v1", parseStudioSourceBundle(input));
|
|
1578
|
-
var studioJobSha256 = (input) => studioHash("slopcamera.studio-job/v1", parseStudioJob(input));
|
|
1579
|
-
var studioRuntimeSha256 = (input) => studioHash("slopcamera.studio-runtime/v1", parseStudioRuntimeIdentity(input));
|
|
1580
|
-
function studioOutputPath(output, frame) {
|
|
1581
|
-
if (output.kind === "sequence") {
|
|
1582
|
-
studioRequire(Number.isInteger(frame) && frame >= 0 && frame < STUDIO_LIMITS.frameIndexExclusive, "Sequence output requires an admitted frame index.");
|
|
1583
|
-
return output.pathPattern.replace("%06d", String(frame).padStart(6, "0"));
|
|
1584
|
-
}
|
|
1585
|
-
studioRequire(frame === undefined, "Only sequence outputs have a frame index.");
|
|
1586
|
-
return output.path;
|
|
1587
|
-
}
|
|
1588
|
-
function declaration(job2) {
|
|
1589
|
-
const files = [];
|
|
1590
|
-
const directories = [];
|
|
1591
|
-
for (const output of job2.outputs) {
|
|
1592
|
-
if (output.kind === "directory")
|
|
1593
|
-
directories.push({ id: output.id, path: output.path });
|
|
1594
|
-
else if (output.kind === "file")
|
|
1595
|
-
files.push({ id: output.id, path: output.path });
|
|
1596
|
-
else {
|
|
1597
|
-
studioRequire(job2.render !== undefined, "Sequence requires a render interval.");
|
|
1598
|
-
for (let frame = job2.render.startFrame;frame < job2.render.endFrameExclusive; frame++)
|
|
1599
|
-
files.push({ id: output.id, path: studioOutputPath(output, frame), frame });
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
studioRequire(files.length + directories.length <= job2.limits.maximumOutputFiles, "Declared outputs exceed the output-file budget.");
|
|
1603
|
-
assertDistinctPaths([...files, ...directories].map((item) => item.path));
|
|
1604
|
-
return { files, directories };
|
|
1605
|
-
}
|
|
1606
|
-
function requiredCapabilities(bundle, job2) {
|
|
1607
|
-
const values = new Set([bundle.entrypoint.kind === "blend" ? "blend-authoring" : "python-authoring", job2.stage]);
|
|
1608
|
-
if (job2.engine.engine === "blender" && job2.engine.device === "gpu" && job2.stage === "render")
|
|
1609
|
-
values.add("gpu-render");
|
|
1610
|
-
for (const output of job2.outputs) {
|
|
1611
|
-
if (output.kind === "sequence")
|
|
1612
|
-
values.add("image-sequence");
|
|
1613
|
-
if (output.role === "beauty" && ["mov", "mp4", "webm"].includes(output.format))
|
|
1614
|
-
values.add("beauty-video");
|
|
1615
|
-
if (output.role === "model")
|
|
1616
|
-
values.add("model-export");
|
|
1617
|
-
if (output.role === "auxiliary")
|
|
1618
|
-
values.add("auxiliary-passes");
|
|
1619
|
-
if (output.role === "simulation-cache")
|
|
1620
|
-
values.add("native-cache");
|
|
1621
|
-
if (output.role === "audio")
|
|
1622
|
-
values.add("audio-output");
|
|
1623
|
-
}
|
|
1624
|
-
return [...values].sort(studioCompare);
|
|
1625
|
-
}
|
|
1626
|
-
function derivePlan(input) {
|
|
1627
|
-
const { bundle, job: job2, runtime: runtime2 } = input;
|
|
1628
|
-
const bundleSha256 = studioSourceBundleSha256(bundle), jobSha256 = studioJobSha256(job2);
|
|
1629
|
-
studioRequire(bundleSha256 === job2.bundleSha256, "Job does not bind the exact source bundle.");
|
|
1630
|
-
studioRequire(bundle.engine === job2.engine.engine && (runtime2 === undefined || runtime2.engine === bundle.engine), "Bundle, job, and runtime must use one engine.");
|
|
1631
|
-
studioRequire(bundle.entrypoint.kind !== "blend" || job2.stage === "render", "Blend entrypoints support render stage only; explicit Python authoring owns builds and bakes.");
|
|
1632
|
-
const declared = declaration(job2), required = requiredCapabilities(bundle, job2);
|
|
1633
|
-
const capabilityChecks = required.map((name) => ({ name, support: runtime2 === undefined ? "unbound" : runtime2.capabilities.find((item) => item.name === name)?.support ?? "unverified" }));
|
|
1634
|
-
const readiness = runtime2 === undefined ? "runtime-unbound" : capabilityChecks.some((item) => item.support === "unavailable") ? "capability-unavailable" : capabilityChecks.some((item) => item.support === "unverified") ? "capability-unverified" : "authorization-required";
|
|
1635
|
-
const body = {
|
|
1636
|
-
kind: "slopcamera.studio-plan",
|
|
1637
|
-
schemaVersion: 1,
|
|
1638
|
-
bundle,
|
|
1639
|
-
job: job2,
|
|
1640
|
-
...runtime2 === undefined ? {} : { runtime: runtime2, runtimeSha256: studioRuntimeSha256(runtime2) },
|
|
1641
|
-
bundleSha256,
|
|
1642
|
-
jobSha256,
|
|
1643
|
-
sourceBytes: bundle.files.reduce((total, file) => total + file.bytes, 0),
|
|
1644
|
-
frameCount: job2.render === undefined ? 0 : job2.render.endFrameExclusive - job2.render.startFrame,
|
|
1645
|
-
outputCount: { minimum: declared.files.length + declared.directories.length, maximum: declared.directories.length === 0 ? declared.files.length : job2.limits.maximumOutputFiles },
|
|
1646
|
-
requiredCapabilities: required,
|
|
1647
|
-
capabilityChecks,
|
|
1648
|
-
readiness
|
|
1649
|
-
};
|
|
1650
|
-
return { ...body, planSha256: studioHash("slopcamera.studio-plan/v1", body) };
|
|
1651
|
-
}
|
|
1652
|
-
var plan = z3.strictObject({
|
|
1653
|
-
kind: z3.literal("slopcamera.studio-plan"),
|
|
1654
|
-
schemaVersion: z3.literal(1),
|
|
1655
|
-
bundle: StudioSourceBundleSchema,
|
|
1656
|
-
job: StudioJobSchema,
|
|
1657
|
-
runtime: StudioRuntimeIdentitySchema.optional(),
|
|
1658
|
-
runtimeSha256: StudioDigestSchema.optional(),
|
|
1659
|
-
bundleSha256: StudioDigestSchema,
|
|
1660
|
-
jobSha256: StudioDigestSchema,
|
|
1661
|
-
planSha256: StudioDigestSchema,
|
|
1662
|
-
sourceBytes: z3.number().int().safe().nonnegative().max(STUDIO_LIMITS.sourceBytes),
|
|
1663
|
-
frameCount: z3.number().int().nonnegative().max(STUDIO_LIMITS.frames),
|
|
1664
|
-
outputCount: z3.strictObject({ minimum: z3.number().int().positive().max(STUDIO_LIMITS.outputFiles), maximum: z3.number().int().positive().max(STUDIO_LIMITS.outputFiles) }),
|
|
1665
|
-
requiredCapabilities: z3.array(StudioCapabilityNameSchema).min(1).max(12),
|
|
1666
|
-
capabilityChecks: z3.array(z3.strictObject({ name: StudioCapabilityNameSchema, support: z3.enum(["available", "unavailable", "unverified", "unbound"]) })).min(1).max(12),
|
|
1667
|
-
readiness: z3.enum(["runtime-unbound", "capability-unavailable", "capability-unverified", "authorization-required"])
|
|
1668
|
-
}).superRefine((value, context) => {
|
|
1669
|
-
try {
|
|
1670
|
-
const derived = derivePlan({ bundle: value.bundle, job: value.job, ...value.runtime === undefined ? {} : { runtime: value.runtime } });
|
|
1671
|
-
if (canonicalJson(derived) !== canonicalJson(value))
|
|
1672
|
-
context.addIssue({ code: "custom", message: "Studio plan differs from its canonical source, job, runtime, or admission derivation." });
|
|
1673
|
-
} catch (error) {
|
|
1674
|
-
context.addIssue({ code: "custom", message: error instanceof Error ? error.message : "Invalid studio plan." });
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
var StudioPlanSchema = studioDocument(plan, "studio plan");
|
|
1678
|
-
var parseStudioPlan = (input) => parseStudioValue(StudioPlanSchema, input);
|
|
1679
|
-
function planStudioJob(input) {
|
|
1680
|
-
const captured = parseStudioValue(studioDocument(z3.strictObject({ bundle: StudioSourceBundleSchema, job: StudioJobSchema, runtime: StudioRuntimeIdentitySchema.optional() }), "studio planning input"), input);
|
|
1681
|
-
return parseStudioPlan(derivePlan(captured));
|
|
1682
|
-
}
|
|
1683
|
-
var failure2 = z3.strictObject({ code: z3.enum(["subprocess", "cancelled", "deadline", "validation", "custody", "publication", "unavailable"]), message: z3.string().min(1).max(2048) });
|
|
1684
|
-
var receiptCommon = {
|
|
1685
|
-
kind: z3.literal("slopcamera.studio-receipt"),
|
|
1686
|
-
schemaVersion: z3.literal(1),
|
|
1687
|
-
jobId: z3.string().regex(/^studio_[a-zA-Z0-9][a-zA-Z0-9_-]{0,120}$/u),
|
|
1688
|
-
attemptId: z3.string().regex(/^attempt_[a-zA-Z0-9][a-zA-Z0-9_-]{0,120}$/u),
|
|
1689
|
-
planSha256: StudioDigestSchema,
|
|
1690
|
-
bundleSha256: StudioDigestSchema,
|
|
1691
|
-
jobSha256: StudioDigestSchema,
|
|
1692
|
-
runtime: StudioRuntimeIdentitySchema,
|
|
1693
|
-
runtimeSha256: StudioDigestSchema,
|
|
1694
|
-
startedAt: z3.iso.datetime({ offset: true }),
|
|
1695
|
-
finishedAt: z3.iso.datetime({ offset: true }),
|
|
1696
|
-
outputs: z3.array(StudioOutputArtifactSchema).max(STUDIO_LIMITS.outputFiles)
|
|
1697
|
-
};
|
|
1698
|
-
var receiptShape = z3.discriminatedUnion("state", [
|
|
1699
|
-
z3.strictObject({ ...receiptCommon, state: z3.literal("succeeded"), custody: z3.literal("closed"), exitCode: z3.literal(0) }),
|
|
1700
|
-
z3.strictObject({ ...receiptCommon, state: z3.literal("failed"), custody: z3.literal("closed"), exitCode: z3.number().int().min(-255).max(255).nullable(), failure: failure2 }),
|
|
1701
|
-
z3.strictObject({ ...receiptCommon, state: z3.literal("unknown-custody"), custody: z3.literal("unknown"), exitCode: z3.number().int().min(-255).max(255).nullable(), failure: failure2 })
|
|
1702
|
-
]).superRefine((value, context) => {
|
|
1703
|
-
if (studioRuntimeSha256(value.runtime) !== value.runtimeSha256)
|
|
1704
|
-
context.addIssue({ code: "custom", message: "Receipt runtime digest differs from its evidence." });
|
|
1705
|
-
if (Date.parse(value.finishedAt) < Date.parse(value.startedAt))
|
|
1706
|
-
context.addIssue({ code: "custom", message: "Receipt finishes before it starts." });
|
|
1707
|
-
try {
|
|
1708
|
-
assertDistinctPaths(value.outputs.map((item) => item.path));
|
|
1709
|
-
} catch {
|
|
1710
|
-
context.addIssue({ code: "custom", message: "Receipt output paths collide." });
|
|
1711
|
-
}
|
|
1712
|
-
if (value.outputs.reduce((sum, item) => sum + item.bytes, 0) > STUDIO_LIMITS.outputBytes)
|
|
1713
|
-
context.addIssue({ code: "custom", message: "Receipt exceeds the global output byte bound." });
|
|
1714
|
-
});
|
|
1715
|
-
var receipt = receiptShape.transform((value) => ({ ...value, outputs: [...value.outputs].sort((a, b) => studioCompare(a.path, b.path)) })).pipe(receiptShape);
|
|
1716
|
-
var StudioReceiptSchema = studioDocument(receipt, "studio receipt");
|
|
1717
|
-
var parseStudioReceipt = (input) => parseStudioValue(StudioReceiptSchema, input);
|
|
1718
|
-
function validateStudioReceipt(input) {
|
|
1719
|
-
const captured = parseStudioValue(studioDocument(z3.strictObject({ plan: StudioPlanSchema, receipt: StudioReceiptSchema }), "studio receipt validation input"), input);
|
|
1720
|
-
const { plan: plan2, receipt: receipt2 } = captured;
|
|
1721
|
-
studioRequire(receipt2.jobId === plan2.job.jobId && receipt2.planSha256 === plan2.planSha256 && receipt2.jobSha256 === plan2.jobSha256 && receipt2.bundleSha256 === plan2.bundleSha256, "Receipt identity differs from the exact planned job.");
|
|
1722
|
-
studioRequire(plan2.runtime !== undefined && receipt2.runtimeSha256 === plan2.runtimeSha256 && canonicalJson(receipt2.runtime) === canonicalJson(plan2.runtime), "Execution receipt requires the exact planned runtime binding.");
|
|
1723
|
-
if (receipt2.state === "succeeded")
|
|
1724
|
-
studioRequire(plan2.readiness === "authorization-required", "A successful receipt requires available observed runtime capabilities; consent remains host-owned.");
|
|
1725
|
-
studioRequire(receipt2.outputs.length <= plan2.job.limits.maximumOutputFiles && receipt2.outputs.reduce((total, output) => total + output.bytes, 0) <= plan2.job.limits.maximumOutputBytes, "Receipt exceeds the planned output budget.");
|
|
1726
|
-
const declared = declaration(plan2.job), expected = new Map(declared.files.map((file) => [file.path, file]));
|
|
1727
|
-
const counts = new Map;
|
|
1728
|
-
for (const artifact of receipt2.outputs) {
|
|
1729
|
-
const specification = plan2.job.outputs.find((output) => output.id === artifact.outputId);
|
|
1730
|
-
studioRequire(specification !== undefined, "Receipt contains an undeclared output ID.");
|
|
1731
|
-
studioRequire(artifact.role === specification.role && artifact.format === specification.format, "Receipt output role or format differs from its declaration.");
|
|
1732
|
-
if (specification.kind === "directory") {
|
|
1733
|
-
studioRequire(artifact.path.startsWith(`${specification.path}/`) && artifact.frame === undefined, "Cache artifact must be a file inside its declared directory.");
|
|
1734
|
-
} else {
|
|
1735
|
-
const wanted = expected.get(artifact.path);
|
|
1736
|
-
studioRequire(wanted !== undefined && wanted.id === artifact.outputId && artifact.frame === wanted.frame, "Receipt contains an undeclared file or incorrect frame index.");
|
|
1737
|
-
expected.delete(artifact.path);
|
|
1738
|
-
}
|
|
1739
|
-
counts.set(artifact.outputId, (counts.get(artifact.outputId) ?? 0) + 1);
|
|
1740
|
-
}
|
|
1741
|
-
if (receipt2.state === "succeeded") {
|
|
1742
|
-
studioRequire(expected.size === 0 && declared.directories.every((directory) => (counts.get(directory.id) ?? 0) > 0), "Successful receipt must cover every declared file, frame, and cache directory.");
|
|
1743
|
-
}
|
|
1744
|
-
return deepFreezeJson(receipt2);
|
|
1745
|
-
}
|
|
1746
|
-
function inspectStudioBundle(input) {
|
|
1747
|
-
const bundle = parseStudioSourceBundle(input);
|
|
1748
|
-
return deepFreezeJson({
|
|
1749
|
-
engine: bundle.engine,
|
|
1750
|
-
entrypoint: bundle.entrypoint,
|
|
1751
|
-
bundleSha256: studioSourceBundleSha256(bundle),
|
|
1752
|
-
files: bundle.files.length,
|
|
1753
|
-
sourceBytes: bundle.files.reduce((total, file) => total + file.bytes, 0),
|
|
1754
|
-
executed: false,
|
|
1755
|
-
dependencyDiscovery: "explicit-files-only"
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
function inspectStudioPlan(input) {
|
|
1759
|
-
const plan2 = parseStudioPlan(input);
|
|
1760
|
-
return deepFreezeJson({
|
|
1761
|
-
jobId: plan2.job.jobId,
|
|
1762
|
-
stage: plan2.job.stage,
|
|
1763
|
-
engine: plan2.job.engine.engine,
|
|
1764
|
-
planSha256: plan2.planSha256,
|
|
1765
|
-
sourceBytes: plan2.sourceBytes,
|
|
1766
|
-
frameCount: plan2.frameCount,
|
|
1767
|
-
outputCount: plan2.outputCount,
|
|
1768
|
-
readiness: plan2.readiness,
|
|
1769
|
-
capabilityChecks: plan2.capabilityChecks,
|
|
1770
|
-
execution: plan2.job.execution,
|
|
1771
|
-
limits: plan2.job.limits,
|
|
1772
|
-
executed: false,
|
|
1773
|
-
outputs: plan2.job.outputs.map((output) => ({ id: output.id, role: output.role, format: output.format, kind: output.kind, path: output.kind === "sequence" ? output.pathPattern : output.path }))
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// src/index.ts
|
|
1778
|
-
var slopcameraApi = Object.freeze({
|
|
1779
|
-
artifactSummary,
|
|
1780
|
-
builtInIcons,
|
|
1781
|
-
bundledSkillPath,
|
|
1782
|
-
checkDiagramFile,
|
|
1783
|
-
defineSlopcameraWorkflow,
|
|
1784
|
-
DiagramValidationError,
|
|
1785
|
-
generateSlopcameraImage,
|
|
1786
|
-
generateSlopcameraImageFile,
|
|
1787
|
-
slopcameraGatewayCredentialStatus,
|
|
1788
|
-
slopcameraMcpProtocolVersion,
|
|
1789
|
-
slopcameraMcpServerName,
|
|
1790
|
-
slopcameraMcpTools,
|
|
1791
|
-
slopcameraOperationRegistry,
|
|
1792
|
-
SlopcameraMcpToolRuntime,
|
|
1793
|
-
installSkill,
|
|
1794
|
-
lintDiagram,
|
|
1795
|
-
mcpMaximumRenderedPixels,
|
|
1796
|
-
mcpMaximumScale,
|
|
1797
|
-
mcpSourceByteLimit,
|
|
1798
|
-
parseDiagramSource,
|
|
1799
|
-
parseDiagramSpec,
|
|
1800
|
-
readDiagramFile,
|
|
1801
|
-
renderDiagramFile,
|
|
1802
|
-
renderPng,
|
|
1803
|
-
renderSvg,
|
|
1804
|
-
resolveEdge,
|
|
1805
|
-
resolveDiagramSource,
|
|
1806
|
-
resolveStackLayout,
|
|
1807
|
-
runSlopcameraWorkflow,
|
|
1808
|
-
runMcpServer,
|
|
1809
|
-
searchSlopcameraOperations,
|
|
1810
|
-
serializeTldr,
|
|
1811
|
-
stackLayoutDefaults,
|
|
1812
|
-
StackLayoutError,
|
|
1813
|
-
SlopcameraWorkflowError,
|
|
1814
|
-
vectorizeImage,
|
|
1815
|
-
WorkspaceBoundary,
|
|
1816
|
-
WorkspaceBoundaryError,
|
|
1817
|
-
executeSlopcameraOperation
|
|
1818
|
-
});
|
|
1819
|
-
var diagramApi = slopcameraApi;
|
|
1820
|
-
export { readDiagramFile, checkDiagramFile, renderDiagramFile, artifactSummary, mcpSourceByteLimit, WorkspaceBoundaryError, WorkspaceBoundary, mcpMaximumScale, mcpMaximumRenderedPixels, slopcameraMcpTools, SlopcameraMcpToolRuntime, SLOPCAMERA_VERSION, slopcameraMcpProtocolVersion, slopcameraMcpServerName, runMcpServer, STUDIO_LIMITS, StudioDigestSchema, StudioEngineSchema, StudioPathSchema, StudioSourceBundleSchema, parseStudioSourceBundle, StudioSourceSpaceSchema, StudioOutputRoleSchema, StudioOutputFormatSchema, StudioOutputInterpretationSchema, StudioOutputSpecSchema, StudioRenderSchema, StudioEngineOptionsSchema, StudioExecutionProfileSchema, StudioJobSchema, parseStudioJob, StudioCapabilityNameSchema, StudioRuntimeIdentitySchema, parseStudioRuntimeIdentity, StudioOutputArtifactSchema, studioSourceBundleSha256, studioJobSha256, studioRuntimeSha256, studioOutputPath, StudioPlanSchema, parseStudioPlan, planStudioJob, StudioReceiptSchema, parseStudioReceipt, validateStudioReceipt, inspectStudioBundle, inspectStudioPlan, slopcameraApi, diagramApi };
|