@volter/editor-sdk 0.5.57
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
CORE_ERROR_CODES,
|
|
4
|
+
isOperationError,
|
|
5
|
+
type StructuredOperationError,
|
|
6
|
+
toStructuredIssues,
|
|
7
|
+
} from './errors.js';
|
|
8
|
+
import {
|
|
9
|
+
type ExecutionHost,
|
|
10
|
+
type ExecutionRequirements,
|
|
11
|
+
type PermissionMetadata,
|
|
12
|
+
TOOL_NAMESPACES,
|
|
13
|
+
type ToolContext,
|
|
14
|
+
type ToolNamespace,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
/** One declared, machine-readable failure mode of an operation (§8 B1: "structured error codes and data schemas"). */
|
|
18
|
+
export interface ToolErrorDefinition<TCode extends string = string> {
|
|
19
|
+
code: TCode;
|
|
20
|
+
/** One-line human summary of when this code fires — for docs/help text, never parsed by callers. */
|
|
21
|
+
summary: string;
|
|
22
|
+
/** Optional schema for this code's `data` payload. When present, `dispatch()` validates a thrown ToolError's data against it. */
|
|
23
|
+
data?: z.ZodType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One operation, fully self-describing: identity, both schemas, every
|
|
28
|
+
* declared failure mode, where/how it runs, and its own implementation.
|
|
29
|
+
* Built via `defineTool` (below), which validates the name shape and
|
|
30
|
+
* error-code uniqueness at definition time.
|
|
31
|
+
*/
|
|
32
|
+
export interface ToolDefinition<
|
|
33
|
+
TInput extends z.ZodType = z.ZodType,
|
|
34
|
+
TResult extends z.ZodType = z.ZodType,
|
|
35
|
+
TErrorCode extends string = string,
|
|
36
|
+
> {
|
|
37
|
+
/** Fully qualified dotted name, e.g. "project.scene.read" (§5.7 namespaces). */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Short, one-line summary (for CLI help / listOperations tables). */
|
|
40
|
+
summary: string;
|
|
41
|
+
/** Longer prose description of behavior, side effects, and caveats. */
|
|
42
|
+
description: string;
|
|
43
|
+
/** Zod schema every `dispatch()` input is validated against before `impl` runs. */
|
|
44
|
+
input: TInput;
|
|
45
|
+
/** Zod schema every `impl` return value is validated against before `dispatch()` succeeds. */
|
|
46
|
+
result: TResult;
|
|
47
|
+
/** Every structured failure mode this operation may raise via `ToolError`. */
|
|
48
|
+
errors: ReadonlyArray<ToolErrorDefinition<TErrorCode>>;
|
|
49
|
+
/** Which live contexts this operation needs (project/editor/play/render). */
|
|
50
|
+
requires: ExecutionRequirements;
|
|
51
|
+
/** Which of the three hosts this operation executes on (node / editor-browser / runtime-page). */
|
|
52
|
+
host: ExecutionHost;
|
|
53
|
+
/** True if this operation writes/changes state (files, editor, runtime). */
|
|
54
|
+
mutates: boolean;
|
|
55
|
+
/** True if this operation supports a dry-run mode (mutations only, meaningful subset). */
|
|
56
|
+
supportsDryRun: boolean;
|
|
57
|
+
/** True for jobs that must not ride a short request/response timeout (e.g. `cinematic.render` on the editor relay). */
|
|
58
|
+
longRunning?: boolean;
|
|
59
|
+
/** Coarse permission/risk metadata for gated callers (agents, HTTP/MCP auth). */
|
|
60
|
+
permission: PermissionMetadata;
|
|
61
|
+
/** The actual implementation. Receives already-schema-validated input. */
|
|
62
|
+
impl: (input: z.infer<TInput>, ctx: ToolContext) => Promise<z.infer<TResult>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `listOperations()`'s element shape — every field of `ToolDefinition` except `impl`, so enumerating never risks invoking anything. */
|
|
66
|
+
export type ToolSummary<
|
|
67
|
+
TInput extends z.ZodType = z.ZodType,
|
|
68
|
+
TResult extends z.ZodType = z.ZodType,
|
|
69
|
+
TErrorCode extends string = string,
|
|
70
|
+
> = Omit<ToolDefinition<TInput, TResult, TErrorCode>, 'impl'>;
|
|
71
|
+
|
|
72
|
+
const NAME_PATTERN = /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/;
|
|
73
|
+
|
|
74
|
+
function namespaceOf(name: string): string {
|
|
75
|
+
const dot = name.indexOf('.');
|
|
76
|
+
return dot === -1 ? name : name.slice(0, dot);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Typed helper that builds an `ToolDefinition`. Pure and synchronous —
|
|
81
|
+
* it does not touch a registry (so it can never itself throw "duplicate
|
|
82
|
+
* name"; that check happens at `ToolRegistry.register`, which has the
|
|
83
|
+
* cross-operation state to detect it) — but it DOES validate the two things
|
|
84
|
+
* that are decidable from the definition alone: the name is a valid dotted
|
|
85
|
+
* `namespace.rest` string under one of the four operation namespaces
|
|
86
|
+
* (§5.7), and no two declared error codes on the same operation collide.
|
|
87
|
+
*
|
|
88
|
+
* Exists mainly for type inference: it pins `impl`'s parameter/return types
|
|
89
|
+
* to `z.infer<TInput>` / `z.infer<TResult>` so a mismatched implementation
|
|
90
|
+
* fails to compile rather than failing at runtime.
|
|
91
|
+
*/
|
|
92
|
+
export function defineTool<
|
|
93
|
+
TInput extends z.ZodType,
|
|
94
|
+
TResult extends z.ZodType,
|
|
95
|
+
TErrorCode extends string = string,
|
|
96
|
+
>(def: ToolDefinition<TInput, TResult, TErrorCode>): ToolDefinition<TInput, TResult, TErrorCode> {
|
|
97
|
+
if (!NAME_PATTERN.test(def.name)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`defineTool: "${def.name}" is not a valid dotted operation name ` +
|
|
100
|
+
'(expected e.g. "project.scene.read" — lowercase-leading segments joined by dots).',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const ns = namespaceOf(def.name);
|
|
104
|
+
if (!(TOOL_NAMESPACES as readonly string[]).includes(ns)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`defineTool: "${def.name}" has unknown namespace "${ns}" — expected one of ` +
|
|
107
|
+
`${TOOL_NAMESPACES.join(', ')} (§5.7).`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const seen = new Set<string>();
|
|
111
|
+
for (const err of def.errors) {
|
|
112
|
+
if (seen.has(err.code)) {
|
|
113
|
+
throw new Error(`defineTool: "${def.name}" declares duplicate error code "${err.code}".`);
|
|
114
|
+
}
|
|
115
|
+
seen.add(err.code);
|
|
116
|
+
}
|
|
117
|
+
return def;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `dispatch()`'s result — a discriminated union, never a thrown exception, so every projection (CLI/HTTP/MCP) gets one uniform JSON-able shape for both success and failure. */
|
|
121
|
+
export type ToolOutcome<TResult = unknown> =
|
|
122
|
+
| { ok: true; data: TResult }
|
|
123
|
+
| { ok: false; error: StructuredOperationError };
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalize whatever an `impl` threw into a `StructuredOperationError`.
|
|
127
|
+
* Three cases:
|
|
128
|
+
* 1. A declared `ToolError` whose code IS in `def.errors` and whose
|
|
129
|
+
* `data` (if the code declares a schema) validates — forwarded as-is,
|
|
130
|
+
* `data` replaced by its *parsed* form.
|
|
131
|
+
* 2. A declared code whose `data` fails its own schema — that is itself an
|
|
132
|
+
* implementation bug, surfaced as INVALID_OUTPUT (never silently
|
|
133
|
+
* forwarding unvalidated data).
|
|
134
|
+
* 3. Anything else — an `ToolError` with an undeclared code, a plain
|
|
135
|
+
* `Error`, or a non-Error throw — normalized into INTERNAL_ERROR. The
|
|
136
|
+
* raw exception/message is never used as the identifying `code`, but it
|
|
137
|
+
* IS carried in `message` as well as `data.message`: every projection
|
|
138
|
+
* (the CLI's `vgai tool`, the oclif commands, `vgai screenshot`'s module
|
|
139
|
+
* lane) shows `error.message` and only some of them dump `data`, so a
|
|
140
|
+
* `message` that said nothing but "threw an unstructured exception"
|
|
141
|
+
* hid the one sentence the caller needed ("No editor connected — open
|
|
142
|
+
* the editor in a browser tab, then retry.") behind whichever surface
|
|
143
|
+
* happened to print the whole outcome.
|
|
144
|
+
*/
|
|
145
|
+
function normalizeThrown(def: ToolDefinition, err: unknown): StructuredOperationError {
|
|
146
|
+
if (isOperationError(err)) {
|
|
147
|
+
const declared = def.errors.find((e) => e.code === err.code);
|
|
148
|
+
if (!declared) {
|
|
149
|
+
return {
|
|
150
|
+
code: CORE_ERROR_CODES.INTERNAL_ERROR,
|
|
151
|
+
message: `"${def.name}" threw undeclared error code "${err.code}".`,
|
|
152
|
+
data: { undeclaredCode: err.code, message: err.message },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (declared.data) {
|
|
156
|
+
const parsed = declared.data.safeParse(err.data);
|
|
157
|
+
if (!parsed.success) {
|
|
158
|
+
return {
|
|
159
|
+
code: CORE_ERROR_CODES.INVALID_OUTPUT,
|
|
160
|
+
message:
|
|
161
|
+
`"${def.name}" threw declared code "${err.code}" but its data failed that ` +
|
|
162
|
+
"code's own schema.",
|
|
163
|
+
issues: toStructuredIssues(parsed.error.issues),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { code: err.code, message: err.message, data: parsed.data };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
code: err.code,
|
|
170
|
+
message: err.message,
|
|
171
|
+
...(err.data !== undefined ? { data: err.data } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
return {
|
|
177
|
+
code: CORE_ERROR_CODES.INTERNAL_ERROR,
|
|
178
|
+
message: `"${def.name}" failed: ${message}`,
|
|
179
|
+
data: { message },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function toSummary(def: ToolDefinition): ToolSummary {
|
|
184
|
+
const { impl: _impl, ...summary } = def;
|
|
185
|
+
return summary;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Zod object schemas strip unknown keys by default. That is unsafe at an
|
|
190
|
+
* operation boundary: a misspelled `dryRun` could disappear and let the
|
|
191
|
+
* operation use its write-default. Detect anything parsing removed, at any
|
|
192
|
+
* nested object level, and reject it before the implementation can run.
|
|
193
|
+
*/
|
|
194
|
+
function firstStrippedInputPath(
|
|
195
|
+
raw: unknown,
|
|
196
|
+
parsed: unknown,
|
|
197
|
+
path: Array<string | number> = [],
|
|
198
|
+
): Array<string | number> | null {
|
|
199
|
+
if (Array.isArray(raw) && Array.isArray(parsed)) {
|
|
200
|
+
for (let index = 0; index < raw.length; index++) {
|
|
201
|
+
const stripped = firstStrippedInputPath(raw[index], parsed[index], [...path, index]);
|
|
202
|
+
if (stripped) return stripped;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
raw === null ||
|
|
208
|
+
parsed === null ||
|
|
209
|
+
typeof raw !== 'object' ||
|
|
210
|
+
typeof parsed !== 'object' ||
|
|
211
|
+
Array.isArray(raw) ||
|
|
212
|
+
Array.isArray(parsed)
|
|
213
|
+
) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const parsedRecord = parsed as Record<string, unknown>;
|
|
217
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
218
|
+
if (!Object.hasOwn(parsedRecord, key)) return [...path, key];
|
|
219
|
+
const stripped = firstStrippedInputPath(value, parsedRecord[key], [...path, key]);
|
|
220
|
+
if (stripped) return stripped;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The one registry of operation definitions (§8 B1). Holds definitions
|
|
227
|
+
* keyed by their fully-qualified name; `register` rejects a duplicate name
|
|
228
|
+
* outright (names are unique and stable per the AC), `listOperations`
|
|
229
|
+
* enumerates metadata without ever touching `impl`, and `dispatch` is the
|
|
230
|
+
* single validated call path: input schema -> impl -> result schema, with
|
|
231
|
+
* every failure normalized into `StructuredOperationError`.
|
|
232
|
+
*/
|
|
233
|
+
export class ToolRegistry {
|
|
234
|
+
private readonly definitions = new Map<string, ToolDefinition>();
|
|
235
|
+
|
|
236
|
+
/** Register a definition. Throws synchronously on a duplicate name — names are unique and stable by construction, not by convention. */
|
|
237
|
+
register<TInput extends z.ZodType, TResult extends z.ZodType, TErrorCode extends string>(
|
|
238
|
+
def: ToolDefinition<TInput, TResult, TErrorCode>,
|
|
239
|
+
): void {
|
|
240
|
+
if (this.definitions.has(def.name)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`ToolRegistry.register: "${def.name}" is already registered — operation names ` +
|
|
243
|
+
'must be unique and stable.',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
this.definitions.set(def.name, def as unknown as ToolDefinition);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Enumerate every registered operation's metadata. Never invokes `impl`. */
|
|
250
|
+
listOperations(): ToolSummary[] {
|
|
251
|
+
return [...this.definitions.values()].map(toSummary);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Look up one operation's full definition (including `impl`) by name, or `undefined`. */
|
|
255
|
+
getOperation(name: string): ToolDefinition | undefined {
|
|
256
|
+
return this.definitions.get(name);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
has(name: string): boolean {
|
|
260
|
+
return this.definitions.has(name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validate `input` against the named operation's input schema, run its
|
|
265
|
+
* `impl`, validate the return value against its result schema, and return
|
|
266
|
+
* a uniform `ToolOutcome` — success or a `StructuredOperationError`.
|
|
267
|
+
* Never throws for an expected failure (unknown name, bad input, impl
|
|
268
|
+
* throw, bad output); those are exactly what this method exists to turn
|
|
269
|
+
* into a machine-readable result instead of an exception a caller has to
|
|
270
|
+
* parse prose out of.
|
|
271
|
+
*/
|
|
272
|
+
async dispatch<TResult = unknown>(
|
|
273
|
+
name: string,
|
|
274
|
+
input: unknown,
|
|
275
|
+
ctx: ToolContext = {},
|
|
276
|
+
): Promise<ToolOutcome<TResult>> {
|
|
277
|
+
const def = this.definitions.get(name);
|
|
278
|
+
if (!def) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
error: {
|
|
282
|
+
code: CORE_ERROR_CODES.OPERATION_NOT_FOUND,
|
|
283
|
+
message: `No operation is registered as "${name}".`,
|
|
284
|
+
data: { name },
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const parsedInput = def.input.safeParse(input);
|
|
290
|
+
if (!parsedInput.success) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
error: {
|
|
294
|
+
code: CORE_ERROR_CODES.INVALID_INPUT,
|
|
295
|
+
message: `Input for "${name}" failed schema validation.`,
|
|
296
|
+
issues: toStructuredIssues(parsedInput.error.issues),
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const strippedPath = firstStrippedInputPath(input, parsedInput.data);
|
|
301
|
+
if (strippedPath) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
error: {
|
|
305
|
+
code: CORE_ERROR_CODES.INVALID_INPUT,
|
|
306
|
+
message: `Input for "${name}" contains an unknown field.`,
|
|
307
|
+
issues: [
|
|
308
|
+
{
|
|
309
|
+
path: strippedPath,
|
|
310
|
+
message: 'Unknown input field. Check spelling; unknown fields are never ignored.',
|
|
311
|
+
code: 'unrecognized_key',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let rawResult: unknown;
|
|
319
|
+
try {
|
|
320
|
+
rawResult = await def.impl(parsedInput.data, ctx);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return { ok: false, error: normalizeThrown(def, err) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const parsedResult = def.result.safeParse(rawResult);
|
|
326
|
+
if (!parsedResult.success) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
error: {
|
|
330
|
+
code: CORE_ERROR_CODES.INVALID_OUTPUT,
|
|
331
|
+
message: `Result of "${name}" failed schema validation (implementation bug).`,
|
|
332
|
+
issues: toStructuredIssues(parsedResult.error.issues),
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { ok: true, data: parsedResult.data as TResult };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export type { ExecutionHost, ExecutionRequirements, ToolContext, ToolNamespace };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for the operation registry (B1).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** The four namespaces every operation name must live under (§5.7). */
|
|
6
|
+
export const TOOL_NAMESPACES = ['project', 'editor', 'play', 'cinematic'] as const;
|
|
7
|
+
export type ToolNamespace = (typeof TOOL_NAMESPACES)[number];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Which of the three hosts an operation executes on (§8 B1, the
|
|
11
|
+
* review-hardened addition):
|
|
12
|
+
* - `node` — runs in the SDK's own Node process against files (or, for
|
|
13
|
+
* long-running jobs like `cinematic.render`, launches its own browser).
|
|
14
|
+
* - `editor-browser` — invoked through the existing
|
|
15
|
+
* `POST /__editor/command` SSE relay in
|
|
16
|
+
* `packages/editor/server/editor-server.ts`, which broadcasts to the
|
|
17
|
+
* connected browser editor and awaits its callback under a short timeout
|
|
18
|
+
* (~5s; 120s for `play`). The projection generates the relay stub.
|
|
19
|
+
* - `runtime-page` — invoked through the play/render harness in a launched
|
|
20
|
+
* runtime page.
|
|
21
|
+
*
|
|
22
|
+
* `cinematic.render` is pinned `node` and MUST NOT ride the editor relay's
|
|
23
|
+
* short timeout — it is a long-running job (see `longRunning` on
|
|
24
|
+
* `ToolDefinition`).
|
|
25
|
+
*/
|
|
26
|
+
export type ExecutionHost = 'node' | 'editor-browser' | 'runtime-page';
|
|
27
|
+
|
|
28
|
+
/** Which live contexts an operation needs before it can run. */
|
|
29
|
+
export interface ExecutionRequirements {
|
|
30
|
+
/** Needs a project directory on disk (no editor process required). */
|
|
31
|
+
project?: boolean;
|
|
32
|
+
/** Needs an existing, connected editor session. */
|
|
33
|
+
editor?: boolean;
|
|
34
|
+
/** Needs a playable runtime (starts or targets one). */
|
|
35
|
+
play?: boolean;
|
|
36
|
+
/** Needs a controlled render runtime. */
|
|
37
|
+
render?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Coarse risk classification surfaced to permission-gated callers (agents, HTTP/MCP auth). */
|
|
41
|
+
export type PermissionRisk = 'read' | 'write' | 'destructive';
|
|
42
|
+
|
|
43
|
+
export interface PermissionMetadata {
|
|
44
|
+
risk: PermissionRisk;
|
|
45
|
+
/** Human summary of what this operation is permitted to touch or do. */
|
|
46
|
+
summary: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One completed file handed to the host-managed generated-output boundary. */
|
|
50
|
+
export interface ProjectOutputFile {
|
|
51
|
+
/** Project-relative destination. Slice 1 deliberately permits only public/**. */
|
|
52
|
+
path: string;
|
|
53
|
+
content: string | Uint8Array;
|
|
54
|
+
mediaType?: string;
|
|
55
|
+
role?: 'asset' | 'provenance' | 'other';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** JSON-safe summary returned after an atomic generated-output transaction. */
|
|
59
|
+
export interface ProjectGeneratedOutputFile {
|
|
60
|
+
path: string;
|
|
61
|
+
bytes: number;
|
|
62
|
+
mediaType?: string;
|
|
63
|
+
role?: ProjectOutputFile['role'];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ProjectGeneratedOutput {
|
|
67
|
+
files: ProjectGeneratedOutputFile[];
|
|
68
|
+
totalBytes: number;
|
|
69
|
+
dryRun: boolean;
|
|
70
|
+
/** Host-created logical record in .vgai/provenance.json; absent for dry runs. */
|
|
71
|
+
provenanceOperationId?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProjectProviderExecution {
|
|
75
|
+
mode: 'mock' | 'direct' | 'managed';
|
|
76
|
+
provider: string;
|
|
77
|
+
operation?: string;
|
|
78
|
+
model?: string;
|
|
79
|
+
requestId?: string;
|
|
80
|
+
taskId?: string;
|
|
81
|
+
managedJobId?: string;
|
|
82
|
+
/** Route-aware charge snapshot when this execution produced accepted output. */
|
|
83
|
+
billing?: import('../generations.js').GenerationBilling;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Facts the operation host already owns and stamps onto every committed batch. */
|
|
87
|
+
export interface ProjectOutputProvenanceContext {
|
|
88
|
+
operationName: string;
|
|
89
|
+
operationSource?: string;
|
|
90
|
+
/** Sanitized facts for a single provider execution. */
|
|
91
|
+
execution?: ProjectProviderExecution;
|
|
92
|
+
/** Ordered facts for a native multi-task pipeline that produced one output batch. */
|
|
93
|
+
executions?: readonly ProjectProviderExecution[];
|
|
94
|
+
/** WHICH editor session produced the bytes, for a `project.session.write`
|
|
95
|
+
* batch — the kind whose producer is a script an agent ran inside a live
|
|
96
|
+
* session rather than an operation that ran here. Shape and meaning:
|
|
97
|
+
* `project/provenance.ts`'s `ProjectProvenanceSessionSchema`, which is the
|
|
98
|
+
* validated form; this mirror exists because `types.ts` carries no zod. */
|
|
99
|
+
session?: {
|
|
100
|
+
id: string;
|
|
101
|
+
port: number;
|
|
102
|
+
revision: number;
|
|
103
|
+
callId?: string;
|
|
104
|
+
};
|
|
105
|
+
/** Schema-validated, JSON-safe operation input. */
|
|
106
|
+
input?: unknown;
|
|
107
|
+
/** Project-relative paths of the project FILES these bytes were produced
|
|
108
|
+
* from — a session-written GLB names the `src/models/<name>.blend` it was
|
|
109
|
+
* exported from. Shape and meaning: `project/provenance.ts`'s `inputs`,
|
|
110
|
+
* which is the validated form; this mirror exists because `types.ts`
|
|
111
|
+
* carries no zod. It is what lets a reader drill DOWN by kind from a
|
|
112
|
+
* prefab's glTF to the model it came from. */
|
|
113
|
+
inputs?: readonly string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Host capability supplied only to Node-hosted project operations. Generator
|
|
118
|
+
* implementations may stay ordinary native JS; their thin operation wrapper
|
|
119
|
+
* calls this once it has a complete batch ready to commit.
|
|
120
|
+
*/
|
|
121
|
+
export interface ProjectOutputWriter {
|
|
122
|
+
write(
|
|
123
|
+
files: readonly ProjectOutputFile[],
|
|
124
|
+
options?: { dryRun?: boolean },
|
|
125
|
+
): Promise<ProjectGeneratedOutput>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Runtime context passed to every operation's `impl`. Intentionally an open
|
|
130
|
+
* record (`[key: string]: unknown`) — later units (B2-B8) will grow this with
|
|
131
|
+
* concrete fields (editor session ids, play session handles, render job
|
|
132
|
+
* state); B1 only needs the two seams its sample operations touch.
|
|
133
|
+
*/
|
|
134
|
+
export interface ToolContext {
|
|
135
|
+
/** Absolute path to the target project's root, for `project`-context ops. */
|
|
136
|
+
projectRoot?: string;
|
|
137
|
+
/** Base URL of a connected editor session, for `editor`-context ops. */
|
|
138
|
+
editorUrl?: string;
|
|
139
|
+
/** Cooperative cancellation for long-running (`node`, `longRunning`) ops. */
|
|
140
|
+
signal?: AbortSignal;
|
|
141
|
+
/** Atomic writer for generated project assets (Node project operations). */
|
|
142
|
+
projectOutputs?: ProjectOutputWriter;
|
|
143
|
+
/** WHICH mounted instance a game-driving tool should address, when the
|
|
144
|
+
* editor has several live (multiplayer authoring). A tool that drives the
|
|
145
|
+
* game binds `game.instance(ctx.instance)` from it; omitted means the sole
|
|
146
|
+
* live instance. */
|
|
147
|
+
instance?: string;
|
|
148
|
+
/** The HOST's project-module loader (the editor server's Vite SSR loader,
|
|
149
|
+
* wrapped with dependency-change invalidation). A tool that imports the
|
|
150
|
+
* project's own source at run time — `project.bake.preview` importing
|
|
151
|
+
* `src/models/barrel.ts` — MUST load through this rather than a raw
|
|
152
|
+
* `import()`: Node's ESM cache never invalidates, so a raw import returned
|
|
153
|
+
* the FIRST version of a model for the life of the server and every later
|
|
154
|
+
* edit re-rendered byte-identical until `vgai restart` (measured on the
|
|
155
|
+
* blind modeling bench, 2026-09-05: three restarts in one barrel). Absent
|
|
156
|
+
* only when the tool runs outside an editor server. */
|
|
157
|
+
loadProjectModule?: (absolutePath: string) => Promise<unknown>;
|
|
158
|
+
[key: string]: unknown;
|
|
159
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE STAGE TRANSPORT'S PUBLISHED TYPES — the vocabulary a contribution needs
|
|
3
|
+
* to attach something to a stage's transport and to draw a look over it.
|
|
4
|
+
*
|
|
5
|
+
* Declared HERE rather than beside the implementation
|
|
6
|
+
* (`@editor/animation/stage-transport`) so `@vgai/blender` — and any other
|
|
7
|
+
* skew package — reaches them through `@volter/editor-sdk/host` with no
|
|
8
|
+
* dependency on the editor's own source and no engine value import. The
|
|
9
|
+
* implementation imports these; nothing imports the implementation.
|
|
10
|
+
*
|
|
11
|
+
* TIME IS A POSITION ON FACTS A WORLD OWNS, AND A POSITION HAS A DRIVER
|
|
12
|
+
* (WORK.md §The stage transport and the animation door). In Edit, time is
|
|
13
|
+
* WRITTEN: nothing is running, and "show me frame 24" originates in the
|
|
14
|
+
* transport and is seeked down into the world. In Play, time is READ: the
|
|
15
|
+
* game's loop advances its own time and the editor may not write it — the
|
|
16
|
+
* transport's `driver` says which of the two is true, and the write verbs
|
|
17
|
+
* refuse by name while it answers `'world'`.
|
|
18
|
+
*
|
|
19
|
+
* SECONDS ARE THE SEAM. `fps` is a subject's DISPLAY rate, carried so a look
|
|
20
|
+
* that thinks in frames (Blender's Timeline) can convert at its own edge and
|
|
21
|
+
* nowhere else. Keys are deliberately NOT here: Blender's authored keys are
|
|
22
|
+
* the door's key COLUMNS rather than a per-frame bake, and its bone names
|
|
23
|
+
* break three's `PropertyBinding` grammar, so no honest common drawing
|
|
24
|
+
* projection exists — each look draws its own from its own door.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What a world attaches to a stage so the transport can show it at a time.
|
|
29
|
+
*
|
|
30
|
+
* A subject is the world's own object: `seek` calls the world's own code
|
|
31
|
+
* (Blender's `mixer.setTime` through the wire, three's `AnimationMixer`).
|
|
32
|
+
* The transport owns the position; the subject owns what that position MEANS.
|
|
33
|
+
*/
|
|
34
|
+
export interface TransportSubject {
|
|
35
|
+
/** Stable for the lifetime of the attachment. Blender: the action name;
|
|
36
|
+
* three: the clip name (or the object's uuid when it carries several). */
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
/** The subject's own valid time domain and its display rate. */
|
|
40
|
+
range(): { start: number; end: number; fps: number };
|
|
41
|
+
/** THE ONE WRITE. Must be idempotent and synchronous: the transport calls
|
|
42
|
+
* it for every crossing, including a re-seek to the time already shown. */
|
|
43
|
+
seek(seconds: number): void;
|
|
44
|
+
/** Optional — which of several clips this subject is showing (three).
|
|
45
|
+
* Blender omits it: an action IS the subject there. */
|
|
46
|
+
clips?(): readonly { id: string; label: string }[];
|
|
47
|
+
setClip?(id: string): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type TransportPlaybackState = 'stopped' | 'paused' | 'playing';
|
|
51
|
+
|
|
52
|
+
/** Everything a look needs to draw the transport, in one immutable read. */
|
|
53
|
+
export interface StageTransportSnapshot {
|
|
54
|
+
readonly time: number;
|
|
55
|
+
readonly range: { start: number; end: number; fps: number; loop: boolean };
|
|
56
|
+
readonly playbackState: TransportPlaybackState;
|
|
57
|
+
readonly timeScale: number;
|
|
58
|
+
readonly activeSubject: string | null;
|
|
59
|
+
readonly subjects: readonly { id: string; label: string }[];
|
|
60
|
+
/**
|
|
61
|
+
* `'world'` ⇔ this stage's store is in Play, so the game's own loop is
|
|
62
|
+
* advancing time and the editor did not cause it. Every write verb below
|
|
63
|
+
* refuses while this answers `'world'`; a look draws itself read-only and
|
|
64
|
+
* says why rather than disabling a button with no reason.
|
|
65
|
+
*/
|
|
66
|
+
readonly driver: 'editor' | 'world';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The transport as a CONTRIBUTION sees it — the editor's own class minus its
|
|
71
|
+
* construction and disposal, which belong to the stage that owns it.
|
|
72
|
+
*/
|
|
73
|
+
export interface StageTransportHandle {
|
|
74
|
+
snapshot(): StageTransportSnapshot;
|
|
75
|
+
subscribe(listener: () => void): () => void;
|
|
76
|
+
/** Attach a subject; the first attach becomes active. Returns detach. */
|
|
77
|
+
attach(subject: TransportSubject): () => void;
|
|
78
|
+
setActiveSubject(id: string): void;
|
|
79
|
+
/** `'reverse'` plays backwards — Blender's Timeline has a play-reverse
|
|
80
|
+
* button beside play, and the engine's clock has carried the direction all
|
|
81
|
+
* along (`AnimationClock.play(direction)`). */
|
|
82
|
+
play(direction?: 'forward' | 'reverse'): void;
|
|
83
|
+
pause(): void;
|
|
84
|
+
stop(): void;
|
|
85
|
+
seek(seconds: number): void;
|
|
86
|
+
seekFrame(frame: number): void;
|
|
87
|
+
setLoop(loop: boolean): void;
|
|
88
|
+
setTimeScale(scale: number): void;
|
|
89
|
+
/**
|
|
90
|
+
* Fires when the playhead SETTLES — on `pause()`, and at scrub-end (the
|
|
91
|
+
* first 150 ms with no further `seek` while not playing). A world with a
|
|
92
|
+
* persisted playhead (Blender's `scene.frame_current`) writes its bookmark
|
|
93
|
+
* here, so a slider drag writes once instead of once per frame. A `seek`
|
|
94
|
+
* while playing never settles.
|
|
95
|
+
*/
|
|
96
|
+
onSettled(listener: (seconds: number) => void): () => void;
|
|
97
|
+
}
|