@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 +3 -3
- package/src/account.ts +20 -0
- package/src/cinematic/gsap-operations.ts +3 -3
- package/src/cinematic/index.ts +3 -4
- package/src/cinematic/theatre-operations.ts +1 -1
- package/src/editor/index.ts +11 -4
- package/src/editor/inspection-operations.ts +189 -0
- package/src/editor/open-operations.ts +4 -44
- package/src/editor/transport.ts +94 -10
- package/src/operations.ts +10 -5
- package/src/perf/perf-run.ts +4 -4
- package/src/play/control-operations.ts +19 -19
- package/src/play/debug-command-operations.ts +2 -3
- package/src/play/index.ts +4 -5
- package/src/play/input-operations.ts +12 -12
- package/src/play/lifecycle-operations.ts +3 -3
- package/src/play/run-ticks-operations.ts +4 -4
- package/src/play/state-operations.ts +4 -4
- package/src/play/status-operations.ts +13 -13
- package/src/play/transport.ts +29 -38
- package/src/project/index.ts +15 -19
- package/src/project/inspection-node.ts +6 -5
- package/src/project/inspection-operation.ts +1 -1
- package/src/project/inspection.ts +6 -6
- package/src/project/manifest-operations.ts +12 -17
- package/src/project/shared.ts +4 -39
- package/src/registry.ts +9 -3
- package/src/render/index.ts +3 -4
- package/src/render/render-cinematic.ts +27 -28
- package/src/types.ts +7 -3
- package/src/project/component-operations.ts +0 -337
- package/src/project/entity-operations.ts +0 -366
- package/src/project/scene-operations.ts +0 -426
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.
|
|
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.
|
|
30
|
-
"@vgai/engine": "0.5.
|
|
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/
|
|
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/
|
|
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/
|
|
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,
|
package/src/cinematic/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
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.
|
|
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
|
package/src/editor/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
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
|
}
|
package/src/editor/transport.ts
CHANGED
|
@@ -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://
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
});
|
package/src/perf/perf-run.ts
CHANGED
|
@@ -47,8 +47,8 @@ export interface PerfFrameSample {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** Mirrors `@engine/runtime/render-control`'s `
|
|
51
|
-
export interface
|
|
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
|
|
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:
|
|
211
|
+
)) as { frames: PerfFrameSample[]; worlds: PerfRootCount[] };
|
|
212
212
|
|
|
213
213
|
const fixedDt = await page.evaluate(() =>
|
|
214
214
|
(
|