@vgai/sdk 0.5.2 → 0.5.3

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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/sdk",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.2",
5
+ "version": "0.5.3",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -26,8 +26,8 @@
26
26
  "./tools": "./src/tools.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.29.0",
30
- "@vgai/engine": "0.5.2",
29
+ "@modelcontextprotocol/sdk": "^1.30.0",
30
+ "@vgai/engine": "0.5.3",
31
31
  "playwright": "^1.58.2",
32
32
  "zod": "^4.3.6"
33
33
  }
package/src/account.ts CHANGED
@@ -37,6 +37,23 @@ export const AccountUserSchema = z.object({
37
37
  email: z.string().email(),
38
38
  name: z.string().min(1).optional(),
39
39
  });
40
+ export const AccountOrganizationDomainSchema = z.object({
41
+ name: z.string().min(1),
42
+ verified: z.boolean(),
43
+ enrollmentMode: z.enum([
44
+ 'manual_invitation',
45
+ 'automatic_invitation',
46
+ 'automatic_suggestion',
47
+ 'enterprise_sso',
48
+ ]),
49
+ });
50
+ export const AccountOrganizationSchema = z.object({
51
+ id: z.string().min(1),
52
+ name: z.string().min(1),
53
+ slug: z.string().min(1).optional(),
54
+ role: z.string().min(1),
55
+ domains: z.array(AccountOrganizationDomainSchema).max(50),
56
+ });
40
57
  export const AccountUsageEntrySchema = z.object({
41
58
  id: z.string().min(1),
42
59
  occurredAt: z.string().datetime(),
@@ -53,6 +70,7 @@ export const AccountSnapshotSchema = z.discriminatedUnion('authenticated', [
53
70
  authenticated: z.literal(true),
54
71
  backend: AccountBackendSchema,
55
72
  user: AccountUserSchema,
73
+ organizations: z.array(AccountOrganizationSchema).max(100).optional(),
56
74
  plan: AccountPlanSchema,
57
75
  credits: AccountCreditsSchema,
58
76
  spendPolicy: AccountSpendPolicySchema,
@@ -89,6 +107,8 @@ export type AccountPlan = z.infer<typeof AccountPlanSchema>;
89
107
  export type AccountCredits = z.infer<typeof AccountCreditsSchema>;
90
108
  export type AccountSpendPolicy = z.infer<typeof AccountSpendPolicySchema>;
91
109
  export type AccountUser = z.infer<typeof AccountUserSchema>;
110
+ export type AccountOrganization = z.infer<typeof AccountOrganizationSchema>;
111
+ export type AccountOrganizationDomain = z.infer<typeof AccountOrganizationDomainSchema>;
92
112
  export type AccountUsageEntry = z.infer<typeof AccountUsageEntrySchema>;
93
113
  export type AccountSnapshot = z.infer<typeof AccountSnapshotSchema>;
94
114
  export type GenerationExecutionRoute = z.infer<typeof GenerationExecutionRouteSchema>;
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * DECISION (per this unit's brief — "static/metadata where possible; named
6
6
  * error if runtime-only"): this is STATIC-ONLY, by design, not a wiring gap.
7
- * `registerGsap` (`packages/engine/src/animation/gsap-registration.ts`)
7
+ * `registerGsap` (`packages/editor/catalog/project-source/src/lib/timeline/gsap-registration.ts`)
8
8
  * stores its bookkeeping in a module-private
9
9
  * `WeakMap<AnimationClock, WeakSet<gsap.core.Timeline>>` — deliberately
10
10
  * un-enumerable (no `.list()`/registry object is exported at all, so that
@@ -13,7 +13,7 @@
13
13
  * live surface — local or over any wire protocol — that could ever answer
14
14
  * "what GSAP timelines are registered on this running clock right now",
15
15
  * independent of this unit's file-ownership boundary (that WeakMap is
16
- * `packages/engine/src/animation/gsap-registration.ts`, not
16
+ * `packages/editor/catalog/project-source/src/lib/timeline/gsap-registration.ts`, not
17
17
  * `vgai-sdk`, and adding an enumeration API to it is out of this unit's
18
18
  * scope regardless). So the honest, useful thing this op CAN do is exactly
19
19
  * what B2's `project.xstate.discover` does for XState machines
@@ -77,7 +77,7 @@ export const cinematicGsapInspect = defineTool({
77
77
  'deterministic GSAP timeline.',
78
78
  description:
79
79
  'STATIC-ONLY BY DESIGN, not a wiring gap — see module jsdoc. registerGsap’s bookkeeping is an ' +
80
- 'un-enumerable WeakMap/WeakSet (packages/engine/src/animation/gsap-registration.ts), so no ' +
80
+ 'un-enumerable WeakMap/WeakSet (packages/editor/catalog/project-source/src/lib/timeline/gsap-registration.ts), so no ' +
81
81
  'live "what is registered right now" surface exists anywhere to poll instead.',
82
82
  input: CinematicGsapInspectInput,
83
83
  result: CinematicGsapInspectResult,
@@ -1,8 +1,7 @@
1
1
  /**
2
- * B5 — `cinematic.*` operations
3
- * (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B5). Registered
4
- * separately from B1's `registerBuiltinTools` (`../operations.ts`),
5
- * B2's `registerProjectOperations` (`../project/index.ts`), B3's
2
+ * `cinematic.*` operations. Registered separately from B1's
3
+ * `registerBuiltinTools` (`../operations.ts`), B2's
4
+ * `registerProjectOperations` (`../project/index.ts`), B3's
6
5
  * `registerEditorOperations` (`../editor/index.ts`), and B4's
7
6
  * `registerPlayOperations` (`../play/index.ts`) — the default `operations`
8
7
  * singleton (`../index.ts`) calls all five.
@@ -11,7 +11,7 @@
11
11
  * (§5.7), which in practice is very often NOT inside a vgai game project at
12
12
  * all — the committed reference fixtures this unit proves against
13
13
  * (`packages/engine/e2e/{reference,render}-cinematic/theatre-project.json`)
14
- * are engine e2e fixtures with no `vgai.game.json` anywhere above them. So
14
+ * are engine e2e fixtures with no `vgai.project.json` anywhere above them. So
15
15
  * these ops take an explicit `root` (falling back to `ctx.projectRoot` when
16
16
  * omitted) rather than being HARD-confined to `ctx.projectRoot` the way
17
17
  * `project.*` ops are — there is deliberately no `PATH_OUTSIDE_PROJECT`-style
@@ -1,14 +1,14 @@
1
1
  /**
2
- * B3 — `editor.*` operations
3
- * (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B3). Registered
4
- * separately from B1's `registerBuiltinTools` (`../operations.ts`) and
5
- * B2's `registerProjectOperations` (`../project/index.ts`) — the default
2
+ * `editor.*` operations. Registered separately from B1's
3
+ * `registerBuiltinTools` (`../operations.ts`) and B2's
4
+ * `registerProjectOperations` (`../project/index.ts`) — the default
6
5
  * `operations` singleton (`../index.ts`) calls all three.
7
6
  */
8
7
 
9
8
  export * from './camera-operations.js';
10
9
  export * from './console-operations.js';
11
10
  export * from './hierarchy-operations.js';
11
+ export * from './inspection-operations.js';
12
12
  export * from './open-operations.js';
13
13
  export * from './screenshot-operations.js';
14
14
  export * from './selection-operations.js';
@@ -23,6 +23,11 @@ export type {
23
23
  EditorTransport,
24
24
  HierarchyNodeInfo,
25
25
  HierarchySummary,
26
+ InspectedActionInfo,
27
+ InspectedFieldInfo,
28
+ InspectedSectionBodyInfo,
29
+ InspectedSectionInfo,
30
+ InspectedSubjectInfo,
26
31
  OidSourceEntry,
27
32
  ScreenshotResult,
28
33
  } from './transport.js';
@@ -43,6 +48,7 @@ import type { ToolRegistry } from '../registry.js';
43
48
  import { registerCameraOperations } from './camera-operations.js';
44
49
  import { registerConsoleOperations } from './console-operations.js';
45
50
  import { registerHierarchyOperations } from './hierarchy-operations.js';
51
+ import { registerEditorInspectionOperations } from './inspection-operations.js';
46
52
  import { registerOpenOperations } from './open-operations.js';
47
53
  import { registerScreenshotOperations } from './screenshot-operations.js';
48
54
  import { registerSelectionOperations } from './selection-operations.js';
@@ -54,6 +60,7 @@ export function registerEditorOperations(registry: ToolRegistry): void {
54
60
  registerSessionOperations(registry);
55
61
  registerSelectionOperations(registry);
56
62
  registerHierarchyOperations(registry);
63
+ registerEditorInspectionOperations(registry);
57
64
  registerCameraOperations(registry);
58
65
  registerSourceLocationOperations(registry);
59
66
  registerOpenOperations(registry);
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `editor.inspection.get` — the SERIALIZED inspection subject, as an
3
+ * operation (design: `docs/ARCHITECTURE-CORE.md` §Editor chrome, "The
4
+ * Inspection Model"; build ledger: `docs/WORK.md` §Inspection Model program,
5
+ * W4).
6
+ *
7
+ * The inspection model's fourth projection: where the column and the compact
8
+ * card render the subject, this one reports it as data. Same composer, same
9
+ * live state, same section identity and order — a `fields` section carries
10
+ * the CURRENT VALUE at each field's scriptable `path`, read through the same
11
+ * io the field rows edit through, and a `custom` body is a named opaque
12
+ * (`{kind:'custom', id, title}`) because the editor renders those with React
13
+ * and there is nothing honest to put on a wire for them — except the values a
14
+ * section can state itself (`data`; the Transform section's
15
+ * position/rotation/scale, which no other read could reach).
16
+ *
17
+ * "The same subject a human reads" includes reading NO subject: when the
18
+ * inspector is unmounted this answers {@link NothingInspectedSchema}'s
19
+ * `{none:true}` rather than a nominal placeholder.
20
+ *
21
+ * Naming: this registry's idiom is `editor.<domain>.<verb>`
22
+ * (`editor.selection.get`, `editor.hierarchy.inspect`,
23
+ * `editor.viewport.camera.get`), so the operation is `editor.inspection.get`.
24
+ * The design's `editor.inspect` spelling is the `@vgai/live` verb over the
25
+ * same wire — `vgai eval 'editor.inspect()'` — where `editor` is the session
26
+ * facade and `inspect` is its method.
27
+ *
28
+ * Wired end to end: `POST /__editor/command` `{type:'inspect'}`, a real,
29
+ * already-handled case in the browser's `handleCommand` switch
30
+ * (`packages/editor/src/command-listener.ts`). A session with no attached
31
+ * browser tab fails `INSPECTION_UNAVAILABLE` — never an empty subject, which
32
+ * would read as "nothing to inspect" and be a fabrication.
33
+ */
34
+
35
+ import { z } from 'zod';
36
+ import { ToolError } from '../errors.js';
37
+ import { defineTool, type ToolRegistry } from '../registry.js';
38
+ import {
39
+ EDITOR_COMMAND_TIMEOUT_MS,
40
+ EDITOR_NOT_RUNNING_ERROR,
41
+ getTransport,
42
+ resolveEditorSession,
43
+ withTimeout,
44
+ } from './transport.js';
45
+
46
+ const INSPECTION_UNAVAILABLE_ERROR = {
47
+ code: 'INSPECTION_UNAVAILABLE',
48
+ summary: 'A session is connected but no browser tab answered the inspection read in time.',
49
+ data: z.object({}),
50
+ } as const;
51
+
52
+ const InspectedFieldSchema = z.object({
53
+ path: z.string().describe('Stable scriptable address of this field.'),
54
+ label: z.string(),
55
+ type: z.string().describe('Field kind: string|number|boolean|vec3|color|enum|asset|json.'),
56
+ value: z
57
+ .unknown()
58
+ .optional()
59
+ .describe('Current value at `path`. Absent when unset, or when `mixed` is set.'),
60
+ mixed: z
61
+ .literal(true)
62
+ .optional()
63
+ .describe('The inspected subjects DISAGREE about this field (the model’s MIXED sentinel).'),
64
+ defaulted: z
65
+ .boolean()
66
+ .optional()
67
+ .describe('The value shown is the declared default — the document does not carry it.'),
68
+ readonly: z.boolean().optional(),
69
+ resettable: z.boolean().optional(),
70
+ revertsTo: z.string().optional(),
71
+ group: z.string().optional(),
72
+ options: z.array(z.unknown()).optional(),
73
+ });
74
+
75
+ const InspectedSectionSchema = z.object({
76
+ id: z.string().describe('Stable section id — the compact card’s persisted tab key.'),
77
+ title: z.string(),
78
+ order: z.number().describe('Display order; sections are already sorted by it.'),
79
+ description: z.string().optional(),
80
+ body: z
81
+ .union([
82
+ z.object({ kind: z.literal('fields'), fields: z.array(InspectedFieldSchema) }),
83
+ z.object({
84
+ kind: z.literal('custom'),
85
+ id: z.string(),
86
+ title: z.string(),
87
+ data: z
88
+ .record(z.string(), z.unknown())
89
+ .optional()
90
+ .describe(
91
+ 'The values this opaque body DISPLAYS, in its own vocabulary — `transform` ' +
92
+ 'carries {position, rotation, scale}, three numbers each, rotation in Euler ' +
93
+ 'XYZ DEGREES exactly as the section’s inputs show it.',
94
+ ),
95
+ }),
96
+ z.object({ kind: z.literal('preview'), id: z.string(), title: z.string() }),
97
+ ])
98
+ .describe(
99
+ 'A field list, or a NAMED OPAQUE for a section the editor renders with React — ' +
100
+ '`custom` for a contributed block, `preview` for the subject’s own live view.',
101
+ ),
102
+ });
103
+
104
+ const InspectedSubjectSchema = z.object({
105
+ id: z.string(),
106
+ title: z.string(),
107
+ kindLabel: z.string().optional(),
108
+ hint: z.string().optional().describe('The quiet line a subject with nothing to edit carries.'),
109
+ presentation: z.object({
110
+ preferred: z.string().describe('The surface’s presentation affinity: card|column.'),
111
+ resolved: z.string().optional().describe('The presentation actually showing.'),
112
+ surface: z.string().optional().describe('three|canvas|dom|asset-lab.'),
113
+ }),
114
+ quickActions: z.array(
115
+ z.object({
116
+ id: z.string(),
117
+ title: z.string(),
118
+ label: z.string().optional(),
119
+ pressed: z
120
+ .boolean()
121
+ .optional()
122
+ .describe('Toggle state — how the visibility eye reports visible/hidden.'),
123
+ disabled: z.boolean().optional(),
124
+ }),
125
+ ),
126
+ sections: z.array(InspectedSectionSchema),
127
+ });
128
+
129
+ /**
130
+ * NOTHING is being inspected — the inspector is unmounted, which since
131
+ * 2026-08-07 is what a human sees whenever nothing is selected on a surface
132
+ * with no empty-state subject of its own.
133
+ *
134
+ * An explicit token rather than `null`, because every "absent" value on this
135
+ * wire already means "nobody answered" and surfaces as
136
+ * `INSPECTION_UNAVAILABLE`. Reporting the two the same way is the only way an
137
+ * agent could mistake a working editor for a broken one.
138
+ */
139
+ const NothingInspectedSchema = z
140
+ .object({ none: z.literal(true).describe('There is no inspector showing.') })
141
+ .describe('Nothing is being inspected.');
142
+
143
+ const InspectionReadSchema = z.union([InspectedSubjectSchema, NothingInspectedSchema]);
144
+
145
+ const EditorInspectionGetInput = z
146
+ .object({})
147
+ .describe('No input — reads whatever the target session’s inspector is showing right now.');
148
+
149
+ export const editorInspectionGet = defineTool({
150
+ name: 'editor.inspection.get',
151
+ summary: 'Read the inspection subject the editor is showing, as data.',
152
+ description:
153
+ 'The serialized projection of the inspection model — the same subject a human reads in the ' +
154
+ 'inspector: identity, presentation, verbs, and every identified section in display order, ' +
155
+ 'with a fields section carrying the CURRENT VALUE at each scriptable path. When the ' +
156
+ 'inspector is showing nothing the answer is {none:true}; a surface whose empty space is a ' +
157
+ 'real thing (an open Asset Lab document) reports THAT subject, never another surface’s. An ' +
158
+ 'opaque section body is a named {kind,id,title}, plus `data` when the section can say what ' +
159
+ 'it displays (transform: position/rotation/scale).',
160
+ input: EditorInspectionGetInput,
161
+ result: InspectionReadSchema,
162
+ errors: [EDITOR_NOT_RUNNING_ERROR, INSPECTION_UNAVAILABLE_ERROR],
163
+ requires: { editor: true },
164
+ host: 'editor-browser',
165
+ mutates: false,
166
+ supportsDryRun: false,
167
+ permission: { risk: 'read', summary: 'Reads live editor inspection state only.' },
168
+ async impl(_input, ctx) {
169
+ const transport = getTransport(ctx);
170
+ const session = await resolveEditorSession(ctx, transport);
171
+ const subject = await withTimeout(
172
+ transport.getInspection(session, EDITOR_COMMAND_TIMEOUT_MS),
173
+ EDITOR_COMMAND_TIMEOUT_MS,
174
+ 'editor.inspection.get',
175
+ ).catch(() => undefined);
176
+ if (!subject) {
177
+ throw new ToolError(
178
+ 'INSPECTION_UNAVAILABLE',
179
+ 'The connected editor session did not answer the inspection read in time.',
180
+ {},
181
+ );
182
+ }
183
+ return subject;
184
+ },
185
+ });
186
+
187
+ export function registerEditorInspectionOperations(registry: ToolRegistry): void {
188
+ registry.register(editorInspectionGet);
189
+ }
@@ -49,51 +49,12 @@ const STORY_OPEN_UNSUPPORTED_ERROR = {
49
49
  const OkResult = z.object({ ok: z.literal(true) });
50
50
 
51
51
  // ---------------------------------------------------------------------------
52
- // editor.scene.open
52
+ // WO-8: `editor.scene.open` lived here. It relayed `{type:'open-scene', path}` to
53
+ // a connected editor, and that verb now REJECTS — there is no scene document to
54
+ // open, because the `.vscn.json` format is deleted. A tool whose only relay is a
55
+ // guaranteed rejection is worse than no tool.
53
56
  // ---------------------------------------------------------------------------
54
57
 
55
- const EditorSceneOpenInput = z.object({
56
- path: z
57
- .string()
58
- .describe('Project-relative path to a .vscn.json scene file to open in the editor.'),
59
- });
60
-
61
- export const editorSceneOpen = defineTool({
62
- name: 'editor.scene.open',
63
- summary: 'Open a scene file in a connected editor session.',
64
- description:
65
- 'Relays {type:"open-scene", path} through POST /__editor/command — an existing, already-handled case.',
66
- input: EditorSceneOpenInput,
67
- result: OkResult,
68
- errors: [EDITOR_NOT_RUNNING_ERROR, COMMAND_FAILED_ERROR],
69
- requires: { editor: true },
70
- host: 'editor-browser',
71
- mutates: true,
72
- supportsDryRun: false,
73
- permission: { risk: 'write', summary: 'Changes which scene the live editor has loaded.' },
74
- async impl(input, ctx) {
75
- const transport = getTransport(ctx);
76
- const session = await resolveEditorSession(ctx, transport);
77
- const result = await withTimeout(
78
- transport.sendCommand(
79
- session,
80
- { type: 'open-scene', path: input.path },
81
- EDITOR_COMMAND_TIMEOUT_MS,
82
- ),
83
- EDITOR_COMMAND_TIMEOUT_MS,
84
- 'editor.scene.open',
85
- ).catch((err: unknown) => ({
86
- ok: false,
87
- error: err instanceof Error ? err.message : String(err),
88
- }));
89
- if (!result.ok) {
90
- throw new ToolError('COMMAND_FAILED', result.error ?? 'open-scene command failed', {
91
- message: result.error ?? 'open-scene command failed',
92
- });
93
- }
94
- return { ok: true as const };
95
- },
96
- });
97
58
 
98
59
  // ---------------------------------------------------------------------------
99
60
  // editor.asset.open
@@ -203,7 +164,6 @@ export const editorStoryOpen = defineTool({
203
164
  });
204
165
 
205
166
  export function registerOpenOperations(registry: ToolRegistry): void {
206
- registry.register(editorSceneOpen);
207
167
  registry.register(editorAssetOpen);
208
168
  registry.register(editorStoryOpen);
209
169
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Transport seam for B3's `editor.*` operations
3
- * (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B3).
2
+ * Transport seam for B3's `editor.*` operations.
4
3
  *
5
4
  * Every `editor.*` op needs two things this module provides:
6
5
  *
@@ -49,6 +48,12 @@
49
48
  * silently acking (B3-followup's false-ack fix — see command-listener.ts)
50
49
  * — this module never has to work around that fallthrough by avoiding a
51
50
  * command send the way `openStory` below still does.
51
+ * - Inspection read: `POST /__editor/command` `{type:'inspect'}` — a real,
52
+ * handled case that serializes the LIVE inspection subject
53
+ * (`packages/editor/src/inspection/active-subject.ts`), the same one the
54
+ * inspector's column and compact card render. When those render nothing it
55
+ * answers `{none:true}`; an ABSENT answer stays reserved for "no tab
56
+ * replied", which is what `INSPECTION_UNAVAILABLE` reports.
52
57
  * - Hierarchy inspect / viewport camera read (B3-followup, now genuinely
53
58
  * wired): `collectState` (`packages/editor/src/command-listener.ts`) now
54
59
  * reports the real flattened entity tree (`entities`, reusing
@@ -172,8 +177,6 @@ export interface EditorSessionInfo {
172
177
  project: string | null;
173
178
  /** Dev-server process id, when known from the local session registry (null for an unregistered/legacy server the caller only probed by port). */
174
179
  pid: number | null;
175
- /** Registry discriminator. `kind: 'e2e'` is a standalone proof server, never an editor-control target. */
176
- kind?: string;
177
180
  /** Exact explicitly targeted editor origin/base URL, including protocol and host. */
178
181
  url?: string;
179
182
  }
@@ -206,6 +209,68 @@ export interface HierarchySummary {
206
209
  entities: HierarchyNodeInfo[] | null;
207
210
  }
208
211
 
212
+ /**
213
+ * The serialized inspection subject (`editor.inspection.get`) — the wire
214
+ * mirror of the editor's own `SerializedInspectionSubject`
215
+ * (`packages/editor/src/inspection/serialize.ts`, which owns the contract).
216
+ * `InspectionSubjectSchema` in `inspection-operations.ts` is the Zod half.
217
+ */
218
+ export interface InspectedFieldInfo {
219
+ path: string;
220
+ label: string;
221
+ type: string;
222
+ value?: unknown;
223
+ mixed?: true;
224
+ defaulted?: boolean;
225
+ readonly?: boolean;
226
+ resettable?: boolean;
227
+ revertsTo?: string;
228
+ group?: string;
229
+ options?: unknown[];
230
+ }
231
+
232
+ export type InspectedSectionBodyInfo =
233
+ | { kind: 'fields'; fields: InspectedFieldInfo[] }
234
+ /** `data` is the section's own displayed values, in its own vocabulary,
235
+ * when it has any (`transform`: `{position, rotation, scale}`). */
236
+ | { kind: 'custom'; id: string; title: string; data?: Record<string, unknown> }
237
+ | { kind: 'preview'; id: string; title: string };
238
+
239
+ export interface InspectedSectionInfo {
240
+ id: string;
241
+ title: string;
242
+ order: number;
243
+ description?: string;
244
+ body: InspectedSectionBodyInfo;
245
+ }
246
+
247
+ export interface InspectedActionInfo {
248
+ id: string;
249
+ title: string;
250
+ label?: string;
251
+ pressed?: boolean;
252
+ disabled?: boolean;
253
+ }
254
+
255
+ export interface InspectedSubjectInfo {
256
+ id: string;
257
+ title: string;
258
+ kindLabel?: string;
259
+ hint?: string;
260
+ presentation: { preferred: string; resolved?: string; surface?: string };
261
+ quickActions: InspectedActionInfo[];
262
+ sections: InspectedSectionInfo[];
263
+ }
264
+
265
+ /** The editor answered, and the answer is that NOTHING is being inspected —
266
+ * the box is unmounted. Distinct from an absent reply, which means nobody
267
+ * answered and surfaces as `INSPECTION_UNAVAILABLE`. */
268
+ export interface InspectedNothingInfo {
269
+ none: true;
270
+ }
271
+
272
+ export type InspectionReadInfo = InspectedSubjectInfo | InspectedNothingInfo;
273
+
209
274
  export interface CameraPose {
210
275
  position: { x: number; y: number; z: number };
211
276
  target: { x: number; y: number; z: number };
@@ -257,6 +322,13 @@ export interface EditorTransport {
257
322
  timeoutMs: number,
258
323
  ): Promise<HierarchySummary | undefined>;
259
324
  getCamera(session: EditorSessionInfo, timeoutMs: number): Promise<CameraPose | undefined>;
325
+ /** What the editor's inspector is showing — a subject, or the explicit
326
+ * `{none:true}` when the box is unmounted. `undefined` means no browser tab
327
+ * answered the relay, which is a different fact and a different error. */
328
+ getInspection(
329
+ session: EditorSessionInfo,
330
+ timeoutMs: number,
331
+ ): Promise<InspectionReadInfo | undefined>;
260
332
  setCamera(
261
333
  session: EditorSessionInfo,
262
334
  request: CameraSetRequest,
@@ -307,8 +379,7 @@ function isRegistrySessionEntry(v: unknown): v is RegistrySessionEntry {
307
379
  (typeof s['project'] === 'string' || s['project'] === null) &&
308
380
  typeof s['port'] === 'number' &&
309
381
  typeof s['pid'] === 'number' &&
310
- typeof s['startedAt'] === 'string' &&
311
- (s['kind'] === undefined || typeof s['kind'] === 'string')
382
+ typeof s['startedAt'] === 'string'
312
383
  );
313
384
  }
314
385
 
@@ -334,7 +405,6 @@ function readRegisteredSessions(): RegistrySessionEntry[] {
334
405
  return Array.isArray(raw)
335
406
  ? raw
336
407
  .filter(isRegistrySessionEntry)
337
- .filter((s) => s.kind !== 'e2e')
338
408
  .filter((s) => pidAlive(s.pid))
339
409
  : [];
340
410
  } catch {
@@ -372,7 +442,7 @@ async function postJson(
372
442
  }
373
443
 
374
444
  function baseUrl(session: EditorSessionInfo): string {
375
- return session.url ?? `http://localhost:${session.port}`;
445
+ return session.url ?? `http://127.0.0.1:${session.port}`;
376
446
  }
377
447
 
378
448
  /** The real, production transport — HTTP against a live `vgai edit` dev server. */
@@ -411,7 +481,6 @@ export class HttpEditorTransport implements EditorTransport {
411
481
  port: s.port,
412
482
  project,
413
483
  pid: s.pid,
414
- ...(s.kind !== undefined ? { kind: s.kind } : {}),
415
484
  };
416
485
  return info;
417
486
  }),
@@ -463,6 +532,22 @@ export class HttpEditorTransport implements EditorTransport {
463
532
  return body?.camera;
464
533
  }
465
534
 
535
+ async getInspection(
536
+ session: EditorSessionInfo,
537
+ timeoutMs: number,
538
+ ): Promise<InspectionReadInfo | undefined> {
539
+ // A real, handled case in the browser's `handleCommand` switch
540
+ // (`packages/editor/src/command-listener.ts`, `case 'inspect'`), which
541
+ // composes the subject through the SAME composer both visual projections
542
+ // use. No browser tab attached -> `{ok:false}` -> undefined, which the
543
+ // operation reports as INSPECTION_UNAVAILABLE rather than an empty subject.
544
+ const result = await this.sendCommand(session, { type: 'inspect' }, timeoutMs).catch(
545
+ () => undefined,
546
+ );
547
+ const subject = result?.ok ? result.data?.['subject'] : undefined;
548
+ return subject && typeof subject === 'object' ? (subject as InspectionReadInfo) : undefined;
549
+ }
550
+
466
551
  async setCamera(
467
552
  session: EditorSessionInfo,
468
553
  request: CameraSetRequest,
@@ -681,7 +766,6 @@ export async function resolveEditorSession(
681
766
  } catch {
682
767
  return notRunning();
683
768
  }
684
- sessions = sessions.filter((session) => session.kind !== 'e2e');
685
769
  if (sessions.length === 0) return notRunning();
686
770
 
687
771
  if (editorUrl !== undefined) {
package/src/operations.ts CHANGED
@@ -18,9 +18,10 @@
18
18
  * implementation.
19
19
  */
20
20
 
21
- import { existsSync } from 'node:fs';
22
- import { join } from 'node:path';
23
21
  import { z } from 'zod';
22
+ // Zod-only, no DOM — the same relative-path seam `create-vgai-project` and
23
+ // `vgai-cli` already use to reach the manifest helpers.
24
+ import { hasManifest } from '../../engine/src/manifest/load-file.js';
24
25
  import { ToolError } from './errors.js';
25
26
  import { defineTool, type ToolRegistry } from './registry.js';
26
27
 
@@ -37,7 +38,7 @@ export const ProjectStatusResult = z
37
38
  projectRoot: z.string().describe('Absolute path this status was computed for.'),
38
39
  hasProject: z
39
40
  .boolean()
40
- .describe('True when a vgai.game.json manifest exists directly under projectRoot.'),
41
+ .describe('True when a vgai.project.json manifest exists directly under projectRoot.'),
41
42
  })
42
43
  .describe('Discovery result for the project at ctx.projectRoot.');
43
44
 
@@ -47,7 +48,7 @@ export const projectStatus = defineTool({
47
48
  description:
48
49
  'File-native discovery operation (B2 project.* namespace stand-in for B1). Reads ' +
49
50
  'ctx.projectRoot from disk — no editor process required — and reports whether a ' +
50
- 'vgai.game.json manifest is present there.',
51
+ 'vgai.project.json manifest is present there.',
51
52
  input: ProjectStatusInput,
52
53
  result: ProjectStatusResult,
53
54
  errors: [
@@ -72,7 +73,11 @@ export const projectStatus = defineTool({
72
73
  }
73
74
  return {
74
75
  projectRoot: ctx.projectRoot,
75
- hasProject: existsSync(join(ctx.projectRoot, 'vgai.game.json')),
76
+ // `hasManifest` OWNS the dual-name contract. Spelling the two filenames
77
+ // out here made this the fifth independent copy of it, and the one most
78
+ // likely to drift silently: it is the answer `project.status` gives
79
+ // through all four projections (SDK/CLI/HTTP/MCP).
80
+ hasProject: hasManifest(ctx.projectRoot),
76
81
  };
77
82
  },
78
83
  });
@@ -47,8 +47,8 @@ export interface PerfFrameSample {
47
47
  };
48
48
  }
49
49
 
50
- /** Mirrors `@engine/runtime/render-control`'s `PerfWorldCount`. */
51
- export interface PerfWorldCount {
50
+ /** Mirrors `@engine/runtime/render-control`'s `PerfRootCount`. */
51
+ export interface PerfRootCount {
52
52
  readonly id: string;
53
53
  readonly kind: string;
54
54
  readonly nodes: number;
@@ -95,7 +95,7 @@ export interface PerfRunResult {
95
95
  readonly entryQuery: string | undefined;
96
96
  readonly excludedFromDeterminism: readonly string[];
97
97
  readonly frameSamples: readonly PerfFrameSample[];
98
- readonly worlds: readonly PerfWorldCount[];
98
+ readonly worlds: readonly PerfRootCount[];
99
99
  }
100
100
 
101
101
  export class PerfRunError extends Error {}
@@ -208,7 +208,7 @@ export async function runPerfCapture(request: PerfRunRequest): Promise<PerfRunRe
208
208
  }
209
209
  ).__vgaiRender.perfSample(steps),
210
210
  frames,
211
- )) as { frames: PerfFrameSample[]; worlds: PerfWorldCount[] };
211
+ )) as { frames: PerfFrameSample[]; worlds: PerfRootCount[] };
212
212
 
213
213
  const fixedDt = await page.evaluate(() =>
214
214
  (