@vgai/editor-sdk 0.1.0 → 0.4.0-canary.20260715.0

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 CHANGED
@@ -24,7 +24,10 @@ One export, `EditorClient`, plus its types (`EditorState`, `ProjectInfo`,
24
24
 
25
25
  - Play control: `play`, `stop`, `pause`, `resume`, `step`
26
26
  - Selection: `select(id | null)`, `selectMultiple`, `selectAll`
27
- - Viewport: `focusEntity`, `focusSelection`, `viewPreset`
27
+ - Viewport: `focusEntity`, `focusSelection`, `viewPreset`, `setCamera`,
28
+ `captureViewport`
29
+ - Asset preview: `captureAssetPreview` for deterministic front, right, top,
30
+ and three-quarter captures of a project model or authored entity hierarchy
28
31
  - Panels: `showViewport('scene'|'game')`, `showInspector`, `openAsset`,
29
32
  `closeAsset`, `toggleConsole`, `toggleCommandPalette`, `showBuild`
30
33
  - Display: `setGrid`, `setHelpers`, `setStats`, `setShadingMode`,
@@ -38,9 +41,36 @@ One export, `EditorClient`, plus its types (`EditorState`, `ProjectInfo`,
38
41
  Commands throw on `{ ok: false }` responses, including the server's timeout
39
42
  when no browser editor is connected — failures are never silently swallowed.
40
43
 
44
+ ### Asset Lab capture
45
+
46
+ `captureAssetPreview` accepts exactly one source: an authored entity hierarchy
47
+ already loaded in the editor, or a same-origin project `.glb`/`.gltf` path.
48
+ Rendering happens in an isolated native editor scene and returns deterministic
49
+ front, right, top, and three-quarter PNGs plus a labeled contact sheet:
50
+
51
+ ```ts
52
+ const sketch = await editor.captureAssetPreview(
53
+ { entityId: 'storefront-sketch' },
54
+ { width: 768, height: 768, background: 'neutral' },
55
+ );
56
+
57
+ const model = await editor.captureAssetPreview(
58
+ { assetPath: '/models/storefront.glb' },
59
+ { background: 'transparent' },
60
+ );
61
+ ```
62
+
63
+ Project model input is bounded to 64 MiB and 25 seconds. External `.gltf`
64
+ buffers and images must resolve on the same project origin. The thin CLI
65
+ equivalent writes the four views and contact sheet to disk:
66
+
67
+ ```bash
68
+ vgai asset-preview --asset /models/storefront.glb --out artifacts/storefront
69
+ ```
70
+
41
71
  ## Limitations
42
72
 
43
- - Exports raw TypeScript (`exports: "./src/index.ts"`); consumable only inside
44
- this monorepo under tsx/vite. It is not built or published.
73
+ - The published package exports raw TypeScript (`exports: "./src/index.ts"`),
74
+ so consumers need a TypeScript-aware runner/bundler such as tsx or Vite.
45
75
  - Requires the editor dev server (`npm run dev` or `vgai edit`); there is no
46
76
  offline mode.
package/package.json CHANGED
@@ -2,8 +2,16 @@
2
2
  "name": "@vgai/editor-sdk",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.1.0",
5
+ "version": "0.4.0-canary.20260715.0",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/volter-ai/vgai-engine.git",
10
+ "directory": "packages/editor-sdk"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
7
15
  "files": [
8
16
  "src"
9
17
  ],
package/src/client.ts CHANGED
@@ -1,4 +1,23 @@
1
- import type { AssetKind, EditorState, ProjectInfo, ProjectTemplate, RecentProject, ShadingMode, TransformMode, TransformSpace, ViewPreset } from './types.js';
1
+ import type {
2
+ AssetKind,
3
+ AssetPreviewCapture,
4
+ AssetPreviewOptions,
5
+ AssetPreviewSource,
6
+ EditorState,
7
+ GameCapture,
8
+ HelperVisibility,
9
+ ProjectInfo,
10
+ ProjectOperationCatalog,
11
+ ProjectOperationOutcome,
12
+ ProjectTemplate,
13
+ RecentProject,
14
+ ShadingMode,
15
+ TransformMode,
16
+ TransformSpace,
17
+ Vec3Value,
18
+ ViewPreset,
19
+ ViewportCapture,
20
+ } from './types.js';
2
21
 
3
22
  const DEFAULT_URL = 'http://localhost:5173';
4
23
 
@@ -9,22 +28,30 @@ export class EditorClient {
9
28
  this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
10
29
  }
11
30
 
12
- private async command(body: Record<string, unknown>): Promise<void> {
31
+ private async command<T extends object = Record<string, never>>(
32
+ body: Record<string, unknown>,
33
+ ): Promise<T> {
13
34
  const res = await fetch(`${this.baseUrl}/__editor/command`, {
14
35
  method: 'POST',
15
36
  headers: { 'Content-Type': 'application/json' },
16
37
  body: JSON.stringify(body),
17
38
  });
18
- const data = (await res.json()) as { ok: boolean; error?: string };
39
+ const data = (await res.json()) as { ok: boolean; error?: string } & T;
19
40
  if (!data.ok) {
20
41
  throw new Error(data.error ?? `Editor command failed: ${res.status}`);
21
42
  }
43
+ return data;
22
44
  }
23
45
 
24
46
  // --- Play control ---
25
47
 
26
- async play(): Promise<void> {
27
- await this.command({ type: 'play' });
48
+ /** `opts.seed` (D15/T-D15.6, objection-4 fix) — `vgai play --seed <n>`'s
49
+ * explicit config leg, relayed as `cmd['seed']`; `handleCommand`'s
50
+ * `'play'` case threads it into `enterPlayMode`'s highest-precedence seed
51
+ * argument (beats manifest.determinism.defaultSeed/?vgai-seed=). Omitted,
52
+ * boot seeding falls back to that precedence unchanged. */
53
+ async play(opts?: { seed?: number }): Promise<void> {
54
+ await this.command({ type: 'play', ...(opts?.seed !== undefined ? { seed: opts.seed } : {}) });
28
55
  }
29
56
 
30
57
  async stop(): Promise<void> {
@@ -71,6 +98,74 @@ export class EditorClient {
71
98
  await this.command({ type: 'view-preset', preset });
72
99
  }
73
100
 
101
+ async setCamera(position: Vec3Value, target: Vec3Value, fov?: number): Promise<void> {
102
+ await this.command({
103
+ type: 'set-camera',
104
+ position,
105
+ target,
106
+ ...(fov === undefined ? {} : { fov }),
107
+ });
108
+ }
109
+
110
+ async captureViewport(size?: number): Promise<ViewportCapture> {
111
+ const data = await this.command<ViewportCapture>({
112
+ type: 'capture-viewport',
113
+ ...(size === undefined ? {} : { size }),
114
+ });
115
+ return { base64: data.base64, mimeType: data.mimeType };
116
+ }
117
+
118
+ /**
119
+ * Unit 4 (live-front-door wave) — capture the RUNNING GAME (`vgai
120
+ * screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
121
+ * `@vgai/probe`'s `RelayTransport.screenshot` (and therefore
122
+ * `game.screenshot()` on the relay path) already sends, so all three
123
+ * surfaces composite the identical full game stack — canvas(es) plus the
124
+ * HUD/react DOM layers — rather than any of them inventing a second,
125
+ * subtly-different capture path. Contrast {@link captureViewport}, which
126
+ * captures the EDITOR viewport's canvas and would silently hand back an
127
+ * editor-only (HUD-less, possibly not-even-playing) image.
128
+ *
129
+ * Rejects — loudly, via `command`'s own `{ok:false}` unwrap — when play
130
+ * mode isn't running ("not in play mode — start play before using the
131
+ * debug seam") or no game canvas is mounted yet. Never returns a blank or
132
+ * editor-only frame as a stand-in.
133
+ */
134
+ async captureGame(): Promise<GameCapture> {
135
+ const data = await this.command<GameCapture>({ type: 'bridge-screenshot' });
136
+ const layers = data.layers;
137
+ return {
138
+ base64: data.base64,
139
+ mimeType: data.mimeType,
140
+ composite: data.composite === true,
141
+ ...(layers && Number.isInteger(layers.canvases) && Number.isInteger(layers.domOverlays)
142
+ ? { layers }
143
+ : {}),
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Capture an isolated, deterministic four-view preview through the editor's
149
+ * native Asset Lab. The SDK delegates rendering to the editor; it never
150
+ * loads, clones, or interprets Three.js assets itself.
151
+ */
152
+ async captureAssetPreview(
153
+ source: AssetPreviewSource,
154
+ options: AssetPreviewOptions = {},
155
+ ): Promise<AssetPreviewCapture> {
156
+ const data = await this.command<AssetPreviewCapture>({
157
+ type: 'capture-asset-preview',
158
+ ...source,
159
+ ...options,
160
+ });
161
+ return {
162
+ width: data.width,
163
+ height: data.height,
164
+ views: data.views,
165
+ contactSheet: data.contactSheet,
166
+ };
167
+ }
168
+
74
169
  // --- Panels ---
75
170
 
76
171
  async showViewport(tab: 'scene' | 'game'): Promise<void> {
@@ -119,7 +214,7 @@ export class EditorClient {
119
214
  await this.command({ type: 'set-shading-mode', mode });
120
215
  }
121
216
 
122
- async setHelperType(helperType: string, enabled: boolean): Promise<void> {
217
+ async setHelperType(helperType: keyof HelperVisibility, enabled: boolean): Promise<void> {
123
218
  await this.command({ type: 'set-helper-type', helperType, enabled });
124
219
  }
125
220
 
@@ -145,7 +240,12 @@ export class EditorClient {
145
240
 
146
241
  // --- Project management ---
147
242
 
148
- async createProject(name: string, location: string, template: ProjectTemplate = 'starter', exampleId?: string): Promise<ProjectInfo> {
243
+ async createProject(
244
+ name: string,
245
+ location: string,
246
+ template: ProjectTemplate = 'default',
247
+ exampleId?: string,
248
+ ): Promise<ProjectInfo> {
149
249
  const res = await fetch(`${this.baseUrl}/__editor/create-project`, {
150
250
  method: 'POST',
151
251
  headers: { 'Content-Type': 'application/json' },
@@ -185,12 +285,46 @@ export class EditorClient {
185
285
  return data.projects;
186
286
  }
187
287
 
288
+ // --- Project operations ---
289
+
290
+ /** List live project-local operation definitions below `src/operations/`.
291
+ * The editor server loads metadata in Node; operation modules never enter the
292
+ * editor browser merely because they were listed. */
293
+ async listProjectOperations(): Promise<ProjectOperationCatalog> {
294
+ const res = await fetch(`${this.baseUrl}/__editor/project-operations`);
295
+ if (!res.ok) throw new Error(`Failed to list project operations: ${res.status}`);
296
+ return (await res.json()) as ProjectOperationCatalog;
297
+ }
298
+
299
+ /** Execute one Node-hosted project operation through the existing validated
300
+ * operation registry. Write/destructive operations require `confirm:true`. */
301
+ async runProjectOperation(
302
+ name: string,
303
+ input: unknown = {},
304
+ options: { confirm?: boolean } = {},
305
+ ): Promise<ProjectOperationOutcome> {
306
+ const res = await fetch(`${this.baseUrl}/__editor/project-operations/run`, {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify({ name, input, confirm: options.confirm === true }),
310
+ });
311
+ const body = (await res.json()) as ProjectOperationOutcome;
312
+ if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
313
+ throw new Error(`Project operation returned an invalid response (${res.status}).`);
314
+ }
315
+ return body;
316
+ }
317
+
188
318
  // --- Logs ---
189
319
 
190
- async getLogEntries(): Promise<Array<{ t: number; level: string; msg: string; source?: string }>> {
320
+ async getLogEntries(): Promise<
321
+ Array<{ t: number; level: string; msg: string; source?: string }>
322
+ > {
191
323
  const res = await fetch(`${this.baseUrl}/__editor/log-entries`);
192
324
  if (!res.ok) return [];
193
- const data = (await res.json()) as { entries: Array<{ t: number; level: string; msg: string; source?: string }> };
325
+ const data = (await res.json()) as {
326
+ entries: Array<{ t: number; level: string; msg: string; source?: string }>;
327
+ };
194
328
  return data.entries;
195
329
  }
196
330
 
package/src/index.ts CHANGED
@@ -1,13 +1,26 @@
1
1
  export { EditorClient } from './client.js';
2
2
  export type {
3
3
  AssetKind,
4
+ AssetPreviewBackground,
5
+ AssetPreviewCapture,
6
+ AssetPreviewOptions,
7
+ AssetPreviewSource,
8
+ AssetPreviewView,
9
+ EditorCameraState,
10
+ EditorEntitySummary,
4
11
  EditorState,
12
+ GameCapture,
5
13
  HelperVisibility,
6
14
  ProjectInfo,
15
+ ProjectOperationCatalog,
16
+ ProjectOperationCatalogEntry,
17
+ ProjectOperationOutcome,
7
18
  ProjectTemplate,
8
19
  RecentProject,
9
20
  ShadingMode,
10
21
  TransformMode,
11
22
  TransformSpace,
23
+ Vec3Value,
12
24
  ViewPreset,
25
+ ViewportCapture,
13
26
  } from './types.js';
package/src/types.ts CHANGED
@@ -15,10 +15,87 @@ export interface HelperVisibility {
15
15
  audio: boolean;
16
16
  splines: boolean;
17
17
  navmesh: boolean;
18
+ skeletons: boolean;
19
+ }
20
+
21
+ export interface Vec3Value {
22
+ x: number;
23
+ y: number;
24
+ z: number;
25
+ }
26
+
27
+ export interface EditorCameraState {
28
+ position: Vec3Value;
29
+ target: Vec3Value;
30
+ fov?: number;
31
+ }
32
+
33
+ export interface EditorEntitySummary {
34
+ id: string;
35
+ name: string;
36
+ childIds: string[];
37
+ }
38
+
39
+ export interface ViewportCapture {
40
+ base64: string;
41
+ mimeType: 'image/png';
42
+ }
43
+
44
+ /**
45
+ * Unit 4 (live-front-door wave) — the RUNNING GAME's pixels, as captured by
46
+ * the `bridge-screenshot` relay op (`command-listener.ts`'s
47
+ * `handleBridgeScreenshot`): the full play-mode game stack — canvas(es) PLUS
48
+ * the DOM adapter layers (for example React roots) composited by
49
+ * `capturePlayComposite`. Distinct from {@link ViewportCapture}, which is the
50
+ * EDITOR viewport's own canvas (`capture-viewport`) and knows nothing about
51
+ * play mode or the HUD.
52
+ *
53
+ * `composite` reports honestly whether the DOM-layer composite leg actually
54
+ * ran, or whether the capture degraded to a canvas-only `toDataURL` frame (no
55
+ * container, no DOM `Image`/`XMLSerializer`, or a rasterization failure) —
56
+ * never a silently HUD-less image passed off as the whole game. `layers` is
57
+ * only present on the composite leg.
58
+ */
59
+ export interface GameCapture {
60
+ base64: string;
61
+ mimeType: 'image/png';
62
+ composite: boolean;
63
+ layers?: { canvases: number; domOverlays: number };
64
+ }
65
+
66
+ export type AssetPreviewView = 'front' | 'right' | 'top' | 'perspective';
67
+ export type AssetPreviewBackground = 'neutral' | 'transparent';
68
+
69
+ export type AssetPreviewSource =
70
+ | { assetPath: string; entityId?: never }
71
+ | { entityId: string; assetPath?: never };
72
+
73
+ export interface AssetPreviewOptions {
74
+ width?: number;
75
+ height?: number;
76
+ background?: AssetPreviewBackground;
77
+ }
78
+
79
+ export interface AssetPreviewCapture {
80
+ width: number;
81
+ height: number;
82
+ views: Array<ViewportCapture & { view: AssetPreviewView }>;
83
+ contactSheet: ViewportCapture & { width: number; height: number };
18
84
  }
19
85
 
20
86
  export interface EditorState {
21
87
  playState: 'stopped' | 'playing' | 'paused';
88
+ /**
89
+ * Issue #175 — the REAL engine `GameLoop.liveness` behind the current play
90
+ * session, distinct from `playState` above (editor UI state — a store
91
+ * flag that never reflected whether the loop was actually ticking).
92
+ * `'hidden-paused'` means the T2.1 idle throttle stopped the loop because
93
+ * the editor tab is backgrounded — deliberate and reversible, but the
94
+ * game is advancing ZERO ticks right now even though `playState` still
95
+ * reads `'playing'`. `null` while not in play mode (no loop to report on)
96
+ * or against an older server that predates this field.
97
+ */
98
+ loopLiveness?: 'running' | 'hidden-paused' | 'stopped' | null;
22
99
  selectedEntityId: string | null;
23
100
  selectedEntityIds: string[];
24
101
  activeViewportTab: 'scene' | 'game';
@@ -33,6 +110,12 @@ export interface EditorState {
33
110
  snapEnabled: boolean;
34
111
  entityCount: number;
35
112
  savePath: string | null;
113
+ /** Live editor persistence state. Wait for `saved` before an external scene-file write. */
114
+ saveState: 'saved' | 'unsaved' | 'failed';
115
+ /** Current editor camera pose when the viewport has bound a camera. */
116
+ camera?: EditorCameraState;
117
+ /** Flattened live authoring hierarchy, useful for agent entity discovery. */
118
+ entities?: EditorEntitySummary[];
36
119
  /**
37
120
  * Live count of editor browser tabs holding an SSE connection right now.
38
121
  * The rest of this object is the last snapshot a browser POSTed and persists
@@ -43,6 +126,44 @@ export interface EditorState {
43
126
  editorsConnected?: number;
44
127
  /** Convenience: `editorsConnected > 0`. */
45
128
  connected?: boolean;
129
+ /**
130
+ * Epoch ms when the server last received a state POST from a browser tab —
131
+ * i.e. the age of the cached snapshot above. Omitted if no tab has ever
132
+ * posted state this server lifetime (or since the last project switch,
133
+ * which clears the cache). Present regardless of `connected`, but only
134
+ * meaningful for interpreting staleness when `connected` is `false`.
135
+ */
136
+ stateUpdatedAt?: number;
137
+ /**
138
+ * Validate-on-change (#103): per-file validation status for every
139
+ * `*.vscn.json`/`*.prefab.json`/`vgai.game.json` the dev server has seen
140
+ * change since it booted (or since the last project switch). Server-
141
+ * computed — unlike the rest of `EditorState`, it is NOT part of the
142
+ * browser-POSTed snapshot, so it is always current. A file appears here
143
+ * ONLY while it is currently failing; a clean write removes its entry
144
+ * (absence means "not known to be invalid", not "never checked"). Always
145
+ * present (`{}` when nothing is failing) so `vgai status` consumers can
146
+ * read it unconditionally.
147
+ */
148
+ projectValidation?: Record<string, { errors: string[]; at: number }>;
149
+ /**
150
+ * #124: the absolute path of the project this editor server currently has
151
+ * open — server-computed (never part of the browser-POSTed snapshot,
152
+ * exactly like `projectValidation` above), so it is always current. Added
153
+ * so a watcher/relay holding only a port number (e.g. an agent that
154
+ * printed a `vgai edit` URL earlier and lost track of which project it
155
+ * belongs to) can identify which project that port serves without also
156
+ * reading the `~/.vgai/editor-sessions.json` registry file. `null` when no
157
+ * project is open (the in-repo "no project selected" default server
158
+ * state — mirrors `/__editor/project`'s own `{ project: null }` shape).
159
+ */
160
+ projectRoot?: string | null;
161
+ /**
162
+ * #124: the open project's declared name, alongside `projectRoot` above
163
+ * (`vgai.game.json`'s `name`). `null` when no project is open, or the open
164
+ * project has no readable manifest name.
165
+ */
166
+ projectName?: string | null;
46
167
  }
47
168
 
48
169
  export type ViewPreset = 'top' | 'front' | 'right' | 'perspective';
@@ -50,7 +171,8 @@ export type ShadingMode = 'solid' | 'wireframe' | 'unlit';
50
171
  export type TransformMode = 'translate' | 'rotate' | 'scale';
51
172
  export type TransformSpace = 'world' | 'local';
52
173
 
53
- export type ProjectTemplate = 'starter' | 'empty' | 'example';
174
+ /** Template ids accepted by the editor's create-project endpoint. */
175
+ export type ProjectTemplate = 'default' | '2d' | 'react' | 'example';
54
176
 
55
177
  export interface ProjectInfo {
56
178
  path: string;
@@ -63,3 +185,36 @@ export interface RecentProject {
63
185
  lastOpened: string;
64
186
  thumbnail?: string;
65
187
  }
188
+
189
+ export interface ProjectOperationCatalogEntry {
190
+ name: string;
191
+ summary: string;
192
+ description: string;
193
+ sourcePath: string;
194
+ inputSchema: unknown;
195
+ resultSchema: unknown;
196
+ errors: Array<{ code: string; summary: string; dataSchema?: unknown }>;
197
+ requires: Record<string, boolean | undefined>;
198
+ host: 'node' | 'editor-browser' | 'runtime-page';
199
+ mutates: boolean;
200
+ supportsDryRun: boolean;
201
+ longRunning: boolean;
202
+ permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
203
+ }
204
+
205
+ export interface ProjectOperationCatalog {
206
+ operations: ProjectOperationCatalogEntry[];
207
+ loadErrors: Array<{ sourcePath: string; message: string }>;
208
+ }
209
+
210
+ export type ProjectOperationOutcome =
211
+ | { ok: true; data: unknown }
212
+ | {
213
+ ok: false;
214
+ error: {
215
+ code: string;
216
+ message: string;
217
+ data?: unknown;
218
+ issues?: Array<{ path?: PropertyKey[]; message?: string; [key: string]: unknown }>;
219
+ };
220
+ };