@vgai/sdk 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/account.ts +5 -4
- package/src/cinematic/capabilities-operations.ts +3 -3
- package/src/cinematic/cue-operations.ts +11 -11
- package/src/cinematic/gsap-operations.ts +5 -5
- package/src/cinematic/index.ts +3 -3
- package/src/cinematic/preview-operations.ts +10 -10
- package/src/cinematic/preview-transport.ts +2 -2
- package/src/cinematic/render-operations.ts +15 -15
- package/src/cinematic/render-transport.ts +2 -2
- package/src/cinematic/theatre-operations.ts +17 -17
- package/src/editor/camera-operations.ts +7 -7
- package/src/editor/console-operations.ts +5 -5
- package/src/editor/hierarchy-operations.ts +5 -5
- package/src/editor/index.ts +3 -3
- package/src/editor/open-operations.ts +10 -10
- package/src/editor/screenshot-operations.ts +5 -5
- package/src/editor/selection-operations.ts +7 -7
- package/src/editor/session-operations.ts +3 -3
- package/src/editor/source-location-operations.ts +6 -6
- package/src/editor/transport.ts +24 -11
- package/src/errors.ts +8 -8
- package/src/generations.ts +12 -1
- package/src/http/http-projection.ts +26 -26
- package/src/index.ts +22 -30
- package/src/mcp/index.ts +2 -2
- package/src/mcp/mcp-projection.ts +25 -25
- package/src/mcp/mcp-server.ts +2 -2
- package/src/operations.ts +5 -5
- package/src/play/control-operations.ts +10 -10
- package/src/play/debug-command-operations.ts +12 -12
- package/src/play/index.ts +3 -3
- package/src/play/input-operations.ts +10 -10
- package/src/play/lifecycle-operations.ts +11 -11
- package/src/play/log-operations.ts +7 -7
- package/src/play/run-ticks-operations.ts +8 -8
- package/src/play/state-operations.ts +9 -9
- package/src/play/status-operations.ts +7 -7
- package/src/play/transport.ts +93 -22
- package/src/project/asset-operations.ts +8 -8
- package/src/project/component-operations.ts +10 -10
- package/src/project/discovery-operations.ts +9 -9
- package/src/project/entity-operations.ts +8 -8
- package/src/project/index.ts +4 -4
- package/src/project/input-map-operations.ts +9 -9
- package/src/project/inspection-operation.ts +3 -3
- package/src/project/manifest-operations.ts +9 -9
- package/src/project/scene-operations.ts +14 -14
- package/src/project/shared.ts +17 -17
- package/src/project-tool-catalog.ts +21 -5
- package/src/registry.ts +39 -39
- package/src/render/render-cinematic.ts +24 -1
- package/src/tools.ts +19 -12
- package/src/types.ts +4 -4
package/src/project/shared.ts
CHANGED
|
@@ -40,24 +40,24 @@ import {
|
|
|
40
40
|
import { dirname, isAbsolute, join, relative, resolve, win32 } from 'node:path';
|
|
41
41
|
import type { SceneEntity } from '@vgai/engine/scene/scene-types';
|
|
42
42
|
import { z } from 'zod';
|
|
43
|
-
import {
|
|
44
|
-
import type {
|
|
45
|
-
import type {
|
|
43
|
+
import { ToolError } from '../errors.js';
|
|
44
|
+
import type { ToolErrorDefinition } from '../registry.js';
|
|
45
|
+
import type { ToolContext } from '../types.js';
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
// Project-root resolution
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
|
|
51
|
-
export const NO_PROJECT_ROOT_ERROR:
|
|
51
|
+
export const NO_PROJECT_ROOT_ERROR: ToolErrorDefinition = {
|
|
52
52
|
code: 'NO_PROJECT_ROOT',
|
|
53
53
|
summary: 'dispatch() was called without ctx.projectRoot.',
|
|
54
54
|
data: z.object({}).describe('No additional data.'),
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
/** Every `project.*` op needs `ctx.projectRoot` — this is the one place that enforces it. */
|
|
58
|
-
export function requireProjectRoot(ctx:
|
|
58
|
+
export function requireProjectRoot(ctx: ToolContext): string {
|
|
59
59
|
if (!ctx.projectRoot) {
|
|
60
|
-
throw new
|
|
60
|
+
throw new ToolError(
|
|
61
61
|
'NO_PROJECT_ROOT',
|
|
62
62
|
'dispatch() was called without ctx.projectRoot.',
|
|
63
63
|
{},
|
|
@@ -66,7 +66,7 @@ export function requireProjectRoot(ctx: OperationContext): string {
|
|
|
66
66
|
return ctx.projectRoot;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export const PATH_OUTSIDE_PROJECT_ERROR:
|
|
69
|
+
export const PATH_OUTSIDE_PROJECT_ERROR: ToolErrorDefinition = {
|
|
70
70
|
code: 'PATH_OUTSIDE_PROJECT',
|
|
71
71
|
summary: 'A given relative path escapes ctx.projectRoot lexically or through a symbolic link.',
|
|
72
72
|
data: z.object({ projectRoot: z.string(), path: z.string() }),
|
|
@@ -85,7 +85,7 @@ export function resolveProjectPath(projectRoot: string, relativePath: string): s
|
|
|
85
85
|
// UNC absolutes even when the SDK process itself is running on POSIX.
|
|
86
86
|
const portablePath = relativePath.replace(/\\/g, '/');
|
|
87
87
|
if (isAbsolute(portablePath) || win32.isAbsolute(relativePath)) {
|
|
88
|
-
throw new
|
|
88
|
+
throw new ToolError(
|
|
89
89
|
'PATH_OUTSIDE_PROJECT',
|
|
90
90
|
`"${relativePath}" must be project-relative, not absolute.`,
|
|
91
91
|
{
|
|
@@ -103,7 +103,7 @@ export function resolveProjectPath(projectRoot: string, relativePath: string): s
|
|
|
103
103
|
rel.startsWith(`..${'\\'}`) ||
|
|
104
104
|
isAbsolute(rel)
|
|
105
105
|
) {
|
|
106
|
-
throw new
|
|
106
|
+
throw new ToolError(
|
|
107
107
|
'PATH_OUTSIDE_PROJECT',
|
|
108
108
|
`"${relativePath}" escapes the project root.`,
|
|
109
109
|
{
|
|
@@ -150,7 +150,7 @@ export function resolveProjectPath(projectRoot: string, relativePath: string): s
|
|
|
150
150
|
// `lstatSync` succeeded but `realpathSync` failed: the path is most
|
|
151
151
|
// commonly a dangling symlink. It must not become a write-through
|
|
152
152
|
// escape when its target later appears.
|
|
153
|
-
throw new
|
|
153
|
+
throw new ToolError(
|
|
154
154
|
'PATH_OUTSIDE_PROJECT',
|
|
155
155
|
`"${relativePath}" contains an unresolved symbolic link.`,
|
|
156
156
|
{ projectRoot, path: relativePath },
|
|
@@ -163,7 +163,7 @@ export function resolveProjectPath(projectRoot: string, relativePath: string): s
|
|
|
163
163
|
physicalRel.startsWith(`..${'\\'}`) ||
|
|
164
164
|
isAbsolute(physicalRel)
|
|
165
165
|
) {
|
|
166
|
-
throw new
|
|
166
|
+
throw new ToolError(
|
|
167
167
|
'PATH_OUTSIDE_PROJECT',
|
|
168
168
|
`"${relativePath}" escapes the project root through a symbolic link.`,
|
|
169
169
|
{ projectRoot, path: relativePath },
|
|
@@ -186,7 +186,7 @@ export interface FileRead {
|
|
|
186
186
|
hash: string;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
export const FILE_NOT_FOUND_ERROR:
|
|
189
|
+
export const FILE_NOT_FOUND_ERROR: ToolErrorDefinition = {
|
|
190
190
|
code: 'FILE_NOT_FOUND',
|
|
191
191
|
summary: 'The referenced project file does not exist on disk.',
|
|
192
192
|
data: z.object({ path: z.string() }),
|
|
@@ -195,7 +195,7 @@ export const FILE_NOT_FOUND_ERROR: ErrorDefinition = {
|
|
|
195
195
|
/** Read a file's raw text + its content hash. Throws the declared `FILE_NOT_FOUND` shape when absent. */
|
|
196
196
|
export function readFileWithHash(absPath: string): FileRead {
|
|
197
197
|
if (!existsSync(absPath)) {
|
|
198
|
-
throw new
|
|
198
|
+
throw new ToolError('FILE_NOT_FOUND', `File not found: ${absPath}`, { path: absPath });
|
|
199
199
|
}
|
|
200
200
|
const raw = readFileSync(absPath, 'utf-8');
|
|
201
201
|
return { raw, hash: sha256Hex(raw) };
|
|
@@ -210,7 +210,7 @@ export function writeFileAtomic(absPath: string, content: string): void {
|
|
|
210
210
|
renameSync(tmp, absPath);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
export const CONFLICT_ERROR:
|
|
213
|
+
export const CONFLICT_ERROR: ToolErrorDefinition = {
|
|
214
214
|
code: 'CONFLICT',
|
|
215
215
|
summary:
|
|
216
216
|
'The target file changed on disk since it was last read (baseHash mismatch) — the write ' +
|
|
@@ -230,7 +230,7 @@ export function checkNotStale(
|
|
|
230
230
|
baseHash: string | undefined,
|
|
231
231
|
): void {
|
|
232
232
|
if (baseHash !== undefined && actualHash !== baseHash) {
|
|
233
|
-
throw new
|
|
233
|
+
throw new ToolError(
|
|
234
234
|
'CONFLICT',
|
|
235
235
|
`"${path}" changed on disk since it was last read (baseHash mismatch) — refusing to overwrite an external change.`,
|
|
236
236
|
{ path, expectedHash: baseHash, actualHash },
|
|
@@ -278,7 +278,7 @@ export function mutationResultSchema<T extends z.ZodType>(afterSchema: T) {
|
|
|
278
278
|
// Scene entity tree walk (read-side helper shared by entity/component ops)
|
|
279
279
|
// ---------------------------------------------------------------------------
|
|
280
280
|
|
|
281
|
-
export const ENTITY_NOT_FOUND_ERROR:
|
|
281
|
+
export const ENTITY_NOT_FOUND_ERROR: ToolErrorDefinition = {
|
|
282
282
|
code: 'ENTITY_NOT_FOUND',
|
|
283
283
|
summary: 'No entity with the given id exists in the scene.',
|
|
284
284
|
data: z.object({ id: z.string() }),
|
|
@@ -300,7 +300,7 @@ export function findEntityById(entities: SceneEntity[], id: string): SceneEntity
|
|
|
300
300
|
export function requireEntityById(entities: SceneEntity[], id: string): SceneEntity {
|
|
301
301
|
const found = findEntityById(entities, id);
|
|
302
302
|
if (!found) {
|
|
303
|
-
throw new
|
|
303
|
+
throw new ToolError(
|
|
304
304
|
'ENTITY_NOT_FOUND',
|
|
305
305
|
`No entity with id "${id}" exists in this scene.`,
|
|
306
306
|
{ id },
|
|
@@ -5,10 +5,18 @@ export type ToolContributionPoint =
|
|
|
5
5
|
| 'generation.result'
|
|
6
6
|
| 'workspace.utility';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* One contribution module, FOUND BY SCANNING — never listed anywhere.
|
|
10
|
+
*
|
|
11
|
+
* Nothing enumerates these. `src/tools/` in project source and `src/` in every
|
|
12
|
+
* `vgai`-declaring dependency are walked for the naming convention
|
|
13
|
+
* (`*.document.tsx`, `*.inspector.tsx`, `*.asset-inspector.tsx`,
|
|
14
|
+
* `*.result.tsx`, `*.utility.tsx`), and the module itself declares everything
|
|
15
|
+
* else: `point`, `title` (or `presentations`), and the `tool` it drives, by
|
|
16
|
+
* name. Its id is derived from this path. A manifest cannot disagree with a
|
|
17
|
+
* module it does not mention.
|
|
18
|
+
*/
|
|
8
19
|
export interface ProjectToolContribution {
|
|
9
|
-
id: string;
|
|
10
|
-
point: ToolContributionPoint;
|
|
11
|
-
title: string;
|
|
12
20
|
/** Project-relative or package-absolute browser module path. */
|
|
13
21
|
entryPath: string;
|
|
14
22
|
}
|
|
@@ -33,8 +41,7 @@ export interface ProjectToolCatalogEntry {
|
|
|
33
41
|
supportsDryRun: boolean;
|
|
34
42
|
longRunning: boolean;
|
|
35
43
|
permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
|
|
36
|
-
generation?: { provider: string; role: 'submit' | 'poll' | 'accept' };
|
|
37
|
-
contributions: ProjectToolContribution[];
|
|
44
|
+
generation?: { provider: string; role: 'submit' | 'poll' | 'cancel' | 'accept' };
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
export interface ProjectToolLoadError {
|
|
@@ -42,7 +49,16 @@ export interface ProjectToolLoadError {
|
|
|
42
49
|
message: string;
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Callables and contribution modules are SIBLINGS, not parent and child.
|
|
54
|
+
*
|
|
55
|
+
* They used to be nested — `tools[].contributions[]` — purely so the browser
|
|
56
|
+
* loader had a tool to hand the component. A document is not owned by one
|
|
57
|
+
* callable, and the nesting made project source structurally unable to present
|
|
58
|
+
* a dependency-provided tool. The module names the tool it drives instead.
|
|
59
|
+
*/
|
|
45
60
|
export interface ProjectToolCatalog {
|
|
46
61
|
tools: ProjectToolCatalogEntry[];
|
|
62
|
+
contributions: ProjectToolContribution[];
|
|
47
63
|
loadErrors: ProjectToolLoadError[];
|
|
48
64
|
}
|
package/src/registry.ts
CHANGED
|
@@ -8,28 +8,28 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
type ExecutionHost,
|
|
10
10
|
type ExecutionRequirements,
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
type
|
|
11
|
+
TOOL_NAMESPACES,
|
|
12
|
+
type ToolContext,
|
|
13
|
+
type ToolNamespace,
|
|
14
14
|
type PermissionMetadata,
|
|
15
15
|
} from './types.js';
|
|
16
16
|
|
|
17
17
|
/** One declared, machine-readable failure mode of an operation (§8 B1: "structured error codes and data schemas"). */
|
|
18
|
-
export interface
|
|
18
|
+
export interface ToolErrorDefinition<TCode extends string = string> {
|
|
19
19
|
code: TCode;
|
|
20
20
|
/** One-line human summary of when this code fires — for docs/help text, never parsed by callers. */
|
|
21
21
|
summary: string;
|
|
22
|
-
/** Optional schema for this code's `data` payload. When present, `dispatch()` validates a thrown
|
|
22
|
+
/** Optional schema for this code's `data` payload. When present, `dispatch()` validates a thrown ToolError's data against it. */
|
|
23
23
|
data?: z.ZodType;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* One operation, fully self-describing: identity, both schemas, every
|
|
28
28
|
* declared failure mode, where/how it runs, and its own implementation.
|
|
29
|
-
* Built via `
|
|
29
|
+
* Built via `defineTool` (below), which validates the name shape and
|
|
30
30
|
* error-code uniqueness at definition time.
|
|
31
31
|
*/
|
|
32
|
-
export interface
|
|
32
|
+
export interface ToolDefinition<
|
|
33
33
|
TInput extends z.ZodType = z.ZodType,
|
|
34
34
|
TResult extends z.ZodType = z.ZodType,
|
|
35
35
|
TErrorCode extends string = string,
|
|
@@ -44,8 +44,8 @@ export interface OperationDefinition<
|
|
|
44
44
|
input: TInput;
|
|
45
45
|
/** Zod schema every `impl` return value is validated against before `dispatch()` succeeds. */
|
|
46
46
|
result: TResult;
|
|
47
|
-
/** Every structured failure mode this operation may raise via `
|
|
48
|
-
errors: ReadonlyArray<
|
|
47
|
+
/** Every structured failure mode this operation may raise via `ToolError`. */
|
|
48
|
+
errors: ReadonlyArray<ToolErrorDefinition<TErrorCode>>;
|
|
49
49
|
/** Which live contexts this operation needs (project/editor/play/render). */
|
|
50
50
|
requires: ExecutionRequirements;
|
|
51
51
|
/** Which of the three hosts this operation executes on (node / editor-browser / runtime-page). */
|
|
@@ -59,15 +59,15 @@ export interface OperationDefinition<
|
|
|
59
59
|
/** Coarse permission/risk metadata for gated callers (agents, HTTP/MCP auth). */
|
|
60
60
|
permission: PermissionMetadata;
|
|
61
61
|
/** The actual implementation. Receives already-schema-validated input. */
|
|
62
|
-
impl: (input: z.infer<TInput>, ctx:
|
|
62
|
+
impl: (input: z.infer<TInput>, ctx: ToolContext) => Promise<z.infer<TResult>>;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** `listOperations()`'s element shape — every field of `
|
|
66
|
-
export type
|
|
65
|
+
/** `listOperations()`'s element shape — every field of `ToolDefinition` except `impl`, so enumerating never risks invoking anything. */
|
|
66
|
+
export type ToolSummary<
|
|
67
67
|
TInput extends z.ZodType = z.ZodType,
|
|
68
68
|
TResult extends z.ZodType = z.ZodType,
|
|
69
69
|
TErrorCode extends string = string,
|
|
70
|
-
> = Omit<
|
|
70
|
+
> = Omit<ToolDefinition<TInput, TResult, TErrorCode>, 'impl'>;
|
|
71
71
|
|
|
72
72
|
const NAME_PATTERN = /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/;
|
|
73
73
|
|
|
@@ -77,9 +77,9 @@ function namespaceOf(name: string): string {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Typed helper that builds an `
|
|
80
|
+
* Typed helper that builds an `ToolDefinition`. Pure and synchronous —
|
|
81
81
|
* it does not touch a registry (so it can never itself throw "duplicate
|
|
82
|
-
* name"; that check happens at `
|
|
82
|
+
* name"; that check happens at `ToolRegistry.register`, which has the
|
|
83
83
|
* cross-operation state to detect it) — but it DOES validate the two things
|
|
84
84
|
* that are decidable from the definition alone: the name is a valid dotted
|
|
85
85
|
* `namespace.rest` string under one of the four operation namespaces
|
|
@@ -89,31 +89,31 @@ function namespaceOf(name: string): string {
|
|
|
89
89
|
* to `z.infer<TInput>` / `z.infer<TResult>` so a mismatched implementation
|
|
90
90
|
* fails to compile rather than failing at runtime.
|
|
91
91
|
*/
|
|
92
|
-
export function
|
|
92
|
+
export function defineTool<
|
|
93
93
|
TInput extends z.ZodType,
|
|
94
94
|
TResult extends z.ZodType,
|
|
95
95
|
TErrorCode extends string = string,
|
|
96
96
|
>(
|
|
97
|
-
def:
|
|
98
|
-
):
|
|
97
|
+
def: ToolDefinition<TInput, TResult, TErrorCode>,
|
|
98
|
+
): ToolDefinition<TInput, TResult, TErrorCode> {
|
|
99
99
|
if (!NAME_PATTERN.test(def.name)) {
|
|
100
100
|
throw new Error(
|
|
101
|
-
`
|
|
101
|
+
`defineTool: "${def.name}" is not a valid dotted operation name ` +
|
|
102
102
|
'(expected e.g. "project.scene.read" — lowercase-leading segments joined by dots).',
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
105
|
const ns = namespaceOf(def.name);
|
|
106
|
-
if (!(
|
|
106
|
+
if (!(TOOL_NAMESPACES as readonly string[]).includes(ns)) {
|
|
107
107
|
throw new Error(
|
|
108
|
-
`
|
|
109
|
-
`${
|
|
108
|
+
`defineTool: "${def.name}" has unknown namespace "${ns}" — expected one of ` +
|
|
109
|
+
`${TOOL_NAMESPACES.join(', ')} (§5.7).`,
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
const seen = new Set<string>();
|
|
113
113
|
for (const err of def.errors) {
|
|
114
114
|
if (seen.has(err.code)) {
|
|
115
115
|
throw new Error(
|
|
116
|
-
`
|
|
116
|
+
`defineTool: "${def.name}" declares duplicate error code "${err.code}".`,
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
seen.add(err.code);
|
|
@@ -122,25 +122,25 @@ export function defineOperation<
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** `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. */
|
|
125
|
-
export type
|
|
125
|
+
export type ToolOutcome<TResult = unknown> =
|
|
126
126
|
| { ok: true; data: TResult }
|
|
127
127
|
| { ok: false; error: StructuredOperationError };
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Normalize whatever an `impl` threw into a `StructuredOperationError`.
|
|
131
131
|
* Three cases:
|
|
132
|
-
* 1. A declared `
|
|
132
|
+
* 1. A declared `ToolError` whose code IS in `def.errors` and whose
|
|
133
133
|
* `data` (if the code declares a schema) validates — forwarded as-is,
|
|
134
134
|
* `data` replaced by its *parsed* form.
|
|
135
135
|
* 2. A declared code whose `data` fails its own schema — that is itself an
|
|
136
136
|
* implementation bug, surfaced as INVALID_OUTPUT (never silently
|
|
137
137
|
* forwarding unvalidated data).
|
|
138
|
-
* 3. Anything else — an `
|
|
138
|
+
* 3. Anything else — an `ToolError` with an undeclared code, a plain
|
|
139
139
|
* `Error`, or a non-Error throw — normalized into INTERNAL_ERROR. The
|
|
140
140
|
* raw exception/message is tucked into `data.message`, never used as
|
|
141
141
|
* the identifying `code`.
|
|
142
142
|
*/
|
|
143
|
-
function normalizeThrown(def:
|
|
143
|
+
function normalizeThrown(def: ToolDefinition, err: unknown): StructuredOperationError {
|
|
144
144
|
if (isOperationError(err)) {
|
|
145
145
|
const declared = def.errors.find((e) => e.code === err.code);
|
|
146
146
|
if (!declared) {
|
|
@@ -178,7 +178,7 @@ function normalizeThrown(def: OperationDefinition, err: unknown): StructuredOper
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
function toSummary(def:
|
|
181
|
+
function toSummary(def: ToolDefinition): ToolSummary {
|
|
182
182
|
const { impl: _impl, ...summary } = def;
|
|
183
183
|
return summary;
|
|
184
184
|
}
|
|
@@ -228,29 +228,29 @@ function firstStrippedInputPath(
|
|
|
228
228
|
* single validated call path: input schema -> impl -> result schema, with
|
|
229
229
|
* every failure normalized into `StructuredOperationError`.
|
|
230
230
|
*/
|
|
231
|
-
export class
|
|
232
|
-
private readonly definitions = new Map<string,
|
|
231
|
+
export class ToolRegistry {
|
|
232
|
+
private readonly definitions = new Map<string, ToolDefinition>();
|
|
233
233
|
|
|
234
234
|
/** Register a definition. Throws synchronously on a duplicate name — names are unique and stable by construction, not by convention. */
|
|
235
235
|
register<TInput extends z.ZodType, TResult extends z.ZodType, TErrorCode extends string>(
|
|
236
|
-
def:
|
|
236
|
+
def: ToolDefinition<TInput, TResult, TErrorCode>,
|
|
237
237
|
): void {
|
|
238
238
|
if (this.definitions.has(def.name)) {
|
|
239
239
|
throw new Error(
|
|
240
|
-
`
|
|
240
|
+
`ToolRegistry.register: "${def.name}" is already registered — operation names ` +
|
|
241
241
|
'must be unique and stable.',
|
|
242
242
|
);
|
|
243
243
|
}
|
|
244
|
-
this.definitions.set(def.name, def as unknown as
|
|
244
|
+
this.definitions.set(def.name, def as unknown as ToolDefinition);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/** Enumerate every registered operation's metadata. Never invokes `impl`. */
|
|
248
|
-
listOperations():
|
|
248
|
+
listOperations(): ToolSummary[] {
|
|
249
249
|
return [...this.definitions.values()].map(toSummary);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/** Look up one operation's full definition (including `impl`) by name, or `undefined`. */
|
|
253
|
-
getOperation(name: string):
|
|
253
|
+
getOperation(name: string): ToolDefinition | undefined {
|
|
254
254
|
return this.definitions.get(name);
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -261,7 +261,7 @@ export class OperationRegistry {
|
|
|
261
261
|
/**
|
|
262
262
|
* Validate `input` against the named operation's input schema, run its
|
|
263
263
|
* `impl`, validate the return value against its result schema, and return
|
|
264
|
-
* a uniform `
|
|
264
|
+
* a uniform `ToolOutcome` — success or a `StructuredOperationError`.
|
|
265
265
|
* Never throws for an expected failure (unknown name, bad input, impl
|
|
266
266
|
* throw, bad output); those are exactly what this method exists to turn
|
|
267
267
|
* into a machine-readable result instead of an exception a caller has to
|
|
@@ -270,8 +270,8 @@ export class OperationRegistry {
|
|
|
270
270
|
async dispatch<TResult = unknown>(
|
|
271
271
|
name: string,
|
|
272
272
|
input: unknown,
|
|
273
|
-
ctx:
|
|
274
|
-
): Promise<
|
|
273
|
+
ctx: ToolContext = {},
|
|
274
|
+
): Promise<ToolOutcome<TResult>> {
|
|
275
275
|
const def = this.definitions.get(name);
|
|
276
276
|
if (!def) {
|
|
277
277
|
return {
|
|
@@ -336,4 +336,4 @@ export class OperationRegistry {
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
export type { ExecutionHost, ExecutionRequirements,
|
|
339
|
+
export type { ExecutionHost, ExecutionRequirements, ToolContext, ToolNamespace };
|
|
@@ -642,6 +642,21 @@ export async function launchEntryServer(
|
|
|
642
642
|
engineRoot: string,
|
|
643
643
|
onProgress: ((e: RenderCinematicProgress) => void) | undefined,
|
|
644
644
|
signal?: AbortSignal,
|
|
645
|
+
options?: {
|
|
646
|
+
/**
|
|
647
|
+
* Extra args appended to the `npx vite` spawn (G3 fold-in). Motivating
|
|
648
|
+
* case: the render-mode e2e fixtures all pin `server.host: '127.0.0.1'`
|
|
649
|
+
* in their own vite configs, but a real project's config (an
|
|
650
|
+
* `examples/<id>/vite.config.ts`) typically leaves `host` at Vite's
|
|
651
|
+
* `localhost` default — which on an IPv6-first box binds only `::1`,
|
|
652
|
+
* so this function's own `127.0.0.1` readiness probe (and baseUrl)
|
|
653
|
+
* never connects. A caller rendering such a project passes
|
|
654
|
+
* `['--host', '127.0.0.1']` here (see
|
|
655
|
+
* `scripts/generate-learn-thumbnails.ts`). Optional and defaulted:
|
|
656
|
+
* every existing caller is byte-identical in behavior.
|
|
657
|
+
*/
|
|
658
|
+
readonly extraViteArgs?: readonly string[];
|
|
659
|
+
},
|
|
645
660
|
): Promise<LaunchedServer> {
|
|
646
661
|
if (/^https?:\/\//.test(entry)) {
|
|
647
662
|
return { baseUrl: entry.replace(/\/$/, ''), stop: async () => {} };
|
|
@@ -667,7 +682,15 @@ export async function launchEntryServer(
|
|
|
667
682
|
// reading those pipes) never exits even after the render is fully done.
|
|
668
683
|
const child: ChildProcess = spawn(
|
|
669
684
|
'npx',
|
|
670
|
-
[
|
|
685
|
+
[
|
|
686
|
+
'vite',
|
|
687
|
+
'--config',
|
|
688
|
+
configPath,
|
|
689
|
+
'--port',
|
|
690
|
+
String(port),
|
|
691
|
+
'--strictPort',
|
|
692
|
+
...(options?.extraViteArgs ?? []),
|
|
693
|
+
],
|
|
671
694
|
{ cwd: engineRoot, stdio: ['ignore', 'pipe', 'pipe'], detached: true },
|
|
672
695
|
);
|
|
673
696
|
let serverOutput = '';
|
package/src/tools.ts
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public project-tool contract.
|
|
3
3
|
*
|
|
4
|
-
* A tool is an ordinary registered
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* because
|
|
4
|
+
* A tool is an ordinary registered function with a WIRE CONTRACT: declared
|
|
5
|
+
* input/result schemas, structured error codes, and the metadata a caller reads
|
|
6
|
+
* before invoking (`permission`, `host`, `mutates`, `supportsDryRun`). The
|
|
7
|
+
* contract exists because a tool is called across a process boundary — CLI to
|
|
8
|
+
* editor to node host, and `vgai mcp` to an external agent over stdio — where
|
|
9
|
+
* you cannot throw. `ToolRegistry.dispatch` turns every expected failure
|
|
10
|
+
* (unknown name, bad input, an `impl` throw, a bad return) into a typed outcome
|
|
11
|
+
* instead.
|
|
12
|
+
*
|
|
13
|
+
* This module is the surface every consumer imports. It re-exports rather than
|
|
14
|
+
* renames: there is ONE vocabulary, and it is this one.
|
|
8
15
|
*/
|
|
9
|
-
export {
|
|
16
|
+
export { ToolError } from './errors.js';
|
|
10
17
|
export {
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
type
|
|
18
|
+
defineTool,
|
|
19
|
+
type ToolDefinition,
|
|
20
|
+
type ToolErrorDefinition,
|
|
21
|
+
type ToolOutcome,
|
|
22
|
+
ToolRegistry,
|
|
23
|
+
type ToolSummary,
|
|
17
24
|
} from './registry.js';
|
|
18
25
|
export type {
|
|
19
26
|
ExecutionHost as ToolHost,
|
|
20
27
|
ExecutionRequirements as ToolRequirements,
|
|
21
|
-
OperationContext as ToolContext,
|
|
22
28
|
PermissionMetadata as ToolPermission,
|
|
23
29
|
PermissionRisk as ToolPermissionRisk,
|
|
30
|
+
ToolContext,
|
|
24
31
|
} from './types.js';
|
package/src/types.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** The four namespaces every operation name must live under (§5.7). */
|
|
7
|
-
export const
|
|
8
|
-
export type
|
|
7
|
+
export const TOOL_NAMESPACES = ['project', 'editor', 'play', 'cinematic'] as const;
|
|
8
|
+
export type ToolNamespace = (typeof TOOL_NAMESPACES)[number];
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Which of the three hosts an operation executes on (§8 B1, the
|
|
@@ -22,7 +22,7 @@ export type OperationNamespace = (typeof OPERATION_NAMESPACES)[number];
|
|
|
22
22
|
*
|
|
23
23
|
* `cinematic.render` is pinned `node` and MUST NOT ride the editor relay's
|
|
24
24
|
* short timeout — it is a long-running job (see `longRunning` on
|
|
25
|
-
* `
|
|
25
|
+
* `ToolDefinition`).
|
|
26
26
|
*/
|
|
27
27
|
export type ExecutionHost = 'node' | 'editor-browser' | 'runtime-page';
|
|
28
28
|
|
|
@@ -114,7 +114,7 @@ export interface ProjectOutputWriter {
|
|
|
114
114
|
* concrete fields (editor session ids, play session handles, render job
|
|
115
115
|
* state); B1 only needs the two seams its sample operations touch.
|
|
116
116
|
*/
|
|
117
|
-
export interface
|
|
117
|
+
export interface ToolContext {
|
|
118
118
|
/** Absolute path to the target project's root, for `project`-context ops. */
|
|
119
119
|
projectRoot?: string;
|
|
120
120
|
/** Base URL of a connected editor session, for `editor`-context ops. */
|