@volter/editor-live 0.5.57

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/dist/index.js ADDED
@@ -0,0 +1,2080 @@
1
+ // ../editor-sdk/src/http-transport.node.ts
2
+ import { Agent, fetch as nodeFetch } from "undici";
3
+ function createDispatcher(timeoutMs) {
4
+ return new Agent({ headersTimeout: timeoutMs, bodyTimeout: timeoutMs });
5
+ }
6
+ async function dispatchFetch(url, init, dispatcher) {
7
+ if (!dispatcher) return fetch(url, init);
8
+ return await nodeFetch(url, {
9
+ ...init,
10
+ dispatcher
11
+ });
12
+ }
13
+
14
+ // ../editor-sdk/src/client.ts
15
+ var DEFAULT_URL = "http://127.0.0.1:20173";
16
+ var EditorCommandError = class extends Error {
17
+ code;
18
+ /**
19
+ * True when the RELAY ended the command itself rather than the editor
20
+ * answering it — the HTTP 504 that `server/server-utils.ts`'s
21
+ * `commandResponseFor` gives any `timedOut` result, or this client's own
22
+ * deadline below.
23
+ *
24
+ * Read it as "no answer", not as "the budget expired". `editor-server.ts`
25
+ * raises `timedOut` for five conditions and only one of them takes the full
26
+ * budget: the command's timer expiring, the controlling tab's socket dying,
27
+ * the receipt window closing unanswered, a beating-but-dead tab, and no tab
28
+ * present at all. The last four can fail in milliseconds.
29
+ *
30
+ * A caller that converges by retrying (`vgai restart`) needs the distinction
31
+ * because a refusal the editor ANSWERED may go differently next time, while
32
+ * a command the relay abandoned tells you nothing new on a second identical
33
+ * attempt — and when the abandonment was a 120s budget, re-running it three
34
+ * times is `restart-readiness.ts`'s 361-seconds-of-silence defect.
35
+ */
36
+ timedOut;
37
+ constructor(message, code, timedOut = false) {
38
+ super(message);
39
+ this.name = "EditorCommandError";
40
+ this.code = code;
41
+ this.timedOut = timedOut;
42
+ }
43
+ };
44
+ var COMMAND_DEADLINE_MS = 15e4;
45
+ var BLENDER_DEADLINE_MS = 30 * 6e4;
46
+ var UNDICI_DEFAULT_HEADERS_TIMEOUT_MS = 3e5;
47
+ var CONSOLE_DRAIN_TIMEOUT_MS = 1500;
48
+ function describeFetchFailure(error) {
49
+ const messages = [];
50
+ let current = error;
51
+ for (let depth = 0; depth < 8; depth++) {
52
+ if (!(current instanceof Error)) break;
53
+ if (current.message) messages.push(current.message);
54
+ const code = current.code;
55
+ if (typeof code === "string" && code !== "") {
56
+ return { code, detail: messages.join(" <- ") };
57
+ }
58
+ const aggregate = current.errors;
59
+ if (Array.isArray(aggregate) && aggregate.length > 0) {
60
+ const inner = describeFetchFailure(aggregate[0]);
61
+ return { code: inner.code, detail: [...messages, inner.detail].join(" <- ") };
62
+ }
63
+ current = current.cause;
64
+ }
65
+ return { code: "UNKNOWN", detail: messages.join(" <- ") || String(error) };
66
+ }
67
+ var RETRYABLE_TRANSPORT_CODES = /* @__PURE__ */ new Set([
68
+ "ECONNREFUSED",
69
+ "ECONNRESET",
70
+ "EPIPE",
71
+ "ETIMEDOUT",
72
+ "EHOSTUNREACH",
73
+ "UND_ERR_SOCKET",
74
+ "UND_ERR_CONNECT_TIMEOUT"
75
+ ]);
76
+ var TRANSPORT_RETRY_DELAY_MS = 400;
77
+ var EditorClient = class {
78
+ baseUrl;
79
+ /**
80
+ * The running game's debug plane — see {@link GameDebugDoor}. It rides the
81
+ * SAME `/__editor/command` relay every other method here uses (relay cases
82
+ * `inspect-gameplay-state` / `invoke-debug-command`, `command-listener.ts`),
83
+ * so a tool contribution reaches the game through the client it already has
84
+ * rather than a second channel of its own.
85
+ */
86
+ game;
87
+ /**
88
+ * Called with the raw body of EVERY response this client receives — command
89
+ * envelopes and `/__editor/state` alike, on success AND on refusal.
90
+ *
91
+ * It exists for exactly one contract: the server stamps `unresolvedConsole`
92
+ * onto every envelope (`server-utils.ts`'s `commandResponseFor`), and the CLI
93
+ * has to see those counts to be loud about them. Routing that through a
94
+ * single observer here — rather than teaching each of the CLI's output sites
95
+ * to unpack a response — is what keeps the loudness contract ONE mechanism.
96
+ * The observer must not throw; anything it raises is swallowed, because a
97
+ * reporting hook may never break the command it is reporting on.
98
+ */
99
+ transport;
100
+ onEnvelope;
101
+ constructor(opts) {
102
+ if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
103
+ throw new TypeError(
104
+ 'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").'
105
+ );
106
+ }
107
+ const unknownOptions = Object.keys(opts ?? {}).filter(
108
+ (key) => key !== "url" && key !== "onEnvelope" && key !== "transport"
109
+ );
110
+ if (unknownOptions.length > 0) {
111
+ throw new Error(
112
+ `EditorClient: unknown option${unknownOptions.length === 1 ? "" : "s"} ${unknownOptions.map((key) => `"${key}"`).join(", ")}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`
113
+ );
114
+ }
115
+ this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, "");
116
+ this.onEnvelope = opts?.onEnvelope ?? null;
117
+ this.transport = opts?.transport ?? null;
118
+ this.game = {
119
+ state: async (name) => {
120
+ const data = await this.command({
121
+ type: "inspect-gameplay-state",
122
+ keys: [name]
123
+ });
124
+ return data.state?.[name];
125
+ },
126
+ command: async (name, ...args) => (await this.command({ type: "invoke-debug-command", name, args })).result
127
+ };
128
+ }
129
+ /**
130
+ * `retryTransport` opts a command into ONE automatic retry after a transport
131
+ * failure (see {@link RETRYABLE_TRANSPORT_CODES}). It is deliberately
132
+ * OPT-IN and off by default: a socket that died after the request was written
133
+ * cannot prove the editor did not already run the command, so a blanket retry
134
+ * would risk playing/stopping/writing twice. Read-only relays — the captures —
135
+ * have no such hazard and turn it on.
136
+ */
137
+ async command(body, options) {
138
+ const type = String(body["type"] ?? "command");
139
+ const deadlineMs = options?.deadlineMs ?? COMMAND_DEADLINE_MS;
140
+ if (this.transport) {
141
+ const answered = await this.transport(body);
142
+ if (!answered.ok) {
143
+ throw new EditorCommandError(
144
+ answered.error ?? `Editor command "${type}" failed`,
145
+ answered.code
146
+ );
147
+ }
148
+ return answered;
149
+ }
150
+ const url = `${this.baseUrl}/__editor/command`;
151
+ const request = JSON.stringify(body);
152
+ let res;
153
+ let retried = false;
154
+ let commandDispatcher;
155
+ for (; ; ) {
156
+ try {
157
+ const init = {
158
+ method: "POST",
159
+ headers: { "Content-Type": "application/json" },
160
+ body: request,
161
+ signal: AbortSignal.timeout(deadlineMs)
162
+ };
163
+ commandDispatcher = deadlineMs > UNDICI_DEFAULT_HEADERS_TIMEOUT_MS ? createDispatcher(deadlineMs + 3e4) : void 0;
164
+ res = await dispatchFetch(url, init, commandDispatcher);
165
+ break;
166
+ } catch (error) {
167
+ await commandDispatcher?.destroy?.();
168
+ commandDispatcher = void 0;
169
+ if (error?.name === "TimeoutError") {
170
+ throw new EditorCommandError(
171
+ `The editor at ${this.baseUrl} never answered "${type}" within ${Math.round(deadlineMs / 1e3)}s \u2014 past every server-side budget, so the server itself is not answering. Check the terminal running \`volter-editor edit\`.`,
172
+ void 0,
173
+ true
174
+ );
175
+ }
176
+ const { code, detail } = describeFetchFailure(error);
177
+ if (options?.retryTransport === true && !retried && RETRYABLE_TRANSPORT_CODES.has(code)) {
178
+ retried = true;
179
+ await new Promise((resolve3) => setTimeout(resolve3, TRANSPORT_RETRY_DELAY_MS));
180
+ continue;
181
+ }
182
+ throw new EditorCommandError(
183
+ `POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ""}.` + (retried ? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.` : "") + (options?.retryTransport === true ? "" : " Not retried automatically: this command can change editor state, and a socket that died after the request was written cannot prove the editor did not already run it.") + " A transport failure means the port stopped answering, not that the editor refused \u2014 the dev server restarts on any watched source edit, and it shuts itself down after an idle window. Check the terminal running `volter-editor edit` and confirm the port this client resolved.",
184
+ code,
185
+ false
186
+ );
187
+ }
188
+ }
189
+ let data;
190
+ try {
191
+ data = await this.readJson(res);
192
+ } finally {
193
+ await commandDispatcher?.destroy?.();
194
+ }
195
+ if (!data.ok) {
196
+ throw new EditorCommandError(
197
+ data.error ?? `Editor command failed: ${res.status}`,
198
+ data.code,
199
+ // 504 is every `timedOut` result (`commandResponseFor`) — the relay
200
+ // gave up, on any of its five grounds. A 200 body with `ok: false` is
201
+ // an ANSWER from the editor, however unwelcome. See `timedOut` above.
202
+ res.status === 504
203
+ );
204
+ }
205
+ return data;
206
+ }
207
+ // --- Play control ---
208
+ /** `opts.seed` (D15/T-D15.6, objection-4 fix) — `vgai play --seed <n>`'s
209
+ * explicit config leg, relayed as `cmd['seed']`; `handleCommand`'s
210
+ * `'play'` case threads it into `enterPlayMode`'s highest-precedence seed
211
+ * argument (beats manifest.determinism.defaultSeed/?vgai-seed=). Omitted,
212
+ * boot seeding falls back to that precedence unchanged.
213
+ *
214
+ * `opts.name` (`vgai play --name <text>`) — an OPTIONAL label for this run,
215
+ * relayed as `cmd['name']` and slugified server-side into the run's
216
+ * `logs/play-*.jsonl` filename and its session-journal line. Findability
217
+ * only: no registry, no uniqueness, no lookup verb — grep and `ls` are the
218
+ * query engine. Omitted, the filename keeps its exact unnamed shape. */
219
+ /* `opts.record` (`vgai play --record <name>`) — NAMES this run's recording
220
+ * file. It does not ENABLE recording: every relayed play records, with no
221
+ * flag (see `@vgai/game`'s `src/play/play-recording.ts`). Omitted, the clip is named for
222
+ * the durable Gameplay Session; named, it becomes an explicit keepsake in
223
+ * `.vgai/recordings/<name>.webm`. */
224
+ async play(opts) {
225
+ return this.command({
226
+ type: "play",
227
+ ...opts?.seed !== void 0 ? { seed: opts.seed } : {},
228
+ ...opts?.name ? { name: opts.name } : {},
229
+ ...opts?.record ? { record: opts.record } : {}
230
+ });
231
+ }
232
+ /** Dispose the current play session and mount it again from fresh project entry source. */
233
+ async restart() {
234
+ await this.command({ type: "play" });
235
+ }
236
+ /** Stops play, and finalizes this run's recording before the surface it was
237
+ * photographing is torn down. The capture is absent when nothing recorded. */
238
+ async stop() {
239
+ return this.command({ type: "stop" });
240
+ }
241
+ async pause() {
242
+ await this.command({ type: "pause" });
243
+ }
244
+ async resume() {
245
+ await this.command({ type: "resume" });
246
+ }
247
+ async step() {
248
+ await this.command({ type: "step" });
249
+ }
250
+ // --- Selection ---
251
+ async select(id) {
252
+ await this.command({ type: "select", id });
253
+ }
254
+ async selectMultiple(ids) {
255
+ await this.command({ type: "select-multiple", ids });
256
+ }
257
+ async selectAll() {
258
+ await this.command({ type: "select-all" });
259
+ }
260
+ // --- Viewport ---
261
+ async focusEntity(id) {
262
+ await this.command({ type: "focus-entity", id });
263
+ }
264
+ async focusSelection() {
265
+ await this.command({ type: "focus-selection" });
266
+ }
267
+ /**
268
+ * Frame the EDIT viewport camera on one entity — the strict sibling of
269
+ * {@link focusEntity}. Same framing; an id the scene does not know is a
270
+ * refusal naming the id (`EditorCommandError`, code `ENTITY_NOT_FOUND`)
271
+ * rather than `focusEntity`'s silent no-op, so a caller that frames an
272
+ * entity before capturing it cannot photograph the wrong thing.
273
+ */
274
+ async frameEntity(id) {
275
+ await this.command({ type: "frame-entity", id });
276
+ }
277
+ async viewPreset(preset) {
278
+ await this.command({ type: "view-preset", preset });
279
+ }
280
+ /**
281
+ * LOOK AROUND THE OPEN MODEL, visibly. Swings the active Object3D
282
+ * document's camera — the one on the human's screen — by `azimuth`/
283
+ * `elevation` radians, animated over `duration` seconds, and resolves when
284
+ * the move ends. A human drag during the move cancels it where it stands
285
+ * (`cancelledBy: 'human'`); the promise still resolves.
286
+ */
287
+ async orbitDocument(options) {
288
+ return this.command({ type: "document-orbit", ...options });
289
+ }
290
+ /** A slow full revolution around the open document's subject, at a constant rate. */
291
+ async turntableDocument(options) {
292
+ return this.command({ type: "document-turntable", ...options });
293
+ }
294
+ /**
295
+ * Frame the open document's subject (its selection if it has one). `fit`
296
+ * scales the fitted distance: 1 is the toolbar Frame button's tight fit.
297
+ */
298
+ async frameDocument(fit) {
299
+ return this.command({
300
+ type: "document-frame",
301
+ ...fit === void 0 ? {} : { fit }
302
+ });
303
+ }
304
+ async setCamera(position, target, fov) {
305
+ await this.command({
306
+ type: "set-camera",
307
+ position,
308
+ target,
309
+ ...fov === void 0 ? {} : { fov }
310
+ });
311
+ }
312
+ async captureViewport(size) {
313
+ const data = await this.command({
314
+ type: "capture-viewport",
315
+ ...size === void 0 ? {} : { size }
316
+ });
317
+ return { base64: data.base64, mimeType: data.mimeType };
318
+ }
319
+ /**
320
+ * Unit 4 (live-front-door wave) — capture the RUNNING GAME (`vgai
321
+ * screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
322
+ * `@vgai/live`'s `RelayTransport.screenshot` (and therefore
323
+ * `game.screenshot()` on the relay path) already sends, so all three
324
+ * surfaces composite the identical full game stack — canvas(es) plus the
325
+ * HUD/react DOM layers — rather than any of them inventing a second,
326
+ * subtly-different capture path. Contrast {@link captureViewport}, which
327
+ * captures the EDITOR viewport's canvas and would silently hand back an
328
+ * editor-only (HUD-less, possibly not-even-playing) image.
329
+ *
330
+ * Rejects — loudly, via `command`'s own `{ok:false}` unwrap — when play
331
+ * mode isn't running ("not in play mode — start play before using the
332
+ * debug seam") or no game canvas is mounted yet. Never returns a blank or
333
+ * editor-only frame as a stand-in.
334
+ *
335
+ * `opts.refreshStarvedFrame` is the loop-starvation leg: without recent rAF
336
+ * progress the canvas holds a provably stale frame and the relay
337
+ * refuses it with `BRIDGE_SCREENSHOT_STALE` rather than pass it off as
338
+ * current. Setting this asks the relay to render exactly ONE deterministic
339
+ * tick (`runTicks(1, {render:'last'})`) first — the same escape
340
+ * `@vgai/live`'s `RelayTransport.screenshot` has always used, which is why
341
+ * `vgai eval` could recover these frames while `vgai screenshot` could not.
342
+ * Off by default: a caller who does not ask must never be handed a frame
343
+ * that only exists because the capture drove the game.
344
+ */
345
+ async captureGame(opts) {
346
+ const data = await this.command({
347
+ type: "bridge-screenshot",
348
+ ...opts?.refreshStarvedFrame === true ? { refreshStarvedFrame: true } : {}
349
+ });
350
+ const layers = data.layers;
351
+ const flatness = data.flatness;
352
+ return {
353
+ base64: data.base64,
354
+ mimeType: data.mimeType,
355
+ composite: data.composite === true,
356
+ ...layers && Number.isInteger(layers.canvases) && Number.isInteger(layers.domOverlays) ? { layers } : {},
357
+ // Pass the pixel-honesty fields through as the page reported them: the
358
+ // warning sentence is written where the pixels are, so nothing here
359
+ // re-derives (or softens) it.
360
+ ...flatness && typeof flatness.dominantFraction === "number" ? { flatness } : {},
361
+ ...data.loopRecoveryFrame === true ? { loopRecoveryFrame: true } : {},
362
+ // Same pass-through rule: the recorded-run notice is written where the
363
+ // pixels are, so nothing here re-derives or softens it.
364
+ ...data.recording && typeof data.recording.notice === "string" ? { recording: data.recording } : {}
365
+ };
366
+ }
367
+ /** Start recording the same clean running-game composite `captureGame`
368
+ * photographs. Recording state lives in the editor page, so another process
369
+ * may stop it later through the same project session. */
370
+ async startGameplayRecording(options = {}) {
371
+ return this.command({
372
+ type: "bridge-recording-start",
373
+ ...options.fps !== void 0 ? { fps: options.fps } : {},
374
+ ...options.name !== void 0 ? { name: options.name } : {},
375
+ ...options.format !== void 0 ? { format: options.format } : {}
376
+ });
377
+ }
378
+ /** Export a paused run as fixed-step video. Advances game state; maximum
379
+ * five minutes. `audio` describes the muxed track, or is `false` when the
380
+ * world implements no `AudioAdapter.renderOffline` and the file is
381
+ * genuinely silent — read it, never assume either. */
382
+ async exportGameplayVideo(options) {
383
+ return this.command(
384
+ { type: "bridge-recording-export", ...options },
385
+ { deadlineMs: 61e4, retryTransport: false }
386
+ );
387
+ }
388
+ /** Stop the page-owned recorder and return its WebM path and metadata. */
389
+ async stopGameplayRecording() {
390
+ return this.command({ type: "bridge-recording-stop" });
391
+ }
392
+ /** Read the active capture's monotonic media position. This is the only clock
393
+ * suitable for selecting intervals inside the finalized recording. */
394
+ async getGameplayRecordingTimeline() {
395
+ const timeline = await this.command({
396
+ type: "bridge-recording-timeline"
397
+ });
398
+ return { startedAt: timeline.startedAt, elapsedMs: timeline.elapsedMs };
399
+ }
400
+ /** Encode a recorded canvas/DOM interval into a normal composite WebM. */
401
+ async exportGameplayReplay(options) {
402
+ return this.command(
403
+ { type: "bridge-recording-replay-export", ...options },
404
+ { deadlineMs: 61e4, retryTransport: false }
405
+ );
406
+ }
407
+ async captureGameplayReplay(replayPath, positionMs) {
408
+ return this.command({
409
+ type: "bridge-recording-replay-capture",
410
+ replayPath,
411
+ positionMs
412
+ });
413
+ }
414
+ /**
415
+ * Capture an isolated, deterministic four-view preview through the editor's
416
+ * native Asset Lab. The SDK delegates rendering to the editor; it never
417
+ * loads, clones, or interprets Three.js assets itself.
418
+ */
419
+ async captureAssetPreview(source, options = {}) {
420
+ const data = await this.command(
421
+ {
422
+ type: "capture-asset-preview",
423
+ ...source,
424
+ ...options
425
+ },
426
+ // Photographing changes nothing, and this is the relay `vgai screenshot
427
+ // <module>` / `project.bake.preview` rides — the lane where a momentary
428
+ // transport failure cost a cold agent three probe modules.
429
+ { retryTransport: true }
430
+ );
431
+ return {
432
+ width: data.width,
433
+ height: data.height,
434
+ // Absent from editors that predate orientation reporting.
435
+ ...data.orientation ? { orientation: data.orientation } : {},
436
+ views: data.views,
437
+ contactSheet: data.contactSheet
438
+ };
439
+ }
440
+ /**
441
+ * A project-defined labeled shot set (`vgai screenshot <target> --shots <set>`):
442
+ * the DEFINITION travels with the command (project data — see
443
+ * `AssetPreviewShotSetDefinition`; the CLI resolves it from the registered
444
+ * `project.<set>.previewShots` tool), and the editor's generic
445
+ * capture engine renders it — see `packages/editor/src/asset-preview.ts`'s
446
+ * `captureShotSetAssetPreview`. Throws (via `command`'s `{ok:false}`
447
+ * unwrap) with a clear message naming the missing joint(s) when the asset
448
+ * lacks a bone the definition requires.
449
+ */
450
+ async captureShotSetPreview(source, definition, options = {}) {
451
+ const data = await this.command(
452
+ {
453
+ type: "capture-asset-preview",
454
+ ...source,
455
+ ...options,
456
+ shotSet: definition
457
+ },
458
+ { retryTransport: true }
459
+ );
460
+ return {
461
+ width: data.width,
462
+ height: data.height,
463
+ shots: data.shots,
464
+ // An older editor predates the empty-frame guard and sends none.
465
+ warnings: data.warnings ?? [],
466
+ contactSheet: data.contactSheet
467
+ };
468
+ }
469
+ /**
470
+ * B8.4 — score the asset against a reference GLB (`vgai screenshot
471
+ * <model.glb> --compare <ref.glb>`): matched orthographic front + side silhouettes
472
+ * (equal-height bounding-box framing, both yaw-normalized to face the
473
+ * camera), per-view IoU numbers, and overlay evidence images. The
474
+ * reference GLB's raw bytes travel base64 in the command; the editor
475
+ * renders and scores — the SDK never interprets Three.js assets itself.
476
+ */
477
+ async captureAssetComparePreview(source, refGlbBase64, options = {}) {
478
+ const { refForward, ...dimensions } = options;
479
+ const data = await this.command({
480
+ type: "capture-asset-preview",
481
+ ...source,
482
+ ...dimensions,
483
+ compare: { glbBase64: refGlbBase64, ...refForward ? { forward: refForward } : {} }
484
+ });
485
+ return { width: data.width, height: data.height, views: data.views };
486
+ }
487
+ /**
488
+ * The STORY lane (`vgai screenshot <file>.stories.tsx`): every CSF export of
489
+ * one project story file rendered in the live session's DOM and captured
490
+ * through the same composite leg {@link captureGame} uses, returned as
491
+ * per-export images plus one variant sheet. `options.story` narrows to a
492
+ * single export.
493
+ *
494
+ * Rendering happens in the EDITOR — the SDK never imports, composes or
495
+ * mounts a CSF module itself; the session already owns that machinery for
496
+ * its Stories panel and this drives it.
497
+ */
498
+ async captureStoryVariants(modulePath, options = {}) {
499
+ const data = await this.command({
500
+ type: "capture-story-variants",
501
+ modulePath,
502
+ ...options
503
+ });
504
+ return {
505
+ modulePath: data.modulePath,
506
+ width: data.width,
507
+ height: data.height,
508
+ variants: data.variants,
509
+ contactSheet: data.contactSheet
510
+ };
511
+ }
512
+ // --- Panels ---
513
+ async showViewport(tab) {
514
+ await this.command({ type: "viewport-tab", tab });
515
+ }
516
+ /** Focus a static workspace panel by the key the editor's panel registry
517
+ * holds; an unknown key refuses naming the keys it does hold. */
518
+ async showPanel(panel) {
519
+ await this.command({ type: "show-panel", panel });
520
+ }
521
+ /** Show several instances of the running game split-screen — multiplayer
522
+ * authoring. Pass a total `count` (default "Player N" labels) or an array of
523
+ * `names` (its length is the count; index 0 is the primary). Requires a live
524
+ * play session. */
525
+ async setInstanceCount(countOrNames) {
526
+ await this.command(
527
+ Array.isArray(countOrNames) ? { type: "set-instance-count", names: countOrNames } : { type: "set-instance-count", count: countOrNames }
528
+ );
529
+ }
530
+ async openAsset(path, kind) {
531
+ await this.command({ type: "open-asset-tab", path, kind });
532
+ }
533
+ /** SELECT a project asset — the other half of the browser's
534
+ * selection-vs-open contract (single click selects and fills the
535
+ * Inspector; double click opens a document). */
536
+ async selectAsset(path) {
537
+ await this.command({ type: "select-asset", path });
538
+ }
539
+ async closeAsset(key) {
540
+ await this.command({ type: "close-asset-tab", key });
541
+ }
542
+ async toggleCommandPalette() {
543
+ await this.command({ type: "toggle-command-palette" });
544
+ }
545
+ async toggleConsole() {
546
+ await this.command({ type: "toggle-console" });
547
+ }
548
+ /** Switch the editor's NAMED WORKSPACE — the task-named layout memory
549
+ * (`game`/`model`/`sculpt`/`texture`/`animate`/`look`). Resolves once the
550
+ * dock has finished rebuilding, so a following capture photographs the
551
+ * arrangement that was asked for. */
552
+ async setWorkspace(workspace) {
553
+ await this.command({ type: "set-workspace", workspace });
554
+ }
555
+ /** Apply a STYLE BUNDLE — palette, material, icon set and region defaults
556
+ * in one gesture (`classic`/`glass`/`maya`/`substance`, or one a package
557
+ * the project declares carries, `blender`). */
558
+ async setStyle(style) {
559
+ await this.command({ type: "set-style", style });
560
+ }
561
+ /** Set the MATERIAL apart from the bundle that usually carries it.
562
+ * Answers with what the chrome wears afterwards. */
563
+ async setAppearance(appearance) {
564
+ return this.command({
565
+ type: "set-appearance",
566
+ ...appearance
567
+ });
568
+ }
569
+ async showBuild() {
570
+ await this.command({ type: "show-build" });
571
+ }
572
+ /** Atomically present a durable editor view and return its shareable URL. */
573
+ async present(view) {
574
+ const presented = await this.command({ type: "present-view", view });
575
+ return { view: presented.view, url: presented.url, warnings: presented.warnings };
576
+ }
577
+ /**
578
+ * The INSPECTION SUBJECT the editor is showing right now, as data — the
579
+ * serialized projection of the inspection model (design:
580
+ * `docs/ARCHITECTURE-CORE.md` §Editor chrome, "The Inspection Model").
581
+ *
582
+ * The same subject a human reads in the inspector: identity, presentation,
583
+ * verbs, and the identified sections in display order — with a `fields`
584
+ * section's CURRENT VALUES read through the same io the field rows edit
585
+ * through. With nothing selected it answers the active surface's own
586
+ * no-selection subject when it has one, exactly as the panel does; it never
587
+ * reports another surface's, and when the panel itself is unmounted it
588
+ * answers `{none: true}` rather than a subject nobody is looking at. A
589
+ * `custom` section body is a named opaque (`{kind, id, title}`) — the editor
590
+ * renders those with React — plus its displayed values under `data` when it
591
+ * has any (the Transform section's position/rotation/scale).
592
+ */
593
+ async inspect() {
594
+ const data = await this.command({ type: "inspect" });
595
+ return data.subject;
596
+ }
597
+ /** Run one verb exposed by the active Inspector subject, by its id. */
598
+ async runInspectionAction(actionId) {
599
+ const data = await this.command({
600
+ type: "run-inspection-action",
601
+ actionId
602
+ });
603
+ return data.subject;
604
+ }
605
+ /**
606
+ * Run ONE command by id — the door to everything the command palette lists.
607
+ *
608
+ * Under the Code-OSS frame this is the workbench's own `ICommandService`, so
609
+ * any command id works: a view's `vgai.<view>.<verb>`, an editor action's
610
+ * `vgai.action.<id>`, or one of VS Code's own. Standalone `vgai edit` has no
611
+ * command service and answers the `vgai.<view>.<verb>` shape directly off
612
+ * the views registry, refusing anything else BY NAME.
613
+ *
614
+ * The result is whatever the command answered — a view verb's state, or
615
+ * `null` for a command that returns nothing.
616
+ */
617
+ async runCommand(commandId, args) {
618
+ const data = await this.command({
619
+ type: "run-command",
620
+ commandId,
621
+ ...args === void 0 ? {} : { args }
622
+ });
623
+ return data.result;
624
+ }
625
+ /**
626
+ * One STRUCTURE op on the authored tree — the hierarchy context menu's own
627
+ * verbs, on the same helpers, for a caller with no pointer to right-click
628
+ * with. `id`/`ids` default to the current selection.
629
+ */
630
+ async structureOp(op, options = {}) {
631
+ return this.command({ type: "structure-op", op, ...options });
632
+ }
633
+ /** "Extract Component…" — the hierarchy row's action, as a command. Answers
634
+ * the action's own sentence, which NAMES the files it created. */
635
+ async extractComponent(options = {}) {
636
+ return this.command({ type: "extract-component", ...options });
637
+ }
638
+ /** "Fork Component…" — extract's twin: one new file, one callsite retargeted. */
639
+ async forkComponent(options = {}) {
640
+ return this.command({ type: "fork-component", ...options });
641
+ }
642
+ /**
643
+ * The HIERARCHY PANEL's actual rendered row tree, as data.
644
+ *
645
+ * The same rows a human is looking at: the adapter's tree after the component
646
+ * marks fold implementation subtrees, after the internals reveal, after the
647
+ * document promotion, the child cap, the collapse state, the search filter
648
+ * and the selection scope. Works in play mode and edit mode alike — the
649
+ * answer reports which (`playState`, `activeViewportTab`), because a
650
+ * play-mode tree and an edit-mode tree come from different adapters.
651
+ *
652
+ * Deliberately NOT `status().entities`, which walks the raw adapter tree and
653
+ * therefore answers a different question: a panel defect is invisible in it.
654
+ *
655
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
656
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
657
+ * panel draws a caret at all) — so "this subtree exists but the UI offers no
658
+ * way to open it" is a readable fact rather than something only a human
659
+ * squinting at the panel can notice.
660
+ *
661
+ * Rejects, naming the panel, when no hierarchy panel is mounted: an empty
662
+ * tree would be a fabricated answer about a surface nobody is being shown.
663
+ */
664
+ async hierarchy() {
665
+ const data = await this.command({ type: "hierarchy" });
666
+ return data.hierarchy;
667
+ }
668
+ /** Run the Hierarchy panel's own Expand All action. */
669
+ async expandHierarchyAll() {
670
+ await this.command({ type: "expand-hierarchy-all" });
671
+ }
672
+ /** Run the Hierarchy panel's own Collapse All action — Expand All's other
673
+ * half, and the only way back to the tree's rest state through the product
674
+ * (see `HierarchyPanelSnapshot.collapseAll`). */
675
+ async collapseHierarchyAll() {
676
+ await this.command({ type: "collapse-hierarchy-all" });
677
+ }
678
+ /**
679
+ * Write one editable path through the active Inspector's own IO.
680
+ *
681
+ * The answer carries `write` as well as the subject, because an ack alone
682
+ * cannot be believed: a write with no persistence route open succeeds and
683
+ * changes no byte, and `write.persisted` is how the caller tells the two
684
+ * apart without diffing the tree (`InspectedWriteDestination` in `types.ts`).
685
+ */
686
+ async setInspectionField(path, value) {
687
+ const data = await this.command({
688
+ type: "set-inspection-field",
689
+ path,
690
+ value
691
+ });
692
+ return { subject: data.subject, write: data.write };
693
+ }
694
+ /**
695
+ * REMOVE one editable path's authored override — the other half of the write
696
+ * door, and the only one that can express byte-ABSENCE.
697
+ *
698
+ * {@link setInspectionField} writes a VALUE, so reverting a property an
699
+ * authoring gesture ADDED puts the default back EXPLICITLY and leaves the
700
+ * source one attribute heavier than it started. This drops the property, so
701
+ * whatever governs it in its absence takes over — the same `io.remove` the
702
+ * Inspector's revert arrow calls, the same persistence pipe, the same awaited
703
+ * `{ destination, persisted }` ack.
704
+ *
705
+ * Rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field does not declare
706
+ * itself removable or the lane implements no removal door. That refusal is a
707
+ * MISSING SEAM, not a failed removal, and it is coded rather than phrased
708
+ * precisely so a caller can grade the two differently.
709
+ */
710
+ async removeInspectionField(path) {
711
+ const data = await this.command({
712
+ type: "remove-inspection-field",
713
+ path
714
+ });
715
+ return { subject: data.subject, write: data.write };
716
+ }
717
+ /**
718
+ * OPEN one piece of the adapter's SCENE TABLE by id — a scene, a prefab, or
719
+ * a story state, because the table makes them siblings (they differ only in
720
+ * instance site). The ids are exactly what `getState().adapter.scenes.entries`
721
+ * reports, so the table is both the menu and the address space.
722
+ *
723
+ * With a game LIVE in the session, opening a scene the adapter declares
724
+ * reachable through that game's own scenes contract NAVIGATES it — the same
725
+ * switch the editor's own scene picker makes — and the answer carries the
726
+ * game's own reading (`scene`).
727
+ *
728
+ * Rejects with a coded reason rather than prose: `SCENE_NOT_FOUND` (and it
729
+ * names the ids that DO exist), `SCENE_NOT_OPENABLE` carrying the adapter's
730
+ * own declared reason for a scene it says nothing can reach,
731
+ * `SCENE_NAVIGATION_NOT_RUNNING` for a live-only scene with no game running,
732
+ * `SCENE_CONTRACT_UNAVAILABLE` / `SCENE_NOT_IN_CONTRACT` (naming the ids the
733
+ * game itself publishes) / `SCENE_SWITCH_FAILED` when the running game's own
734
+ * navigation cannot take it, `SCENE_NOT_OPENABLE_LIVE` when this session has
735
+ * no remount for a native swap-slot scene,
736
+ * `SCENE_TABLE_UNAVAILABLE` before the adapter has loaded, and
737
+ * `SCENE_DOCUMENT_NOT_MOUNTED` when the host has no document for a piece the
738
+ * table says is openable — a host gap, not a table statement.
739
+ */
740
+ async open(id) {
741
+ return this.command({ type: "open", id });
742
+ }
743
+ /**
744
+ * Undo / redo one project transaction — the same queue the keyboard shortcut
745
+ * drives. `moved` is false when there was nothing left in that direction.
746
+ */
747
+ async undo() {
748
+ return this.command({ type: "undo" });
749
+ }
750
+ async redo() {
751
+ return this.command({ type: "redo" });
752
+ }
753
+ /** Read the editor's actual current durable projection. */
754
+ async currentView() {
755
+ const data = await this.command({ type: "current-view" });
756
+ return data.view;
757
+ }
758
+ /**
759
+ * Capture the active center document exactly as presented to the user.
760
+ *
761
+ * A number is a SQUARE of that size (the default shape); `{width, height}`
762
+ * asks for a shaped frame — a video-aspect look that needs no crop. Both are
763
+ * bounded by the relay budget; see {@link CaptureDimensions}.
764
+ */
765
+ /** Photograph the editor PAGE itself — every panel as the person sees it, at
766
+ * `scale` output pixels per CSS pixel (default `devicePixelRatio`), which is
767
+ * what a 1 px border or a glyph edge is judged through. */
768
+ async captureEditorChrome(options) {
769
+ return this.command({
770
+ type: "capture-editor-chrome",
771
+ ...options?.scale === void 0 ? {} : { scale: options.scale }
772
+ });
773
+ }
774
+ /** With a view, present and capture it in one request so document discovery
775
+ * cannot retarget the capture between two client calls. */
776
+ async captureActiveDocument(size, view) {
777
+ return this.command({
778
+ type: "capture-active-document",
779
+ ...view ? { view } : {},
780
+ ...typeof size === "number" ? { size } : {},
781
+ ...typeof size === "object" && size !== null ? { width: size.width, height: size.height } : {}
782
+ });
783
+ }
784
+ /**
785
+ * Read or drive the ACTIVE center document's own DOM — the scoped
786
+ * editor-chrome door, and the read/gesture half of the same subject
787
+ * {@link captureActiveDocument} photographs. NOT play-mode gated, and NOT
788
+ * page automation: a target outside the active document's container is
789
+ * refused by name. Design and scope contract:
790
+ * `packages/editor/src/editor-document-probe.ts`.
791
+ */
792
+ async documentProbe(step) {
793
+ return this.command({ type: "document-probe", step });
794
+ }
795
+ /**
796
+ * Run a wire-carried step against the ACTIVE document's published context
797
+ * (`packages/editor/src/document-context-registry.ts`) — the REPL door over
798
+ * an open document, in Edit mode. `src` is the step's own `toString()`;
799
+ * same serialization contract as `page-script` (no closures survive).
800
+ */
801
+ /**
802
+ * The Blender lane's doors (`blender-execute`, `blender-scene-info`,
803
+ * `blender-object-info`, `blender-screenshot-view`, `blender-read-file`,
804
+ * `blender-write-file`, `blender-list-files`, `blender-start`,
805
+ * `blender-status`): Blender runs in the editor tab's worker, and
806
+ * `vgai blender-mcp` is transport onto these. `blender-status` is the only
807
+ * one that creates nothing — it answers whether this tab already has a
808
+ * session, which is how a caller survives an editor restart.
809
+ */
810
+ async blender(type, fields = {}) {
811
+ return this.command({ type, ...fields }, { deadlineMs: BLENDER_DEADLINE_MS });
812
+ }
813
+ async documentScript(src) {
814
+ const outcome = await this.command({ type: "document-script", src });
815
+ return outcome.result;
816
+ }
817
+ // --- Display (set semantics) ---
818
+ async setGrid(enabled) {
819
+ await this.command({ type: "set-grid", enabled });
820
+ }
821
+ async setHelpers(enabled) {
822
+ await this.command({ type: "set-helpers", enabled });
823
+ }
824
+ async setStats(enabled) {
825
+ await this.command({ type: "set-stats", enabled });
826
+ }
827
+ async setShadingMode(mode) {
828
+ await this.command({ type: "set-shading-mode", mode });
829
+ }
830
+ async setHelperType(helperType, enabled) {
831
+ await this.command({ type: "set-helper-type", helperType, enabled });
832
+ }
833
+ // --- Transform tools (set semantics) ---
834
+ async setTransformMode(mode) {
835
+ await this.command({ type: "set-transform-mode", mode });
836
+ }
837
+ async setTransformSpace(space) {
838
+ await this.command({ type: "set-transform-space", space });
839
+ }
840
+ async setSnap(enabled) {
841
+ await this.command({ type: "set-snap", enabled });
842
+ }
843
+ // --- Project management ---
844
+ async createProject(name, location, template = "default", exampleId) {
845
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
846
+ method: "POST",
847
+ headers: { "Content-Type": "application/json" },
848
+ body: JSON.stringify({ name, location, template, ...exampleId ? { exampleId } : {} })
849
+ });
850
+ const data = await this.readJson(res);
851
+ if (!res.ok) {
852
+ throw new Error(data.error ?? `Create project failed: ${res.status}`);
853
+ }
854
+ return { path: data.path, config: data.config };
855
+ }
856
+ async openProject(path) {
857
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
858
+ method: "POST",
859
+ headers: { "Content-Type": "application/json" },
860
+ body: JSON.stringify({ path })
861
+ });
862
+ const body = await this.readJson(res);
863
+ if (!res.ok) {
864
+ throw new Error(body.error ?? `Open project failed: ${res.status}`);
865
+ }
866
+ }
867
+ async getProject() {
868
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
869
+ const data = await this.readJson(res);
870
+ if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
871
+ return data.project;
872
+ }
873
+ async listRecentProjects() {
874
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
875
+ const data = await this.readJson(res);
876
+ if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
877
+ return data.projects;
878
+ }
879
+ // --- Registered project tools ---
880
+ /** List tools explicitly registered in `package.json#vgai.tools`.
881
+ * The editor server loads callable metadata in Node; modules never enter the
882
+ * editor browser merely because they were listed. */
883
+ async listProjectTools() {
884
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools`);
885
+ const body = await this.readJson(res);
886
+ if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
887
+ return body;
888
+ }
889
+ /** Execute one Node-hosted project tool through the shared validated
890
+ * dispatcher. Write/destructive tools require `confirm:true`. */
891
+ async runProjectTool(name, input = {}, options = {}) {
892
+ const dispatcher = createDispatcher(0);
893
+ try {
894
+ const res = await this.httpFetch(
895
+ `${this.baseUrl}/__editor/project-tools/run`,
896
+ {
897
+ method: "POST",
898
+ headers: { "Content-Type": "application/json" },
899
+ body: JSON.stringify({
900
+ name,
901
+ input,
902
+ confirm: options.confirm === true,
903
+ // Omitted (not null) when unset — the wire body is JSON and the tool
904
+ // host reads absence as "the sole instance", same convention as the
905
+ // relay's `instance`.
906
+ ...options.instance !== void 0 ? { instance: options.instance } : {}
907
+ })
908
+ },
909
+ dispatcher
910
+ );
911
+ const body = await this.readJson(res);
912
+ if (!body || typeof body !== "object" || typeof body.ok !== "boolean") {
913
+ throw new Error(`Project tool returned an invalid response (${res.status}).`);
914
+ }
915
+ return body;
916
+ } finally {
917
+ await dispatcher?.destroy?.();
918
+ }
919
+ }
920
+ // --- First-party generation job activity ---
921
+ /** Read the one project-local generation job ledger. Provider-native
922
+ * request/result shapes remain on their registered operations. */
923
+ async listGenerationJobs() {
924
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/generations`);
925
+ const body = await this.readJson(res);
926
+ if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
927
+ return body;
928
+ }
929
+ /** Forget operational job state. Accepted provenance and project assets
930
+ * are deliberately unaffected. */
931
+ async forgetGenerationJob(id) {
932
+ const res = await this.httpFetch(
933
+ `${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`,
934
+ {
935
+ method: "DELETE"
936
+ }
937
+ );
938
+ const body = await this.readJson(res);
939
+ if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
940
+ return body.removed === true;
941
+ }
942
+ // --- Logs ---
943
+ async getLogEntries() {
944
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/log-entries`);
945
+ const data = await this.readJson(res);
946
+ if (!res.ok) return [];
947
+ return data.entries;
948
+ }
949
+ // --- State ---
950
+ /**
951
+ * The document table the host resolved — every scene, prefab, page, model,
952
+ * shot, take … the project's finders produced (`getState().adapter.scenes`
953
+ * is the same projection). A command, so it answers wherever the control
954
+ * channel reaches, not only where `/__editor/state` is served.
955
+ */
956
+ async documentTable() {
957
+ return this.command({ type: "document-table" });
958
+ }
959
+ async getState() {
960
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
961
+ const state = await this.readJson(res);
962
+ if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
963
+ return state;
964
+ }
965
+ /**
966
+ * The complete unresolved console set the session is holding right now.
967
+ *
968
+ * Command envelopes only carry COUNTS (`unresolvedConsole` on
969
+ * `commandResponseFor`). The named conditions live on GET `/__editor/console`.
970
+ * This is the method that turns "a command that exits before an envelope
971
+ * arrives" into a real reading: the CLI calls it at start and at exit
972
+ * through the same {@link onEnvelope} observer every other response uses.
973
+ * A session that does not answer within {@link CONSOLE_DRAIN_TIMEOUT_MS} is
974
+ * a thrown error the caller treats as "nothing learned", never a hang.
975
+ */
976
+ async getUnresolvedConsole(opts) {
977
+ const res = await this.httpFetch(
978
+ `${this.baseUrl}/__editor/console${opts?.all === true ? "?all=1" : ""}`,
979
+ { signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) }
980
+ );
981
+ if (!res.ok) {
982
+ throw new Error(`Failed to read unresolved console: ${res.status}`);
983
+ }
984
+ return await this.readJson(res, true);
985
+ }
986
+ /** Acknowledge one named console condition. The response is observed and
987
+ * hydrated through the same path as every other client response. */
988
+ async acknowledgeConsole(input) {
989
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
990
+ method: "POST",
991
+ headers: { "Content-Type": "application/json" },
992
+ body: JSON.stringify(input),
993
+ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS * 4)
994
+ });
995
+ const body = await this.readJson(res);
996
+ return body;
997
+ }
998
+ /**
999
+ * `fetch` for this client's plain routes, with the ONE thing Node's `fetch`
1000
+ * will not do: name why it failed.
1001
+ *
1002
+ * `readJson` below already owns "the server answered the wrong thing"; this
1003
+ * owns "nothing answered at all", which used to reach the caller as the bare
1004
+ * `TypeError: fetch failed` with the real code buried on `.cause`. No retry
1005
+ * here — these routes create projects, run tools and acknowledge console
1006
+ * conditions, so repeating one is the caller's decision. The relayed
1007
+ * `command` path above has its own opt-in retry for the read-only captures.
1008
+ */
1009
+ async httpFetch(url, init, dispatcher) {
1010
+ try {
1011
+ return await dispatchFetch(url, init, dispatcher);
1012
+ } catch (error) {
1013
+ if (error?.name === "TimeoutError") throw error;
1014
+ const { code, detail } = describeFetchFailure(error);
1015
+ throw new EditorCommandError(
1016
+ `${init?.method ?? "GET"} ${url} never reached the editor: ${code}${detail ? ` (${detail})` : ""}. Nothing answered on that port \u2014 check the terminal running \`volter-editor edit\` and confirm the port this client resolved.`,
1017
+ code,
1018
+ false
1019
+ );
1020
+ }
1021
+ }
1022
+ /** Parse one JSON body and hand it to {@link onEnvelope}.
1023
+ *
1024
+ * A command/state envelope carries current counts but not the named set. If
1025
+ * an observer is installed, do the bounded console GET before resolving the
1026
+ * original request. That makes a subsequent `process.exit()` safe: the
1027
+ * observer has already received every condition and occurrence count. */
1028
+ async readJson(res, consoleComplete = false) {
1029
+ const contentType = res.headers.get("content-type") ?? "";
1030
+ if (!contentType.includes("application/json")) {
1031
+ throw new Error(
1032
+ `The editor at ${this.baseUrl} answered with its page fallback (${contentType || "no content-type"}) rather than JSON, so no editor server handled the request. Check that this URL is a running \`volter-editor edit\` session.`
1033
+ );
1034
+ }
1035
+ const body = await res.json();
1036
+ this.observe(body, { unresolvedConsoleComplete: consoleComplete });
1037
+ if (this.onEnvelope !== null && !consoleComplete && body !== null && typeof body === "object" && "unresolvedConsole" in body) {
1038
+ try {
1039
+ const consoleRes = await this.httpFetch(`${this.baseUrl}/__editor/console`, {
1040
+ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS)
1041
+ });
1042
+ if (consoleRes.ok) {
1043
+ const consoleBody = await consoleRes.json();
1044
+ this.observe(consoleBody, { unresolvedConsoleComplete: true });
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ }
1049
+ return body;
1050
+ }
1051
+ /** Hand one response body to {@link onEnvelope}, never letting it throw. */
1052
+ observe(body, observation) {
1053
+ if (this.onEnvelope === null) return;
1054
+ try {
1055
+ this.onEnvelope(body, observation);
1056
+ } catch {
1057
+ }
1058
+ }
1059
+ /**
1060
+ * Whether an editor browser tab is connected to the server *right now*.
1061
+ * Unlike {@link getState}, this reflects live SSE connections, not cached
1062
+ * state — use it to check whether commands will actually reach an editor.
1063
+ */
1064
+ async isConnected() {
1065
+ const state = await this.getState();
1066
+ return state.connected === true;
1067
+ }
1068
+ async waitForState(predicate, timeoutMs = 1e4) {
1069
+ const start = Date.now();
1070
+ while (Date.now() - start < timeoutMs) {
1071
+ const state = await this.getState();
1072
+ if (predicate(state)) return state;
1073
+ await new Promise((r) => setTimeout(r, 100));
1074
+ }
1075
+ throw new Error(`waitForState timed out after ${timeoutMs}ms`);
1076
+ }
1077
+ };
1078
+
1079
+ // src/editor-document.ts
1080
+ var LiveEditorDocument = class {
1081
+ #client;
1082
+ constructor(client) {
1083
+ this.#client = client;
1084
+ }
1085
+ /**
1086
+ * Read matching elements inside the active document: tag, text, attributes,
1087
+ * value/checked/disabled and rect. `matched` is the total before `limit`.
1088
+ *
1089
+ * `styles` additionally resolves named properties per match — and resolving
1090
+ * is the point, because a theme token is an expression until an element
1091
+ * paints it. Ask for the standard property to learn the colour a person
1092
+ * sees; ask for a `--vgai-…` custom property to learn what a rule WOULD
1093
+ * paint, which is the only way to measure a `:hover` colour (`:hover` is a
1094
+ * browser state no synthetic event can enter, so there is deliberately no
1095
+ * hover verb on this door).
1096
+ *
1097
+ * await editor.document.query('.vgai-tree-row', {
1098
+ * styles: ['backgroundColor', '--vgai-widget-regular-hover'],
1099
+ * });
1100
+ */
1101
+ async query(selector, options) {
1102
+ return this.#probe({
1103
+ action: "query",
1104
+ selector,
1105
+ ...options?.scope === void 0 ? {} : { scope: options.scope },
1106
+ ...options?.limit === void 0 ? {} : { limit: options.limit },
1107
+ ...options?.styles === void 0 ? {} : { styles: [...options.styles] }
1108
+ });
1109
+ }
1110
+ /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
1111
+ * — not `element.click()`, which a `pointerdown` listener never sees. */
1112
+ async click(selector, options) {
1113
+ return this.#probe({
1114
+ action: "click",
1115
+ selector,
1116
+ ...options?.scope === void 0 ? {} : { scope: options.scope },
1117
+ ...options?.index === void 0 ? {} : { index: options.index },
1118
+ ...options?.clicks === void 0 ? {} : { clicks: options.clicks }
1119
+ });
1120
+ }
1121
+ /**
1122
+ * TYPE into a field and commit with Enter, the way a person does — one
1123
+ * character at a time through the prototype's value setter, between real
1124
+ * `keydown`/`keyup`.
1125
+ *
1126
+ * `paste` is not a substitute: an untrusted `ClipboardEvent` performs no
1127
+ * default action, so a plain `<input>` with no paste handler keeps its old
1128
+ * value. Omit `selector` to type into whatever inside the scope has focus —
1129
+ * which is what a rename field is, one gesture after
1130
+ * `click(row, { clicks: 2 })`.
1131
+ */
1132
+ async type(text, options) {
1133
+ return this.#probe({ action: "type", text, ...options ?? {} });
1134
+ }
1135
+ /**
1136
+ * A real pointer DRAG across one matched element — press at `from`, move,
1137
+ * release at `to`. The gesture a direct-manipulation canvas needs; a
1138
+ * zero-length drag is a click at that fraction, which `click` (always the
1139
+ * center) cannot place.
1140
+ *
1141
+ * `from`, `to` and every point in `via` are `[x, y]` FRACTIONS OF THE
1142
+ * MATCHED ELEMENT'S BOX, 0..1 from its top-left — NEVER pixels and never
1143
+ * page coordinates. `[0.5, 0.5]` is its center, `[1, 0]` its top-right.
1144
+ * Compute a pixel target by measuring the element first: `query` answers
1145
+ * its `rect`, and `(px - rect.x) / rect.width` is the fraction to pass.
1146
+ */
1147
+ async drag(selector, options) {
1148
+ return this.#probe({
1149
+ action: "drag",
1150
+ selector,
1151
+ ...options.scope === void 0 ? {} : { scope: options.scope },
1152
+ from: options.from,
1153
+ to: options.to,
1154
+ ...options.via === void 0 ? {} : { via: options.via },
1155
+ ...options.steps === void 0 ? {} : { steps: options.steps },
1156
+ ...options.index === void 0 ? {} : { index: options.index },
1157
+ ...options.altKey === void 0 ? {} : { altKey: options.altKey },
1158
+ ...options.ctrlKey === void 0 ? {} : { ctrlKey: options.ctrlKey },
1159
+ ...options.metaKey === void 0 ? {} : { metaKey: options.metaKey },
1160
+ ...options.shiftKey === void 0 ? {} : { shiftKey: options.shiftKey }
1161
+ });
1162
+ }
1163
+ /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
1164
+ async key(key, options) {
1165
+ return this.#probe({ action: "key", key, ...options ?? {} });
1166
+ }
1167
+ /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
1168
+ * in the product can produce. */
1169
+ async paste(text, options) {
1170
+ return this.#probe({ action: "paste", text, ...options ?? {} });
1171
+ }
1172
+ /**
1173
+ * Choose `value` on a `<select>` — a native dropdown's options are drawn by
1174
+ * the OS, so `click` has nothing in the document to resolve, and a plain
1175
+ * `element.value =` is invisible to React. Set through the prototype's own
1176
+ * value setter plus `input`/`change`; `value` is the option's `value`, not
1177
+ * its label. An unknown value is refused with the options it does offer.
1178
+ */
1179
+ async select(selector, value, options) {
1180
+ return this.#probe({
1181
+ action: "select",
1182
+ selector,
1183
+ value,
1184
+ ...options?.scope === void 0 ? {} : { scope: options.scope },
1185
+ ...options?.index === void 0 ? {} : { index: options.index }
1186
+ });
1187
+ }
1188
+ /**
1189
+ * THE REPL over the open document: run `step` in the editor page against the
1190
+ * object the ACTIVE document published as its context (the mesh document
1191
+ * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
1192
+ * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
1193
+ * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
1194
+ * `game.page`: the step's own source travels, so inline every value it
1195
+ * needs and return plain data.
1196
+ */
1197
+ async run(step) {
1198
+ return this.#client.documentScript(step.toString());
1199
+ }
1200
+ #probe(step) {
1201
+ return this.#client.documentProbe(step);
1202
+ }
1203
+ };
1204
+
1205
+ // src/editor.ts
1206
+ var EXTENSION_KIND = {
1207
+ ".glb": "model",
1208
+ ".gltf": "model",
1209
+ ".png": "image",
1210
+ ".jpg": "image",
1211
+ ".jpeg": "image",
1212
+ ".webp": "image",
1213
+ ".gif": "image",
1214
+ ".svg": "image",
1215
+ ".hdr": "image",
1216
+ ".exr": "image",
1217
+ ".mp4": "video",
1218
+ ".webm": "video",
1219
+ ".mp3": "audio",
1220
+ ".ogg": "audio",
1221
+ ".wav": "audio",
1222
+ ".flac": "audio",
1223
+ ".glsl": "source",
1224
+ ".vert": "source",
1225
+ ".frag": "source",
1226
+ // PROJECT SCRIPTS ARE SOURCE. Without these the guess below falls through to
1227
+ // `'json'`, the asset-document router sends the file to the generic JSON
1228
+ // viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
1229
+ // 'json'` is decided before any content routing), and the LIVE MODELING
1230
+ // DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
1231
+ // silently shows a text pane instead of the model. Only `kind: 'source'`
1232
+ // reaches `SourceAssetViewer`, which is what content-routes a project script
1233
+ // to `LiveModuleDocument`. The set matches that viewer's own
1234
+ // `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
1235
+ ".ts": "source",
1236
+ ".tsx": "source",
1237
+ ".mts": "source",
1238
+ ".cts": "source",
1239
+ ".js": "source",
1240
+ ".jsx": "source",
1241
+ ".mjs": "source",
1242
+ ".cjs": "source"
1243
+ };
1244
+ function inferAssetKind(path) {
1245
+ if (path.endsWith(".prefab.json")) return "prefab";
1246
+ const dot = path.lastIndexOf(".");
1247
+ const ext = dot >= 0 ? path.slice(dot).toLowerCase() : "";
1248
+ return EXTENSION_KIND[ext] ?? "json";
1249
+ }
1250
+ var LiveEditor = class {
1251
+ /** `#`-private, not `private`: `volter-editor eval --list` enumerates this object's
1252
+ * real runtime members, and TypeScript's erased `private` would leave the
1253
+ * raw `EditorClient` advertised beside them. */
1254
+ #client;
1255
+ /**
1256
+ * The ACTIVE center document's own DOM: read it, click it, key it, paste
1257
+ * into it. The one door onto editor chrome that is not play-mode gated, and
1258
+ * deliberately scoped to that document alone —
1259
+ * `packages/editor/src/editor-document-probe.ts` carries the design and the
1260
+ * refusal contract. Screenshotting the same subject is
1261
+ * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
1262
+ */
1263
+ document;
1264
+ constructor(client) {
1265
+ this.#client = client;
1266
+ this.document = new LiveEditorDocument(client);
1267
+ }
1268
+ /**
1269
+ * THE BLENDER LANE'S VERBS, from `volter-editor eval`.
1270
+ *
1271
+ * Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
1272
+ * BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
1273
+ * `blender-execute`, `blender-scene-info`, `blender-object-info`,
1274
+ * `blender-screenshot-view`, `blender-read-file`, `blender-write-file`,
1275
+ * `blender-list-files`, `blender-stop` and `blender-status`. They were
1276
+ * reachable from `@volter/editor-sdk` and through `vgai blender-mcp` but from
1277
+ * no GENERAL door, so driving a session meant writing an MCP client script
1278
+ * per question — the same discovery failure `eval-surface.ts`'s header
1279
+ * records, in a lane that had not noticed it yet.
1280
+ *
1281
+ * volter-editor eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
1282
+ *
1283
+ * `blender-status` is the only verb that creates nothing: it answers whether
1284
+ * this tab already has a session without starting one.
1285
+ */
1286
+ async blender(type, fields = {}) {
1287
+ return this.#client.blender(type, fields);
1288
+ }
1289
+ /**
1290
+ * The active authoring adapter's persistence destination — where a save would
1291
+ * land (`status().savePath`). A read only: a three root has no scene document
1292
+ * to open, and its root is activated instead.
1293
+ */
1294
+ async scene() {
1295
+ const state = await this.#client.getState();
1296
+ return state.savePath;
1297
+ }
1298
+ /**
1299
+ * Make the connected human editor show the same subject/view as the agent.
1300
+ * The returned URL is a compact, shareable projection — not a serialized
1301
+ * workspace or document payload.
1302
+ */
1303
+ async present(view) {
1304
+ return this.#client.present(view);
1305
+ }
1306
+ /** The human editor's actual active document, selection, camera and utility. */
1307
+ async currentView() {
1308
+ return this.#client.currentView();
1309
+ }
1310
+ /**
1311
+ * Capture the same center document the human is currently looking at.
1312
+ *
1313
+ * A number is a SQUARE of that size — the default, and the right shape for
1314
+ * an unstaged look at a model. `{width, height}` asks for a shaped frame, so
1315
+ * a video-aspect look needs no crop afterwards. Both are bounded by the
1316
+ * relay budget (64-1024 per side, total no larger than a 1024 square); see
1317
+ * `@volter/editor-sdk`'s `CaptureDimensions`.
1318
+ * Supply a view to present and photograph it in one editor request.
1319
+ */
1320
+ async captureActiveDocument(size, view) {
1321
+ return this.#client.captureActiveDocument(size, view);
1322
+ }
1323
+ /**
1324
+ * Photograph the editor PAGE — every panel, tab strip and viewport as the
1325
+ * person sees it. `vgai screenshot editor` is this verb from the shell. The
1326
+ * one door for judging chrome sighted: a skin, a workspace arrangement or a
1327
+ * contributed panel is looked at through this, never guessed at from DOM
1328
+ * probes. The page at its own layout, `scale` output pixels per CSS pixel
1329
+ * (default `devicePixelRatio`) — a 1 px border or a glyph stroke is only
1330
+ * judgeable at the scale the reference it is compared against was captured
1331
+ * at, and the result reports its own `size` and `scale`.
1332
+ */
1333
+ async captureEditorChrome(options) {
1334
+ return this.#client.captureEditorChrome(options);
1335
+ }
1336
+ /** `'all'` -> `EditorClient.selectAll()` (mirrors `vgai select --all`); otherwise `EditorClient.select(id)` (mirrors `vgai select <entityId>`). */
1337
+ async select(id) {
1338
+ if (id === "all") {
1339
+ await this.#client.selectAll();
1340
+ return;
1341
+ }
1342
+ await this.#client.select(id);
1343
+ }
1344
+ /** Mirrors `vgai deselect`. */
1345
+ async deselect() {
1346
+ await this.#client.select(null);
1347
+ }
1348
+ /** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
1349
+ async focus(id) {
1350
+ if (id !== void 0) {
1351
+ await this.#client.focusEntity(id);
1352
+ return;
1353
+ }
1354
+ await this.#client.focusSelection();
1355
+ }
1356
+ async frame(target) {
1357
+ if (typeof target === "string") {
1358
+ await this.#client.frameEntity(target);
1359
+ return;
1360
+ }
1361
+ await this.#client.frameDocument(target?.fit);
1362
+ }
1363
+ /**
1364
+ * WATCH THE AGENT LOOK AROUND THE MODEL.
1365
+ *
1366
+ * Swings the open Object3D document's camera — the camera the human's tab is
1367
+ * showing — around the framed subject by `azimuth`/`elevation` RADIANS,
1368
+ * animated over `duration` seconds (default 0.6), and resolves when the move
1369
+ * ends. This is deliberately not a jump cut: the point of the verb is that a
1370
+ * person watching sees the agent walk around the thing it is working on.
1371
+ *
1372
+ * `await editor.orbit({ azimuth: Math.PI / 2 })` — a quarter turn to the right.
1373
+ *
1374
+ * There is ONE camera, and the human owns it: a drag during the move cancels
1375
+ * it exactly where it is, and the resolved outcome says `cancelledBy:
1376
+ * 'human'` rather than throwing. A second look verb supersedes the first.
1377
+ * The move is drawn by the document's own frame loop, so a document that
1378
+ * isn't being drawn (background tab, inactive panel) doesn't orbit.
1379
+ */
1380
+ async orbit(options) {
1381
+ const known = ["azimuth", "elevation", "duration"];
1382
+ const unknown = Object.keys(options ?? {}).filter((key) => !known.includes(key));
1383
+ if (unknown.length > 0)
1384
+ throw new Error(
1385
+ `editor.orbit: ${unknown.join(", ")} ${unknown.length === 1 ? "is not a key" : "are not keys"} this verb takes. It takes azimuth and elevation in RADIANS (relative to where the camera is now) and duration in SECONDS.`
1386
+ );
1387
+ return this.#client.orbitDocument(options);
1388
+ }
1389
+ /**
1390
+ * A slow full revolution of the open document's subject — {@link orbit} with
1391
+ * the turns spelled out and a constant angular rate. Resolves at the end of
1392
+ * the last revolution.
1393
+ */
1394
+ async turntable(options) {
1395
+ return this.#client.turntableDocument(options);
1396
+ }
1397
+ async view(preset) {
1398
+ await this.#client.viewPreset(preset);
1399
+ }
1400
+ /**
1401
+ * Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
1402
+ *
1403
+ * A workspace is a task-named LAYOUT MEMORY over the one dock
1404
+ * (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
1405
+ * standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
1406
+ * Switching is an EXPLICIT act — nothing in the editor moves chrome on its
1407
+ * own, opening a document included — and this is the session door to it,
1408
+ * beside `Window → Workspace` and the registered actions.
1409
+ *
1410
+ * Resolves once the dock has finished rebuilding, so a capture taken
1411
+ * immediately after photographs the arrangement that was asked for. Each
1412
+ * workspace remembers the user's own hand-tuning per project, so switching
1413
+ * away and back is lossless.
1414
+ */
1415
+ async workspace(id) {
1416
+ await this.#client.setWorkspace(id);
1417
+ }
1418
+ /**
1419
+ * Apply a STYLE BUNDLE by id — the chrome's palette, material, icon set and
1420
+ * region defaults in one gesture, the session door beside
1421
+ * `View → <Style> Style`. A bundle the open project does not offer refuses
1422
+ * and names the vocabulary; `currentView().style` reports the one worn.
1423
+ */
1424
+ async style(id) {
1425
+ await this.#client.setStyle(id);
1426
+ }
1427
+ /**
1428
+ * Set the MATERIAL apart from the bundle that usually carries it.
1429
+ * Appearance is palette × material, independent axes by ruling, so
1430
+ * `style()` alone can never say whether a cost belongs to the blur or to
1431
+ * the palette. This is the door that measures them apart; it answers with
1432
+ * what the chrome wears afterwards (`style` is `null` when the mix matches
1433
+ * no registered bundle).
1434
+ */
1435
+ async appearance(appearance) {
1436
+ return this.#client.setAppearance(appearance);
1437
+ }
1438
+ /** Focus an editor panel: a viewport tab, the console, the build surface, or
1439
+ * any key the editor's static-panel registry holds — an unknown key refuses
1440
+ * naming the ones it does. */
1441
+ async showPanel(name) {
1442
+ switch (name) {
1443
+ case "viewport-edit":
1444
+ await this.#client.showViewport("edit");
1445
+ return;
1446
+ case "console":
1447
+ await this.#client.toggleConsole();
1448
+ return;
1449
+ default:
1450
+ await this.#client.showPanel(name);
1451
+ return;
1452
+ }
1453
+ }
1454
+ /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
1455
+ async openAsset(path, kind) {
1456
+ await this.#client.openAsset(path, kind ?? inferAssetKind(path));
1457
+ }
1458
+ /**
1459
+ * SELECT a project asset — the browser's single click, which fills the
1460
+ * Inspector without opening a document. `openAsset` is the double click.
1461
+ *
1462
+ * This is how a project's own `asset.inspector` section is reached: select
1463
+ * the file it matches, then `inspect()` lists the verbs that section
1464
+ * declares and `runAction(id)` runs one. Selecting a path nothing matches
1465
+ * is not an error — the Inspector shows what it has, exactly as it does
1466
+ * for a human.
1467
+ */
1468
+ async selectAsset(path) {
1469
+ await this.#client.selectAsset(path);
1470
+ }
1471
+ /**
1472
+ * Captures the editor's native four-view preview. A bare string is a
1473
+ * project-relative asset path (the common case); an explicit source object
1474
+ * targets a path, a LIVE SCENE ENTITY (`assetPreview({ entityId }, …)`) —
1475
+ * which is what makes `options.stage: 'scene'`, the entity photographed
1476
+ * where it stands under the scene's own lighting, reachable from here — or
1477
+ * RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
1478
+ * only in the calling Node process's memory and has never been written to
1479
+ * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
1480
+ * always produced, and the bytes form is lab-only.
1481
+ */
1482
+ async assetPreview(source, options) {
1483
+ return this.#client.captureAssetPreview(
1484
+ typeof source === "string" ? { assetPath: source } : source,
1485
+ options
1486
+ );
1487
+ }
1488
+ /**
1489
+ * The same subject photographed as a LABELED SHOT SET instead of the four
1490
+ * views — a caller-supplied definition of turntable yaws and bone-anchored
1491
+ * crops, rendered against the asset's own skeleton, with a contact sheet.
1492
+ * Every source {@link assetPreview} takes works here, GLB bytes included:
1493
+ * a shot set stages its own subject, so it needs no place to stand.
1494
+ *
1495
+ * Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
1496
+ */
1497
+ async assetPreviewShots(source, definition, options) {
1498
+ return this.#client.captureShotSetPreview(
1499
+ typeof source === "string" ? { assetPath: source } : source,
1500
+ definition,
1501
+ options
1502
+ );
1503
+ }
1504
+ async grid(on) {
1505
+ await this.#client.setGrid(on);
1506
+ }
1507
+ async helpers(on) {
1508
+ await this.#client.setHelpers(on);
1509
+ }
1510
+ async stats(on) {
1511
+ await this.#client.setStats(on);
1512
+ }
1513
+ async shading(mode) {
1514
+ await this.#client.setShadingMode(mode);
1515
+ }
1516
+ /**
1517
+ * READ the inspector, as data — the serialized inspection subject
1518
+ * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
1519
+ * "The Inspection Model"). This is the Figma-Inspect analog: whatever a
1520
+ * human would see in the inspector right now — the subject's identity, its
1521
+ * verbs, and every identified section in display order, with a `fields`
1522
+ * section's CURRENT VALUES at their scriptable `path`s.
1523
+ *
1524
+ * Reach for it whenever the next step depends on what an object actually
1525
+ * IS: `await editor.select(id)` then `await editor.inspect()` answers "what
1526
+ * properties does this thing have, and what are they set to" in one call,
1527
+ * against the same model the panel renders — no scene-graph reads, no
1528
+ * guessing at property names.
1529
+ *
1530
+ * When the inspector is showing NOTHING — nothing selected on a surface
1531
+ * with no empty-state subject of its own, which is most of them — the answer
1532
+ * is `{none: true}`, so "the human sees no inspector" and "the read failed"
1533
+ * are never the same value. A surface whose empty space IS a real thing (an
1534
+ * open Asset Lab document) still answers with that subject, and never with
1535
+ * another surface's.
1536
+ *
1537
+ * A `custom` section body is a named opaque: the editor renders it with
1538
+ * React, so the wire reports its identity rather than pretending to describe
1539
+ * its rendering — plus, when the section can say what it DISPLAYS, a `data`
1540
+ * payload in its own vocabulary (`transform` carries
1541
+ * `{position, rotation, scale}`, rotation in Euler XYZ degrees).
1542
+ */
1543
+ async inspect() {
1544
+ return this.#client.inspect();
1545
+ }
1546
+ /** Run one verb listed by `inspect().quickActions`, through the same action
1547
+ * the human Inspector button invokes. */
1548
+ async runAction(actionId) {
1549
+ return this.#client.runInspectionAction(actionId);
1550
+ }
1551
+ /**
1552
+ * Run ONE command by id — the door to everything the command palette lists.
1553
+ *
1554
+ * ONE NAME (orchestrator ruling 2026-09-19). There were briefly TWO doors
1555
+ * onto the one view-verb table — this one and `editor.viewVerb(view, verb)`,
1556
+ * which addressed the same registry by its two halves. A second addressing
1557
+ * of one table is a second name for one thing, and an agent reading
1558
+ * `--list` had to choose between them with nothing to choose on. This door
1559
+ * stays because it is strictly wider: it addresses a COMMAND ID, so under
1560
+ * the frame it reaches everything the workbench knows — a `vgai.action.<id>`
1561
+ * editor action, one of VS Code's own — and not only a view. A VIEW is
1562
+ * reached by spelling its verb's command id:
1563
+ *
1564
+ * await editor.command('vgai.blender-uv-view.state')
1565
+ * await editor.command('vgai.blender-uv-view.zoom', { to: 600 })
1566
+ *
1567
+ * await editor.command('vgai.blender-node-view.view-all')
1568
+ * await editor.command('vgai.blender-node-view.look', { node: 'Principled BSDF' })
1569
+ *
1570
+ * Under the Code-OSS frame this is the workbench's own command service, so
1571
+ * any command id works — ours and VS Code's alike. Standalone `vgai edit`
1572
+ * has no command service and answers the `vgai.<view>.<verb>` shape off the
1573
+ * SAME verb table the frame's commands call, refusing any other id by name.
1574
+ * One table, two doors, exactly like the keymap's.
1575
+ *
1576
+ * Answers with whatever the command returned — a view verb's own state, or
1577
+ * `null` for a command that returns nothing.
1578
+ */
1579
+ async command(commandId, args) {
1580
+ return this.#client.runCommand(commandId, args);
1581
+ }
1582
+ /**
1583
+ * RESTRUCTURE the authored tree — the hierarchy context menu's own verbs.
1584
+ *
1585
+ * `create`, `delete`, `duplicate`, `reparent`, `reorder`, `wrap`, `unwrap`,
1586
+ * `group`, `ungroup`, `copy`, `cut`, `paste`; `extractComponent` and
1587
+ * `forkComponent` are the two that write whole new files and have their own
1588
+ * doors below. All of them run the SAME `authoring/consumer-actions.ts`
1589
+ * helpers the menu items call, so there is one implementation of each op and
1590
+ * not a second that can disagree with what a human gets.
1591
+ *
1592
+ * It exists because the menu is a POINTER surface: every one of these ops was
1593
+ * reachable only by right-clicking a hierarchy row, which is nothing an agent
1594
+ * can do — so for an ingest root, whose only authoring surface IS the editor,
1595
+ * structure was closed entirely.
1596
+ *
1597
+ * `id`/`ids` default to the current selection. The answer carries the same
1598
+ * per-edit `write` ack `setField` does, so `write.persisted` tells a saved
1599
+ * restructure from a live-only one. An op the active adapter does not provide
1600
+ * REJECTS by name — never a silent no-op.
1601
+ */
1602
+ async structure(op, options) {
1603
+ return this.#client.structureOp(op, options ?? {});
1604
+ }
1605
+ /**
1606
+ * "Extract Component…" — lift the selected native subtree into its own
1607
+ * component file (plus a story) and replace the callsite with it.
1608
+ *
1609
+ * Answers the action's own sentence, which NAMES both new files, because
1610
+ * undo owns the callsite edit and will not remove them.
1611
+ */
1612
+ async extractComponent(options) {
1613
+ return (await this.#client.extractComponent(options ?? {})).hint;
1614
+ }
1615
+ /**
1616
+ * "Fork Component…" — copy the selected instance's component definition to a
1617
+ * new file and retarget THIS CALLSITE at it.
1618
+ *
1619
+ * One callsite is the unit of the edit; when that callsite sits inside a
1620
+ * component rendered many times, every one of those renders now renders the
1621
+ * fork.
1622
+ */
1623
+ async forkComponent(options) {
1624
+ return (await this.#client.forkComponent(options ?? {})).hint;
1625
+ }
1626
+ /**
1627
+ * READ the hierarchy panel, as data — the rows a human is looking at right
1628
+ * now, nested exactly as the panel nests them.
1629
+ *
1630
+ * The companion to {@link inspect}: that one answers "what IS the selected
1631
+ * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
1632
+ * own output, not a fresh walk of the scene — the adapter's tree after the
1633
+ * component marks fold implementation subtrees (bones, particle renderers,
1634
+ * instanced pools), after the internals reveal, the document promotion, the
1635
+ * child cap, the collapse state, the search filter and the selection scope.
1636
+ *
1637
+ * Works in play mode and edit mode; the answer says which (`playState`,
1638
+ * `activeViewportTab`), because the two are different adapters and a tree
1639
+ * that looks wrong is very often the wrong adapter's tree.
1640
+ *
1641
+ * Prefer this over `status().entities`, which is deliberately a different
1642
+ * question — the RAW adapter tree, unprojected. A panel that renders the
1643
+ * wrong rows looks perfectly healthy in that facet.
1644
+ *
1645
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
1646
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
1647
+ * panel draws a caret at all), so "this subtree exists but nothing in the UI
1648
+ * opens it" is a fact you can read rather than one you have to notice.
1649
+ *
1650
+ * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
1651
+ * tree would be a fabricated answer about a surface nobody is being shown.
1652
+ */
1653
+ async hierarchy() {
1654
+ return this.#client.hierarchy();
1655
+ }
1656
+ /** Expand every branch through the Hierarchy panel's own action. */
1657
+ async expandHierarchyAll() {
1658
+ await this.#client.expandHierarchyAll();
1659
+ }
1660
+ /** Collapse every branch through the same panel action. Expanding is
1661
+ * persisted per project, so without this the tree's REST STATE — what a
1662
+ * person sees on opening the project — is unreachable once any reader has
1663
+ * expanded it. */
1664
+ async collapseHierarchyAll() {
1665
+ await this.#client.collapseHierarchyAll();
1666
+ }
1667
+ /**
1668
+ * Write one editable field from `inspect()` by its stable path, through the
1669
+ * same Inspector IO and persistence boundary the human control uses.
1670
+ *
1671
+ * The answer is `{ subject, write }`, and `write` is the half worth reading
1672
+ * first: a write with no persistence route open still succeeds — it lands on
1673
+ * the live object and journals live-only — so `write.persisted` is how you
1674
+ * tell a saved edit from one that will not survive the session, without
1675
+ * diffing the tree. `write.destination` is the adapter's own words for where
1676
+ * it went ("live-only (not saved)" is a destination, never silence).
1677
+ */
1678
+ async setField(path, value) {
1679
+ return this.#client.setInspectionField(path, value);
1680
+ }
1681
+ /**
1682
+ * REMOVE one field's authored override — the revert arrow, as a command.
1683
+ *
1684
+ * Reach for this instead of `setField` whenever you are UNDOING an edit that
1685
+ * added a property the source did not carry: `setField` can only write a
1686
+ * value, so setting the default back leaves `position={[0, 0, 0]}` in the
1687
+ * file where there was nothing before. Only this door restores the bytes.
1688
+ *
1689
+ * The answer is the same `{ subject, write }` shape, awaited past the bytes.
1690
+ * It rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field is not
1691
+ * declared removable or the lane has no removal door — which is a missing
1692
+ * seam to report, not a removal that failed.
1693
+ */
1694
+ async removeField(path) {
1695
+ return this.#client.removeInspectionField(path);
1696
+ }
1697
+ /** Open a document by its adapter-declared id, through its registered owner. */
1698
+ async open(id) {
1699
+ return this.#client.open(id);
1700
+ }
1701
+ /** Undo / redo one project transaction, through the session's own history
1702
+ * queue — the same one the keyboard shortcut drives. */
1703
+ async undo() {
1704
+ return this.#client.undo();
1705
+ }
1706
+ async redo() {
1707
+ return this.#client.redo();
1708
+ }
1709
+ /** Mirrors `vgai status` — the full live editor state as JSON. */
1710
+ async status() {
1711
+ return this.#client.getState();
1712
+ }
1713
+ /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
1714
+ async screenshot(size) {
1715
+ return this.#client.captureViewport(size);
1716
+ }
1717
+ };
1718
+
1719
+ // src/tools.ts
1720
+ var LiveTools = class {
1721
+ /** `#`-private for the same reason `LiveEditor.#client` is. */
1722
+ #client;
1723
+ constructor(client) {
1724
+ this.#client = client;
1725
+ }
1726
+ /** Enumerate the exact `package.json#vgai.tools` catalog without executing it. */
1727
+ async list() {
1728
+ return this.#client.listProjectTools();
1729
+ }
1730
+ /** Return one tool's discoverable metadata, or `null` when it is not registered. */
1731
+ async describe(name) {
1732
+ const catalog = await this.list();
1733
+ return catalog.tools.find((tool) => tool.name === name) ?? null;
1734
+ }
1735
+ /**
1736
+ * Invoke the same validated callable used by the editor and CLI.
1737
+ *
1738
+ * `instance` names WHICH mounted instance the tool should drive when several
1739
+ * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
1740
+ * and a tool that drives the game binds
1741
+ * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
1742
+ * the tool then targets the sole live instance, exactly as before.
1743
+ */
1744
+ async run(name, input = {}, options = {}) {
1745
+ return this.#client.runProjectTool(name, input, options);
1746
+ }
1747
+ };
1748
+
1749
+ // src/lazy-proxy.ts
1750
+ function isFunction(value) {
1751
+ return typeof value === "function";
1752
+ }
1753
+ function walk(root, path) {
1754
+ if (path.length === 0) return { thisArg: void 0, fn: root };
1755
+ let obj = root;
1756
+ for (let i = 0; i < path.length - 1; i++) {
1757
+ const key = path[i];
1758
+ obj = obj[key];
1759
+ }
1760
+ const lastKey = path[path.length - 1];
1761
+ return { thisArg: obj, fn: obj[lastKey] };
1762
+ }
1763
+ function lazyChainProxy(resolveRoot, path = []) {
1764
+ const callableTarget = (() => {
1765
+ });
1766
+ return new Proxy(callableTarget, {
1767
+ get(_target, prop) {
1768
+ if (prop === "then" || prop === "catch" || prop === "finally") return void 0;
1769
+ return lazyChainProxy(resolveRoot, [...path, prop]);
1770
+ },
1771
+ apply(_target, _thisArg, args) {
1772
+ return resolveRoot().then((root) => {
1773
+ const { thisArg, fn } = walk(root, path);
1774
+ if (!isFunction(fn)) {
1775
+ const label = path.length > 0 ? path.map(String).join(".") : "(the connected value)";
1776
+ throw new TypeError(`@volter/editor-live: ${label} is not a function on the connected session.`);
1777
+ }
1778
+ return fn.apply(thisArg, args);
1779
+ });
1780
+ }
1781
+ });
1782
+ }
1783
+
1784
+ // src/session.ts
1785
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
1786
+ import { dirname, join as join3, resolve as resolve2 } from "node:path";
1787
+
1788
+ // ../editor-project/src/manifest/locate.ts
1789
+ import { existsSync } from "node:fs";
1790
+ import { join } from "node:path";
1791
+
1792
+ // ../editor-project/src/manifest/filename.ts
1793
+ var MANIFEST_FILENAME = "vgai.project.json";
1794
+ var REMOVED_MANIFEST_FILENAME = "vgai.game.json";
1795
+ function removedManifestFilenameMessage(where) {
1796
+ return `${where}: found \`${REMOVED_MANIFEST_FILENAME}\` and no \`${MANIFEST_FILENAME}\`. \`${REMOVED_MANIFEST_FILENAME}\` was REMOVED (the legacy-removal doctrine, docs/ARCHITECTURE-CORE.md \xA7Vocabulary) \u2014 nothing reads it any more, and it is deliberately NOT read as a fallback, because a second accepted name is the defect.
1797
+ Fix: rename it \u2014 \`git mv ${REMOVED_MANIFEST_FILENAME} ${MANIFEST_FILENAME}\`. The contents are unchanged; only the filename moved.`;
1798
+ }
1799
+
1800
+ // ../editor-project/src/manifest/locate.ts
1801
+ function assertNoRemovedManifestFilename(dir) {
1802
+ if (existsSync(join(dir, MANIFEST_FILENAME))) return;
1803
+ if (!existsSync(join(dir, REMOVED_MANIFEST_FILENAME))) return;
1804
+ throw new Error(removedManifestFilenameMessage(join(dir, REMOVED_MANIFEST_FILENAME)));
1805
+ }
1806
+ function resolveManifestPath(dir) {
1807
+ assertNoRemovedManifestFilename(dir);
1808
+ return join(dir, MANIFEST_FILENAME);
1809
+ }
1810
+
1811
+ // ../editor-sdk/src/session/registry-format.ts
1812
+ import {
1813
+ mkdirSync,
1814
+ readdirSync,
1815
+ readFileSync,
1816
+ realpathSync,
1817
+ unlinkSync,
1818
+ writeFileSync
1819
+ } from "node:fs";
1820
+ import { homedir } from "node:os";
1821
+ import { join as join2, resolve } from "node:path";
1822
+ var EDITOR_SESSIONS_REGISTRY_FILE = join2(homedir(), ".vgai", "editor-sessions.json");
1823
+ function isEditorSessionEntry(v) {
1824
+ if (typeof v !== "object" || v === null) return false;
1825
+ const s = v;
1826
+ const optionalIdentity = (value) => value === void 0 || value === null || typeof value === "string";
1827
+ return (typeof s["project"] === "string" || s["project"] === null) && typeof s["port"] === "number" && typeof s["pid"] === "number" && typeof s["startedAt"] === "string" && optionalIdentity(s["sessionId"]) && optionalIdentity(s["controlSecret"]) && optionalIdentity(s["repositoryId"]) && optionalIdentity(s["worktreeId"]) && optionalIdentity(s["worktreeRoot"]) && optionalIdentity(s["projectRelativePath"]) && optionalIdentity(s["branch"]) && optionalIdentity(s["headCommit"]) && optionalIdentity(s["baseCommit"]);
1828
+ }
1829
+ function normalizeEditorSessionEntry(session) {
1830
+ return {
1831
+ ...session,
1832
+ sessionId: session.sessionId ?? null,
1833
+ controlSecret: session.controlSecret ?? null,
1834
+ repositoryId: session.repositoryId ?? null,
1835
+ worktreeId: session.worktreeId ?? null,
1836
+ worktreeRoot: session.worktreeRoot ?? null,
1837
+ projectRelativePath: session.projectRelativePath ?? null,
1838
+ branch: session.branch ?? null,
1839
+ headCommit: session.headCommit ?? null,
1840
+ baseCommit: session.baseCommit ?? null
1841
+ };
1842
+ }
1843
+ function pidAlive(pid) {
1844
+ try {
1845
+ process.kill(pid, 0);
1846
+ return true;
1847
+ } catch {
1848
+ return false;
1849
+ }
1850
+ }
1851
+ function readLiveRegisteredSessions() {
1852
+ try {
1853
+ const raw = JSON.parse(readFileSync(EDITOR_SESSIONS_REGISTRY_FILE, "utf8"));
1854
+ return Array.isArray(raw) ? raw.filter(isEditorSessionEntry).map(normalizeEditorSessionEntry).filter((s) => pidAlive(s.pid)) : [];
1855
+ } catch {
1856
+ return [];
1857
+ }
1858
+ }
1859
+ function servedProjectAnswer(body) {
1860
+ const b = body;
1861
+ return {
1862
+ path: b.project?.path ?? b.serving?.path ?? null,
1863
+ manifestError: b.project ? null : b.serving?.error ?? null
1864
+ };
1865
+ }
1866
+
1867
+ // ../editor-sdk/src/session/discovery.ts
1868
+ var EDITOR_SESSION_DISCOVERY_TIMEOUT_MS = 2e3;
1869
+ var EDITOR_PROBE_TIMEOUT_MS = 1500;
1870
+ var EditorTimeoutError = class extends Error {
1871
+ constructor(label, ms) {
1872
+ super(`${label} timed out after ${ms}ms`);
1873
+ this.name = "EditorTimeoutError";
1874
+ }
1875
+ };
1876
+ function withTimeout(promise, ms, label) {
1877
+ return new Promise((resolvePromise, reject) => {
1878
+ const timer = setTimeout(() => reject(new EditorTimeoutError(label, ms)), ms);
1879
+ promise.then(
1880
+ (v) => {
1881
+ clearTimeout(timer);
1882
+ resolvePromise(v);
1883
+ },
1884
+ (err) => {
1885
+ clearTimeout(timer);
1886
+ reject(err instanceof Error ? err : new Error(String(err)));
1887
+ }
1888
+ );
1889
+ });
1890
+ }
1891
+ async function fetchJson(url, timeoutMs) {
1892
+ try {
1893
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
1894
+ if (!res.ok) return void 0;
1895
+ return await res.json();
1896
+ } catch {
1897
+ return void 0;
1898
+ }
1899
+ }
1900
+ function baseUrl(session) {
1901
+ return session.url ?? `http://127.0.0.1:${session.port}`;
1902
+ }
1903
+ var HttpSessionDiscovery = class {
1904
+ async listSessions(timeoutMs) {
1905
+ const registered = readLiveRegisteredSessions();
1906
+ const perProbeTimeout = Math.min(EDITOR_PROBE_TIMEOUT_MS, Math.max(200, timeoutMs));
1907
+ const probes = await Promise.all(
1908
+ registered.map(async (s) => {
1909
+ const body = await fetchJson(
1910
+ `${baseUrl({ port: s.port, project: null, pid: s.pid })}/__editor/project`,
1911
+ perProbeTimeout
1912
+ );
1913
+ if (body === void 0) return void 0;
1914
+ const served = servedProjectAnswer(body);
1915
+ const info = {
1916
+ port: s.port,
1917
+ project: served.path,
1918
+ pid: s.pid,
1919
+ manifestError: served.manifestError
1920
+ };
1921
+ return info;
1922
+ })
1923
+ );
1924
+ return probes.filter((p) => p !== void 0);
1925
+ }
1926
+ };
1927
+
1928
+ // src/session.ts
1929
+ function findProjectRootFrom(dir) {
1930
+ let cur = resolve2(dir);
1931
+ for (; ; ) {
1932
+ if (existsSync2(resolveManifestPath(cur))) return cur;
1933
+ const parent = dirname(cur);
1934
+ if (parent === cur) return null;
1935
+ cur = parent;
1936
+ }
1937
+ }
1938
+ function canonicalPath(path) {
1939
+ try {
1940
+ return realpathSync2(path);
1941
+ } catch {
1942
+ return resolve2(path);
1943
+ }
1944
+ }
1945
+ function readProjectSession(projectRoot) {
1946
+ try {
1947
+ const value = JSON.parse(
1948
+ readFileSync2(join3(projectRoot, ".vgai", "session.json"), "utf8")
1949
+ );
1950
+ if (typeof value !== "object" || value === null) return null;
1951
+ const hint = value;
1952
+ if (typeof hint["port"] !== "number" || !Number.isInteger(hint["port"]) || hint["port"] <= 0 || typeof hint["pid"] !== "number" || typeof hint["url"] !== "string" || typeof hint["startedAt"] !== "string") {
1953
+ return null;
1954
+ }
1955
+ return hint;
1956
+ } catch {
1957
+ return null;
1958
+ }
1959
+ }
1960
+ async function probeServedProject(port) {
1961
+ try {
1962
+ const response = await fetch(`http://127.0.0.1:${port}/__editor/project`, {
1963
+ signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS)
1964
+ });
1965
+ if (!response.ok) return void 0;
1966
+ return servedProjectAnswer(await response.json());
1967
+ } catch {
1968
+ return void 0;
1969
+ }
1970
+ }
1971
+ function manifestRefusal(projectRoot, manifestError) {
1972
+ return new Error(
1973
+ `@volter/editor-live: the editor session for ${projectRoot} is live, but its vgai.project.json does not load, so there is no editor or game to drive \u2014 the editor page is showing this same error. Fix the manifest and retry; the session recovers on save, no restart needed.
1974
+ ${manifestError}`
1975
+ );
1976
+ }
1977
+ function noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure) {
1978
+ if (discoveryFailure !== null) {
1979
+ return new Error(
1980
+ `@volter/editor-live: could not READ the editor session registry while looking for ${projectRoot} \u2014 ${discoveryFailure}. This is not the answer "no editor is running": the question went unanswered, so nothing is known about what is live. Retry (a probe can time out while the box is loaded); if it keeps failing, \`vgai sessions\` asks the same question directly.`
1981
+ );
1982
+ }
1983
+ if (sessions.length === 0) {
1984
+ return new Error(
1985
+ `@volter/editor-live: the editor session registry is readable and lists NO live sessions, so none covers ${projectRoot}. @volter/editor-live only attaches to an already-running session \u2014 it never starts one \u2014 so run \`volter-editor edit\` in that project first, then retry.`
1986
+ );
1987
+ }
1988
+ const listed = sessions.map((s) => ` port ${s.port} \u2192 ${s.project === null ? "(no project)" : s.project}`).join("\n");
1989
+ return new Error(
1990
+ `@volter/editor-live: ${sessions.length} live editor session(s) are running, but none of them opens ${projectRoot}. @volter/editor-live never silently attaches to a different project.
1991
+ looking for (resolved): ${canon}
1992
+ live sessions:
1993
+ ${listed}
1994
+ If one of those is meant to be this project, the two paths differ after resolution \u2014 the usual cause is a git worktree or a symlink, where the session was opened through a different path to the same files. Run \`volter-editor edit\` from THIS path, or use the path the session lists.`
1995
+ );
1996
+ }
1997
+ async function resolveSession(projectDir = process.cwd(), deps = {}) {
1998
+ const findRoot = deps.findProjectRootFrom ?? findProjectRootFrom;
1999
+ const transport = deps.transport ?? new HttpSessionDiscovery();
2000
+ const projectRoot = findRoot(projectDir);
2001
+ if (projectRoot === null) {
2002
+ throw new Error(
2003
+ `@volter/editor-live: no vgai.project.json found in ${projectDir} or any parent directory \u2014 is this a vgai project?`
2004
+ );
2005
+ }
2006
+ const localHint = (deps.readProjectSession ?? readProjectSession)(projectRoot);
2007
+ if (localHint) {
2008
+ if (deps.verifyProjectSession) {
2009
+ if (await deps.verifyProjectSession(localHint, projectRoot)) {
2010
+ return { port: localHint.port, projectRoot };
2011
+ }
2012
+ } else {
2013
+ const served = await probeServedProject(localHint.port);
2014
+ if (served?.path != null && canonicalPath(served.path) === canonicalPath(projectRoot)) {
2015
+ if (served.manifestError !== null) throw manifestRefusal(projectRoot, served.manifestError);
2016
+ return { port: localHint.port, projectRoot };
2017
+ }
2018
+ }
2019
+ }
2020
+ let sessions;
2021
+ let discoveryFailure = null;
2022
+ try {
2023
+ sessions = await withTimeout(
2024
+ transport.listSessions(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
2025
+ EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
2026
+ "editor session discovery"
2027
+ );
2028
+ } catch (err) {
2029
+ sessions = [];
2030
+ discoveryFailure = err instanceof Error ? err.message : String(err);
2031
+ }
2032
+ const canon = canonicalPath(projectRoot);
2033
+ const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
2034
+ if (!match) throw noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure);
2035
+ if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError);
2036
+ return { port: match.port, projectRoot };
2037
+ }
2038
+
2039
+ // src/singleton.ts
2040
+ function createLazySession(factory) {
2041
+ let promise = null;
2042
+ return {
2043
+ ensure() {
2044
+ promise ??= factory();
2045
+ return promise;
2046
+ },
2047
+ reset() {
2048
+ promise = null;
2049
+ }
2050
+ };
2051
+ }
2052
+
2053
+ // src/index.ts
2054
+ function bindTo(port) {
2055
+ const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
2056
+ return { editor: new LiveEditor(client), tools: new LiveTools(client) };
2057
+ }
2058
+ function unconnectedBindings() {
2059
+ return bindTo(0);
2060
+ }
2061
+ async function connect(projectDir, deps) {
2062
+ const session = await resolveSession(projectDir, deps);
2063
+ return { ...bindTo(session.port), session };
2064
+ }
2065
+ var lazySession = createLazySession(() => connect());
2066
+ var editor = lazyChainProxy(() => lazySession.ensure().then((s) => s.editor));
2067
+ var tools = lazyChainProxy(() => lazySession.ensure().then((s) => s.tools));
2068
+ export {
2069
+ LiveEditor,
2070
+ LiveEditorDocument,
2071
+ LiveTools,
2072
+ connect,
2073
+ editor,
2074
+ findProjectRootFrom,
2075
+ inferAssetKind,
2076
+ resolveSession,
2077
+ tools,
2078
+ unconnectedBindings
2079
+ };
2080
+ //# sourceMappingURL=index.js.map