@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.
Files changed (54) hide show
  1. package/package.json +2 -2
  2. package/src/account.ts +5 -4
  3. package/src/cinematic/capabilities-operations.ts +3 -3
  4. package/src/cinematic/cue-operations.ts +11 -11
  5. package/src/cinematic/gsap-operations.ts +5 -5
  6. package/src/cinematic/index.ts +3 -3
  7. package/src/cinematic/preview-operations.ts +10 -10
  8. package/src/cinematic/preview-transport.ts +2 -2
  9. package/src/cinematic/render-operations.ts +15 -15
  10. package/src/cinematic/render-transport.ts +2 -2
  11. package/src/cinematic/theatre-operations.ts +17 -17
  12. package/src/editor/camera-operations.ts +7 -7
  13. package/src/editor/console-operations.ts +5 -5
  14. package/src/editor/hierarchy-operations.ts +5 -5
  15. package/src/editor/index.ts +3 -3
  16. package/src/editor/open-operations.ts +10 -10
  17. package/src/editor/screenshot-operations.ts +5 -5
  18. package/src/editor/selection-operations.ts +7 -7
  19. package/src/editor/session-operations.ts +3 -3
  20. package/src/editor/source-location-operations.ts +6 -6
  21. package/src/editor/transport.ts +24 -11
  22. package/src/errors.ts +8 -8
  23. package/src/generations.ts +12 -1
  24. package/src/http/http-projection.ts +26 -26
  25. package/src/index.ts +22 -30
  26. package/src/mcp/index.ts +2 -2
  27. package/src/mcp/mcp-projection.ts +25 -25
  28. package/src/mcp/mcp-server.ts +2 -2
  29. package/src/operations.ts +5 -5
  30. package/src/play/control-operations.ts +10 -10
  31. package/src/play/debug-command-operations.ts +12 -12
  32. package/src/play/index.ts +3 -3
  33. package/src/play/input-operations.ts +10 -10
  34. package/src/play/lifecycle-operations.ts +11 -11
  35. package/src/play/log-operations.ts +7 -7
  36. package/src/play/run-ticks-operations.ts +8 -8
  37. package/src/play/state-operations.ts +9 -9
  38. package/src/play/status-operations.ts +7 -7
  39. package/src/play/transport.ts +93 -22
  40. package/src/project/asset-operations.ts +8 -8
  41. package/src/project/component-operations.ts +10 -10
  42. package/src/project/discovery-operations.ts +9 -9
  43. package/src/project/entity-operations.ts +8 -8
  44. package/src/project/index.ts +4 -4
  45. package/src/project/input-map-operations.ts +9 -9
  46. package/src/project/inspection-operation.ts +3 -3
  47. package/src/project/manifest-operations.ts +9 -9
  48. package/src/project/scene-operations.ts +14 -14
  49. package/src/project/shared.ts +17 -17
  50. package/src/project-tool-catalog.ts +21 -5
  51. package/src/registry.ts +39 -39
  52. package/src/render/render-cinematic.ts +24 -1
  53. package/src/tools.ts +19 -12
  54. package/src/types.ts +4 -4
@@ -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 { OperationError } from '../errors.js';
44
- import type { ErrorDefinition } from '../registry.js';
45
- import type { OperationContext } from '../types.js';
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: ErrorDefinition = {
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: OperationContext): string {
58
+ export function requireProjectRoot(ctx: ToolContext): string {
59
59
  if (!ctx.projectRoot) {
60
- throw new OperationError(
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: ErrorDefinition = {
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 OperationError(
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 OperationError(
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 OperationError(
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 OperationError(
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: ErrorDefinition = {
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 OperationError('FILE_NOT_FOUND', `File not found: ${absPath}`, { path: absPath });
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: ErrorDefinition = {
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 OperationError(
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: ErrorDefinition = {
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 OperationError(
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
- OPERATION_NAMESPACES,
12
- type OperationContext,
13
- type OperationNamespace,
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 ErrorDefinition<TCode extends string = string> {
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 OperationError's data against it. */
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 `defineOperation` (below), which validates the name shape and
29
+ * Built via `defineTool` (below), which validates the name shape and
30
30
  * error-code uniqueness at definition time.
31
31
  */
32
- export interface OperationDefinition<
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 `OperationError`. */
48
- errors: ReadonlyArray<ErrorDefinition<TErrorCode>>;
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: OperationContext) => Promise<z.infer<TResult>>;
62
+ impl: (input: z.infer<TInput>, ctx: ToolContext) => Promise<z.infer<TResult>>;
63
63
  }
64
64
 
65
- /** `listOperations()`'s element shape — every field of `OperationDefinition` except `impl`, so enumerating never risks invoking anything. */
66
- export type OperationSummary<
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<OperationDefinition<TInput, TResult, TErrorCode>, 'impl'>;
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 `OperationDefinition`. Pure and synchronous —
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 `OperationRegistry.register`, which has the
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 defineOperation<
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: OperationDefinition<TInput, TResult, TErrorCode>,
98
- ): OperationDefinition<TInput, TResult, TErrorCode> {
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
- `defineOperation: "${def.name}" is not a valid dotted operation name ` +
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 (!(OPERATION_NAMESPACES as readonly string[]).includes(ns)) {
106
+ if (!(TOOL_NAMESPACES as readonly string[]).includes(ns)) {
107
107
  throw new Error(
108
- `defineOperation: "${def.name}" has unknown namespace "${ns}" — expected one of ` +
109
- `${OPERATION_NAMESPACES.join(', ')} (§5.7).`,
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
- `defineOperation: "${def.name}" declares duplicate error code "${err.code}".`,
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 OperationOutcome<TResult = unknown> =
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 `OperationError` whose code IS in `def.errors` and whose
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 `OperationError` with an undeclared code, a plain
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: OperationDefinition, err: unknown): StructuredOperationError {
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: OperationDefinition): OperationSummary {
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 OperationRegistry {
232
- private readonly definitions = new Map<string, OperationDefinition>();
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: OperationDefinition<TInput, TResult, TErrorCode>,
236
+ def: ToolDefinition<TInput, TResult, TErrorCode>,
237
237
  ): void {
238
238
  if (this.definitions.has(def.name)) {
239
239
  throw new Error(
240
- `OperationRegistry.register: "${def.name}" is already registered — operation names ` +
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 OperationDefinition);
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(): OperationSummary[] {
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): OperationDefinition | undefined {
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 `OperationOutcome` — success or a `StructuredOperationError`.
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: OperationContext = {},
274
- ): Promise<OperationOutcome<TResult>> {
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, OperationContext, OperationNamespace };
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
- ['vite', '--config', configPath, '--port', String(port), '--strictPort'],
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 JavaScript/TypeScript function. The SDK's
5
- * operation registry remains the internal validated dispatcher used by every
6
- * projection, but projects should not need a second public ontology merely
7
- * because the host calls their function through a transport.
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 { OperationError as ToolError } from './errors.js';
16
+ export { ToolError } from './errors.js';
10
17
  export {
11
- defineOperation as defineTool,
12
- type ErrorDefinition as ToolErrorDefinition,
13
- type OperationDefinition as ToolDefinition,
14
- type OperationOutcome as ToolOutcome,
15
- OperationRegistry as ToolRegistry,
16
- type OperationSummary as ToolSummary,
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 OPERATION_NAMESPACES = ['project', 'editor', 'play', 'cinematic'] as const;
8
- export type OperationNamespace = (typeof OPERATION_NAMESPACES)[number];
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
- * `OperationDefinition`).
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 OperationContext {
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. */