@volter/live 0.5.210 → 0.5.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/BUNDLED_NOTICES +13 -13
  2. package/README.md +7 -11
  3. package/dist/chunk-7SZ3C4CC.js +768 -0
  4. package/dist/chunk-7SZ3C4CC.js.map +7 -0
  5. package/dist/chunk-7WKVJGHZ.js +15 -0
  6. package/dist/chunk-7WKVJGHZ.js.map +7 -0
  7. package/dist/chunk-SDIWRX5X.js +189 -0
  8. package/dist/chunk-SDIWRX5X.js.map +7 -0
  9. package/dist/chunk-USXOIBVX.js +81 -0
  10. package/dist/chunk-USXOIBVX.js.map +7 -0
  11. package/dist/chunk-Y5W77HYT.js +123 -0
  12. package/dist/chunk-Y5W77HYT.js.map +7 -0
  13. package/dist/chunk-YPAOTPIF.js +202 -0
  14. package/dist/chunk-YPAOTPIF.js.map +7 -0
  15. package/dist/client.d.ts +370 -0
  16. package/dist/client.js +9 -0
  17. package/dist/client.js.map +7 -0
  18. package/dist/discovery.d.ts +32 -0
  19. package/dist/discovery.js +19 -0
  20. package/dist/discovery.js.map +7 -0
  21. package/dist/editor-document.d.ts +12 -47
  22. package/dist/editor.d.ts +34 -181
  23. package/dist/http-transport.d.ts +6 -0
  24. package/dist/index.d.ts +1 -5
  25. package/dist/index.js +76 -1722
  26. package/dist/index.js.map +4 -4
  27. package/dist/installed-package.d.ts +6 -0
  28. package/dist/installed-package.js +7 -0
  29. package/dist/installed-package.js.map +7 -0
  30. package/dist/lazy-proxy.d.ts +11 -11
  31. package/dist/member-hints.d.ts +6 -9
  32. package/dist/product-create.d.ts +29 -0
  33. package/dist/product-create.js +13 -0
  34. package/dist/product-create.js.map +7 -0
  35. package/dist/product-locator.d.ts +100 -0
  36. package/dist/product-locator.js +25 -0
  37. package/dist/product-locator.js.map +7 -0
  38. package/dist/registry-format.d.ts +44 -0
  39. package/dist/registry-format.js +21 -0
  40. package/dist/registry-format.js.map +7 -0
  41. package/dist/session.d.ts +10 -3
  42. package/dist/singleton.d.ts +1 -13
  43. package/dist/volter-manifest.d.ts +55 -0
  44. package/dist/volter-manifest.js +9 -0
  45. package/dist/volter-manifest.js.map +7 -0
  46. package/dist/workbench-locator.d.ts +84 -0
  47. package/dist/workbench-locator.js +416 -0
  48. package/dist/workbench-locator.js.map +7 -0
  49. package/package.json +35 -3
  50. package/src/client.ts +1052 -0
  51. package/src/discovery.ts +105 -0
  52. package/src/editor-document.ts +12 -49
  53. package/src/editor.ts +45 -212
  54. package/src/http-transport.ts +19 -0
  55. package/src/index.ts +3 -7
  56. package/src/installed-package.ts +15 -0
  57. package/src/lazy-proxy.ts +11 -11
  58. package/src/member-hints.ts +8 -12
  59. package/src/product-create.ts +38 -0
  60. package/src/product-locator.ts +327 -0
  61. package/src/registry-format.ts +185 -0
  62. package/src/session.ts +38 -66
  63. package/src/singleton.ts +1 -16
  64. package/src/volter-manifest.ts +97 -0
  65. package/src/workbench-locator.ts +625 -0
  66. package/dist/tools.d.ts +0 -23
  67. package/src/tools.ts +0 -45
package/dist/index.js CHANGED
@@ -1,1158 +1,19 @@
1
- // ../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
- // ../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 (the editor's `restart` command) 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
- fetchOverride;
80
- /**
81
- * The running game's debug plane — see {@link GameDebugDoor}. It rides the
82
- * SAME `/__editor/command` relay every other method here uses (relay cases
83
- * `inspect-gameplay-state` / `invoke-debug-command`, `command-listener.ts`),
84
- * so a tool contribution reaches the game through the client it already has
85
- * rather than a second channel of its own.
86
- */
87
- game;
88
- /**
89
- * Called with the raw body of EVERY response this client receives — command
90
- * envelopes and `/__editor/state` alike, on success AND on refusal.
91
- *
92
- * It exists for exactly one contract: the server stamps `unresolvedConsole`
93
- * onto every envelope (`server-utils.ts`'s `commandResponseFor`), and the CLI
94
- * has to see those counts to be loud about them. Routing that through a
95
- * single observer here — rather than teaching each of the CLI's output sites
96
- * to unpack a response — is what keeps the loudness contract ONE mechanism.
97
- * The observer must not throw; anything it raises is swallowed, because a
98
- * reporting hook may never break the command it is reporting on.
99
- */
100
- transport;
101
- onEnvelope;
102
- constructor(opts) {
103
- if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
104
- throw new TypeError(
105
- 'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").'
106
- );
107
- }
108
- const unknownOptions = Object.keys(opts ?? {}).filter(
109
- (key) => key !== "url" && key !== "onEnvelope" && key !== "transport" && key !== "fetch"
110
- );
111
- if (unknownOptions.length > 0) {
112
- throw new Error(
113
- `EditorClient: unknown option${unknownOptions.length === 1 ? "" : "s"} ${unknownOptions.map((key) => `"${key}"`).join(", ")}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`
114
- );
115
- }
116
- this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, "");
117
- this.fetchOverride = opts?.fetch;
118
- this.onEnvelope = opts?.onEnvelope ?? null;
119
- this.transport = opts?.transport ?? null;
120
- this.game = {
121
- state: async (name) => {
122
- const data = await this.command({
123
- type: "inspect-gameplay-state",
124
- keys: [name]
125
- });
126
- return data.state?.[name];
127
- },
128
- command: async (name, ...args) => (await this.command({ type: "invoke-debug-command", name, args })).result
129
- };
130
- }
131
- /**
132
- * `retryTransport` opts a command into ONE automatic retry after a transport
133
- * failure (see {@link RETRYABLE_TRANSPORT_CODES}). It is deliberately
134
- * OPT-IN and off by default: a socket that died after the request was written
135
- * cannot prove the editor did not already run the command, so a blanket retry
136
- * would risk playing/stopping/writing twice. Read-only relays — the captures —
137
- * have no such hazard and turn it on.
138
- */
139
- async command(body, options) {
140
- const type = String(body["type"] ?? "command");
141
- const deadlineMs = options?.deadlineMs ?? COMMAND_DEADLINE_MS;
142
- if (this.transport) {
143
- const answered = await this.transport(body);
144
- if (!answered.ok) {
145
- throw new EditorCommandError(
146
- answered.error ?? `Editor command "${type}" failed`,
147
- answered.code
148
- );
149
- }
150
- return answered;
151
- }
152
- const url = `${this.baseUrl}/__editor/command`;
153
- const request = JSON.stringify(body);
154
- let res;
155
- let retried = false;
156
- let commandDispatcher;
157
- for (; ; ) {
158
- try {
159
- const init = {
160
- method: "POST",
161
- headers: { "Content-Type": "application/json" },
162
- body: request,
163
- signal: AbortSignal.timeout(deadlineMs)
164
- };
165
- commandDispatcher = deadlineMs > UNDICI_DEFAULT_HEADERS_TIMEOUT_MS ? createDispatcher(deadlineMs + 3e4) : void 0;
166
- res = this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, commandDispatcher);
167
- break;
168
- } catch (error) {
169
- await commandDispatcher?.destroy?.();
170
- commandDispatcher = void 0;
171
- if (error?.name === "TimeoutError") {
172
- throw new EditorCommandError(
173
- `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 that started this editor session.`,
174
- void 0,
175
- true
176
- );
177
- }
178
- const { code, detail } = describeFetchFailure(error);
179
- if (options?.retryTransport === true && !retried && RETRYABLE_TRANSPORT_CODES.has(code)) {
180
- retried = true;
181
- await new Promise((resolve3) => setTimeout(resolve3, TRANSPORT_RETRY_DELAY_MS));
182
- continue;
183
- }
184
- throw new EditorCommandError(
185
- `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 that started this editor session and confirm the port this client resolved.",
186
- code,
187
- false
188
- );
189
- }
190
- }
191
- let data;
192
- try {
193
- data = await this.readJson(res);
194
- } finally {
195
- await commandDispatcher?.destroy?.();
196
- }
197
- if (!data.ok) {
198
- throw new EditorCommandError(
199
- data.error ?? `Editor command failed: ${res.status}`,
200
- data.code,
201
- // 504 is every `timedOut` result (`commandResponseFor`) — the relay
202
- // gave up, on any of its five grounds. A 200 body with `ok: false` is
203
- // an ANSWER from the editor, however unwelcome. See `timedOut` above.
204
- res.status === 504
205
- );
206
- }
207
- return data;
208
- }
209
- // --- Play control ---
210
- /** `opts.seed` (D15/T-D15.6, objection-4 fix) — the editor's `play --seed <n>` command's
211
- * explicit config leg, relayed as `cmd['seed']`; `handleCommand`'s
212
- * `'play'` case threads it into `enterPlayMode`'s highest-precedence seed
213
- * argument (beats manifest.determinism.defaultSeed/?volter-seed=). Omitted,
214
- * boot seeding falls back to that precedence unchanged.
215
- *
216
- * `opts.name` (the editor's `play --name <text>` command) — an OPTIONAL label for this run,
217
- * relayed as `cmd['name']` and slugified server-side into the run's
218
- * `logs/play-*.jsonl` filename and its session-journal line. Findability
219
- * only: no registry, no uniqueness, no lookup verb — grep and `ls` are the
220
- * query engine. Omitted, the filename keeps its exact unnamed shape. */
221
- /* `opts.record` (the editor's `play --record <name>` command) — NAMES this run's recording
222
- * file. It does not ENABLE recording: every relayed play records, with no
223
- * flag (see `@volter/editor-game`'s `src/play/play-recording.ts`). Omitted, the clip is named for
224
- * the durable Gameplay Session; named, it becomes an explicit keepsake in
225
- * `.volter/recordings/<name>.webm`. */
226
- async play(opts) {
227
- return this.command({
228
- type: "play",
229
- ...opts?.seed !== void 0 ? { seed: opts.seed } : {},
230
- ...opts?.name ? { name: opts.name } : {},
231
- ...opts?.record ? { record: opts.record } : {}
232
- });
233
- }
234
- /** Dispose the current play session and mount it again from fresh project entry source. */
235
- async restart() {
236
- await this.command({ type: "play" });
237
- }
238
- /** Refuse shutdown if a document cannot be saved. Does not stop the server. */
239
- async prepareClose() {
240
- await this.command({ type: "session-prepare-close" });
241
- }
242
- /** Stops play, and finalizes this run's recording before the surface it was
243
- * photographing is torn down. The capture is absent when nothing recorded. */
244
- async stop() {
245
- return this.command({ type: "stop" });
246
- }
247
- async pause() {
248
- await this.command({ type: "pause" });
249
- }
250
- async resume() {
251
- await this.command({ type: "resume" });
252
- }
253
- async step() {
254
- await this.command({ type: "step" });
255
- }
256
- // --- Selection ---
257
- async select(id) {
258
- await this.command({ type: "select", id });
259
- }
260
- async selectMultiple(ids) {
261
- await this.command({ type: "select-multiple", ids });
262
- }
263
- async selectAll() {
264
- await this.command({ type: "select-all" });
265
- }
266
- // --- Viewport ---
267
- async focusEntity(id) {
268
- await this.command({ type: "focus-entity", id });
269
- }
270
- async focusSelection() {
271
- await this.command({ type: "focus-selection" });
272
- }
273
- /**
274
- * Frame the EDIT viewport camera on one entity — the strict sibling of
275
- * {@link focusEntity}. Same framing; an id the scene does not know is a
276
- * refusal naming the id (`EditorCommandError`, code `ENTITY_NOT_FOUND`)
277
- * rather than `focusEntity`'s silent no-op, so a caller that frames an
278
- * entity before capturing it cannot photograph the wrong thing.
279
- */
280
- async frameEntity(id) {
281
- await this.command({ type: "frame-entity", id });
282
- }
283
- async viewPreset(preset) {
284
- await this.command({ type: "view-preset", preset });
285
- }
286
- /**
287
- * LOOK AROUND THE OPEN MODEL, visibly. Swings the active Object3D
288
- * document's camera — the one on the human's screen — by `azimuth`/
289
- * `elevation` radians, animated over `duration` seconds, and resolves when
290
- * the move ends. A human drag during the move cancels it where it stands
291
- * (`cancelledBy: 'human'`); the promise still resolves.
292
- */
293
- async orbitDocument(options) {
294
- return this.command({ type: "document-orbit", ...options });
295
- }
296
- /** What a frame of a 3D document's stage costs, uncapped (`document-frame-cost`). */
297
- async frameCostDocument(options) {
298
- return this.command({ type: "document-frame-cost", ...options });
299
- }
300
- /** A slow full revolution around the open document's subject, at a constant rate. */
301
- async turntableDocument(options) {
302
- return this.command({ type: "document-turntable", ...options });
303
- }
304
- /**
305
- * Frame the open document's subject (its selection if it has one). `fit`
306
- * scales the fitted distance: 1 is the toolbar Frame button's tight fit.
307
- */
308
- async frameDocument(fit) {
309
- return this.command({
310
- type: "document-frame",
311
- ...fit === void 0 ? {} : { fit }
312
- });
313
- }
314
- async setCamera(position, target, fov) {
315
- await this.command({
316
- type: "set-camera",
317
- position,
318
- target,
319
- ...fov === void 0 ? {} : { fov }
320
- });
321
- }
322
- async captureViewport(size) {
323
- const data = await this.command({
324
- type: "capture-viewport",
325
- ...size === void 0 ? {} : { size }
326
- });
327
- return { base64: data.base64, mimeType: data.mimeType };
328
- }
329
- /**
330
- * Unit 4 (live-front-door wave) — capture the RUNNING GAME (the editor's
331
- * `screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
332
- * `@volter/game-live`'s `RelayTransport.screenshot` (and therefore
333
- * `game.screenshot()` on the relay path) already sends, so all three
334
- * surfaces composite the identical full game stack — canvas(es) plus the
335
- * HUD/react DOM layers — rather than any of them inventing a second,
336
- * subtly-different capture path. Contrast {@link captureViewport}, which
337
- * captures the EDITOR viewport's canvas and would silently hand back an
338
- * editor-only (HUD-less, possibly not-even-playing) image.
339
- *
340
- * Rejects — loudly, via `command`'s own `{ok:false}` unwrap — when play
341
- * mode isn't running ("not in play mode — start play before using the
342
- * debug seam") or no game canvas is mounted yet. Never returns a blank or
343
- * editor-only frame as a stand-in.
344
- *
345
- * `opts.refreshStarvedFrame` is the loop-starvation leg: without recent rAF
346
- * progress the canvas holds a provably stale frame and the relay
347
- * refuses it with `BRIDGE_SCREENSHOT_STALE` rather than pass it off as
348
- * current. Setting this asks the relay to render exactly ONE deterministic
349
- * tick (`runTicks(1, {render:'last'})`) first — the same escape
350
- * `@volter/game-live`'s `RelayTransport.screenshot` has always used, which is why
351
- * the editor's `eval` command could recover these frames while the editor's `screenshot` command could not.
352
- * Off by default: a caller who does not ask must never be handed a frame
353
- * that only exists because the capture drove the game.
354
- */
355
- async captureGame(opts) {
356
- const data = await this.command({
357
- type: "bridge-screenshot",
358
- ...opts?.refreshStarvedFrame === true ? { refreshStarvedFrame: true } : {}
359
- });
360
- const layers = data.layers;
361
- const flatness = data.flatness;
362
- return {
363
- base64: data.base64,
364
- mimeType: data.mimeType,
365
- composite: data.composite === true,
366
- ...layers && Number.isInteger(layers.canvases) && Number.isInteger(layers.domOverlays) ? { layers } : {},
367
- // Pass the pixel-honesty fields through as the page reported them: the
368
- // warning sentence is written where the pixels are, so nothing here
369
- // re-derives (or softens) it.
370
- ...flatness && typeof flatness.dominantFraction === "number" ? { flatness } : {},
371
- ...data.loopRecoveryFrame === true ? { loopRecoveryFrame: true } : {},
372
- // Same pass-through rule: the recorded-run notice is written where the
373
- // pixels are, so nothing here re-derives or softens it.
374
- ...data.recording && typeof data.recording.notice === "string" ? { recording: data.recording } : {}
375
- };
376
- }
377
- /** Start recording the same clean running-game composite `captureGame`
378
- * photographs. Recording state lives in the editor page, so another process
379
- * may stop it later through the same project session. */
380
- async startGameplayRecording(options = {}) {
381
- return this.command({
382
- type: "bridge-recording-start",
383
- ...options.fps !== void 0 ? { fps: options.fps } : {},
384
- ...options.name !== void 0 ? { name: options.name } : {},
385
- ...options.format !== void 0 ? { format: options.format } : {}
386
- });
387
- }
388
- /** Export a paused run as fixed-step video. Advances game state; maximum
389
- * five minutes. `audio` describes the muxed track, or is `false` when the
390
- * world implements no `AudioAdapter.renderOffline` and the file is
391
- * genuinely silent — read it, never assume either. */
392
- async exportGameplayVideo(options) {
393
- return this.command(
394
- { type: "bridge-recording-export", ...options },
395
- { deadlineMs: 61e4, retryTransport: false }
396
- );
397
- }
398
- /** Stop the page-owned recorder and return its WebM path and metadata. */
399
- async stopGameplayRecording() {
400
- return this.command({ type: "bridge-recording-stop" });
401
- }
402
- /** Read the active capture's monotonic media position. This is the only clock
403
- * suitable for selecting intervals inside the finalized recording. */
404
- async getGameplayRecordingTimeline() {
405
- const timeline = await this.command({
406
- type: "bridge-recording-timeline"
407
- });
408
- return { startedAt: timeline.startedAt, elapsedMs: timeline.elapsedMs };
409
- }
410
- /** Encode a recorded canvas/DOM interval into a normal composite WebM. */
411
- async exportGameplayReplay(options) {
412
- return this.command(
413
- { type: "bridge-recording-replay-export", ...options },
414
- { deadlineMs: 61e4, retryTransport: false }
415
- );
416
- }
417
- async captureGameplayReplay(replayPath, positionMs) {
418
- return this.command({
419
- type: "bridge-recording-replay-capture",
420
- replayPath,
421
- positionMs
422
- });
423
- }
424
- /**
425
- * Capture an isolated, deterministic four-view preview through the editor's
426
- * native Asset Lab. The SDK delegates rendering to the editor; it never
427
- * loads, clones, or interprets Three.js assets itself.
428
- */
429
- async captureAssetPreview(source, options = {}) {
430
- const data = await this.command(
431
- {
432
- type: "capture-asset-preview",
433
- ...source,
434
- ...options
435
- },
436
- // Photographing changes nothing, and this is the relay the editor's `screenshot
437
- // <module>` command and `project.bake.preview` ride — the lane where a momentary
438
- // transport failure cost a cold agent three probe modules.
439
- { retryTransport: true }
440
- );
441
- return {
442
- width: data.width,
443
- height: data.height,
444
- // Absent from editors that predate orientation reporting.
445
- ...data.orientation ? { orientation: data.orientation } : {},
446
- views: data.views,
447
- contactSheet: data.contactSheet
448
- };
449
- }
450
- /**
451
- * A project-defined labeled shot set (the editor's `screenshot <target> --shots <set>` command):
452
- * the DEFINITION travels with the command (project data — see
453
- * `AssetPreviewShotSetDefinition`; the CLI resolves it from the registered
454
- * `project.<set>.previewShots` tool), and the editor's generic
455
- * capture engine renders it — see `packages/editor-threejs/src/kit/asset-preview.ts`'s
456
- * `captureShotSetAssetPreview`. Throws (via `command`'s `{ok:false}`
457
- * unwrap) with a clear message naming the missing joint(s) when the asset
458
- * lacks a bone the definition requires.
459
- */
460
- async captureShotSetPreview(source, definition, options = {}) {
461
- const data = await this.command(
462
- {
463
- type: "capture-asset-preview",
464
- ...source,
465
- ...options,
466
- shotSet: definition
467
- },
468
- { retryTransport: true }
469
- );
470
- return {
471
- width: data.width,
472
- height: data.height,
473
- shots: data.shots,
474
- // An older editor predates the empty-frame guard and sends none.
475
- warnings: data.warnings ?? [],
476
- contactSheet: data.contactSheet
477
- };
478
- }
479
- /**
480
- * B8.4 — score the asset against a reference GLB (the editor's `screenshot
481
- * <model.glb> --compare <ref.glb>` command): matched orthographic front + side silhouettes
482
- * (equal-height bounding-box framing, both yaw-normalized to face the
483
- * camera), per-view IoU numbers, and overlay evidence images. The
484
- * reference GLB's raw bytes travel base64 in the command; the editor
485
- * renders and scores — the SDK never interprets Three.js assets itself.
486
- */
487
- async captureAssetComparePreview(source, refGlbBase64, options = {}) {
488
- const { refForward, ...dimensions } = options;
489
- const data = await this.command({
490
- type: "capture-asset-preview",
491
- ...source,
492
- ...dimensions,
493
- compare: { glbBase64: refGlbBase64, ...refForward ? { forward: refForward } : {} }
494
- });
495
- return { width: data.width, height: data.height, views: data.views };
496
- }
497
- /**
498
- * The STORY lane (the editor's `screenshot <file>.stories.tsx` command): every CSF export of
499
- * one project story file rendered in the live session's DOM and captured
500
- * through the same composite leg {@link captureGame} uses, returned as
501
- * per-export images plus one variant sheet. `options.story` narrows to a
502
- * single export.
503
- *
504
- * Rendering happens in the EDITOR — the SDK never imports, composes or
505
- * mounts a CSF module itself; the session already owns that machinery for
506
- * its Stories panel and this drives it.
507
- */
508
- async captureStoryVariants(modulePath, options = {}) {
509
- const data = await this.command({
510
- type: "capture-story-variants",
511
- modulePath,
512
- ...options
513
- });
514
- return {
515
- modulePath: data.modulePath,
516
- width: data.width,
517
- height: data.height,
518
- variants: data.variants,
519
- contactSheet: data.contactSheet
520
- };
521
- }
522
- // --- Panels ---
523
- async showViewport(tab) {
524
- await this.command({ type: "viewport-tab", tab });
525
- }
526
- /** Focus a static workspace panel by the key the editor's panel registry
527
- * holds; an unknown key refuses naming the keys it does hold. */
528
- async showPanel(panel) {
529
- await this.command({ type: "show-panel", panel });
530
- }
531
- /** Show several instances of the running game split-screen — multiplayer
532
- * authoring. Pass a total `count` (default "Player N" labels) or an array of
533
- * `names` (its length is the count; index 0 is the primary). Requires a live
534
- * play session. */
535
- async setInstanceCount(countOrNames) {
536
- await this.command(
537
- Array.isArray(countOrNames) ? { type: "set-instance-count", names: countOrNames } : { type: "set-instance-count", count: countOrNames }
538
- );
539
- }
540
- async openAsset(path, kind) {
541
- await this.command({ type: "open-asset-tab", path, kind });
542
- }
543
- /** SELECT a project asset — the other half of the browser's
544
- * selection-vs-open contract (single click selects and fills the
545
- * Inspector; double click opens a document). */
546
- async selectAsset(path) {
547
- await this.command({ type: "select-asset", path });
548
- }
549
- async closeAsset(key) {
550
- await this.command({ type: "close-asset-tab", key });
551
- }
552
- async toggleCommandPalette() {
553
- await this.command({ type: "toggle-command-palette" });
554
- }
555
- async toggleConsole() {
556
- await this.command({ type: "toggle-console" });
557
- }
558
- /** Switch the editor's NAMED WORKSPACE — the task-named layout memory
559
- * (`game`/`model`/`sculpt`/`texture`/`animate`/`look`). Resolves once the
560
- * dock has finished rebuilding, so a following capture photographs the
561
- * arrangement that was asked for. */
562
- async setWorkspace(workspace) {
563
- await this.command({ type: "set-workspace", workspace });
564
- }
565
- /** Apply a STYLE BUNDLE — palette, material, icon set and region defaults
566
- * in one gesture (`classic`/`glass`/`maya`/`substance`, or one a package
567
- * the project or product composes carries, such as `blender`). */
568
- async setStyle(style) {
569
- await this.command({ type: "set-style", style });
570
- }
571
- /** A document stage's viewport PRESENTATION (`kit/viewport-presentation`), resolved; with a
572
- * `layer`, that choice is recorded for the view first, as the toolbar records it. */
573
- /** A view's presentation, answered resolved. With a LAYER the person's choice is recorded
574
- * first; with a STRING, the named view (`*.view.ts`) of that id is put on the view whole. */
575
- async viewportPresentation(documentId, layer) {
576
- return this.command({
577
- type: "viewport-presentation",
578
- documentId,
579
- ...typeof layer === "string" ? { preset: layer } : layer ? { layer } : {}
580
- });
581
- }
582
- /** Set the MATERIAL apart from the bundle that usually carries it.
583
- * Answers with what the chrome wears afterwards. */
584
- async setAppearance(appearance) {
585
- return this.command({
586
- type: "set-appearance",
587
- ...appearance
588
- });
589
- }
590
- async showBuild() {
591
- await this.command({ type: "show-build" });
592
- }
593
- /** Atomically present a durable editor view and return its shareable URL. */
594
- async present(view) {
595
- const presented = await this.command({ type: "present-view", view });
596
- return { view: presented.view, url: presented.url, warnings: presented.warnings };
597
- }
598
- /**
599
- * The INSPECTION SUBJECT the editor is showing right now, as data — the
600
- * serialized projection of the inspection model (design:
601
- * `docs/ARCHITECTURE-CORE.md` §Editor chrome, "The Inspection Model").
602
- *
603
- * The same subject a human reads in the inspector: identity, presentation,
604
- * verbs, and the identified sections in display order — with a `fields`
605
- * section's CURRENT VALUES read through the same io the field rows edit
606
- * through. With nothing selected it answers the active surface's own
607
- * no-selection subject when it has one, exactly as the panel does; it never
608
- * reports another surface's, and when the panel itself is unmounted it
609
- * answers `{none: true}` rather than a subject nobody is looking at. A
610
- * `custom` section body is a named opaque (`{kind, id, title}`) — the editor
611
- * renders those with React — plus its displayed values under `data` when it
612
- * has any (the Transform section's position/rotation/scale).
613
- */
614
- async inspect() {
615
- const data = await this.command({ type: "inspect" });
616
- return data.subject;
617
- }
618
- /** Run one verb exposed by the active Inspector subject, by its id. */
619
- async runInspectionAction(actionId) {
620
- const data = await this.command({
621
- type: "run-inspection-action",
622
- actionId
623
- });
624
- return data.subject;
625
- }
626
- /**
627
- * Run ONE command by id — the door to everything the command palette lists.
628
- *
629
- * Under the Code-OSS frame this is the workbench's own `ICommandService`, so
630
- * any command id works: a view's `volter.<view>.<verb>`, an editor action's
631
- * `volter.action.<id>`, or one of VS Code's own. Standalone the editor's `edit` command has no
632
- * command service and answers the `volter.<view>.<verb>` shape directly off
633
- * the views registry, refusing anything else BY NAME.
634
- *
635
- * The result is whatever the command answered — a view verb's state, or
636
- * `null` for a command that returns nothing.
637
- */
638
- async runCommand(commandId, args) {
639
- const data = await this.command({
640
- type: "run-command",
641
- commandId,
642
- ...args === void 0 ? {} : { args }
643
- });
644
- return data.result;
645
- }
646
- /**
647
- * One STRUCTURE op on the authored tree — the hierarchy context menu's own
648
- * verbs, on the same helpers, for a caller with no pointer to right-click
649
- * with. `id`/`ids` default to the current selection.
650
- */
651
- async structureOp(op, options = {}) {
652
- return this.command({ type: "structure-op", op, ...options });
653
- }
654
- /** "Extract Component…" — the hierarchy row's action, as a command. Answers
655
- * the action's own sentence, which NAMES the files it created. */
656
- async extractComponent(options = {}) {
657
- return this.command({ type: "extract-component", ...options });
658
- }
659
- /** "Fork Component…" — extract's twin: one new file, one callsite retargeted. */
660
- async forkComponent(options = {}) {
661
- return this.command({ type: "fork-component", ...options });
662
- }
663
- /**
664
- * The HIERARCHY PANEL's actual rendered row tree, as data.
665
- *
666
- * The same rows a human is looking at: the adapter's tree after the component
667
- * marks fold implementation subtrees, after the internals reveal, after the
668
- * document promotion, the child cap, the collapse state, the search filter
669
- * and the selection scope. Works in play mode and edit mode alike — the
670
- * answer reports which (`playState`, `activeViewportTab`), because a
671
- * play-mode tree and an edit-mode tree come from different adapters.
672
- *
673
- * Deliberately NOT `status().entities`, which walks the raw adapter tree and
674
- * therefore answers a different question: a panel defect is invisible in it.
675
- *
676
- * Each row carries `childCount` (what its caret opens), `internalChildCount`
677
- * (what is folded behind "Reveal Internals") and `expandable` (whether the
678
- * panel draws a caret at all) — so "this subtree exists but the UI offers no
679
- * way to open it" is a readable fact rather than something only a human
680
- * squinting at the panel can notice.
681
- *
682
- * Rejects, naming the panel, when no hierarchy panel is mounted: an empty
683
- * tree would be a fabricated answer about a surface nobody is being shown.
684
- */
685
- async hierarchy() {
686
- const data = await this.command({ type: "hierarchy" });
687
- return data.hierarchy;
688
- }
689
- /** Run the Hierarchy panel's own Expand All action. */
690
- async expandHierarchyAll() {
691
- await this.command({ type: "expand-hierarchy-all" });
692
- }
693
- /** Run the Hierarchy panel's own Collapse All action — Expand All's other
694
- * half, and the only way back to the tree's rest state through the product
695
- * (see `HierarchyPanelSnapshot.collapseAll`). */
696
- async collapseHierarchyAll() {
697
- await this.command({ type: "collapse-hierarchy-all" });
698
- }
699
- /**
700
- * Write one editable path through the active Inspector's own IO.
701
- *
702
- * The answer carries `write` as well as the subject, because an ack alone
703
- * cannot be believed: a write with no persistence route open succeeds and
704
- * changes no byte, and `write.persisted` is how the caller tells the two
705
- * apart without diffing the tree (`InspectedWriteDestination` in `types.ts`).
706
- */
707
- async setInspectionField(path, value) {
708
- const data = await this.command({
709
- type: "set-inspection-field",
710
- path,
711
- value
712
- });
713
- return { subject: data.subject, write: data.write };
714
- }
715
- /**
716
- * REMOVE one editable path's authored override — the other half of the write
717
- * door, and the only one that can express byte-ABSENCE.
718
- *
719
- * {@link setInspectionField} writes a VALUE, so reverting a property an
720
- * authoring gesture ADDED puts the default back EXPLICITLY and leaves the
721
- * source one attribute heavier than it started. This drops the property, so
722
- * whatever governs it in its absence takes over — the same `io.remove` the
723
- * Inspector's revert arrow calls, the same persistence pipe, the same awaited
724
- * `{ destination, persisted }` ack.
725
- *
726
- * Rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field does not declare
727
- * itself removable or the lane implements no removal door. That refusal is a
728
- * MISSING SEAM, not a failed removal, and it is coded rather than phrased
729
- * precisely so a caller can grade the two differently.
730
- */
731
- async removeInspectionField(path) {
732
- const data = await this.command({
733
- type: "remove-inspection-field",
734
- path
735
- });
736
- return { subject: data.subject, write: data.write };
737
- }
738
- /**
739
- * OPEN one piece of the adapter's SCENE TABLE by id — a scene, a prefab, or
740
- * a story state, because the table makes them siblings (they differ only in
741
- * instance site). The ids are exactly what `getState().adapter.scenes.entries`
742
- * reports, so the table is both the menu and the address space.
743
- *
744
- * With a game LIVE in the session, opening a scene the adapter declares
745
- * reachable through that game's own scenes contract NAVIGATES it — the same
746
- * switch the editor's own scene picker makes — and the answer carries the
747
- * game's own reading (`scene`).
748
- *
749
- * Rejects with a coded reason rather than prose: `SCENE_NOT_FOUND` (and it
750
- * names the ids that DO exist), `SCENE_NOT_OPENABLE` carrying the adapter's
751
- * own declared reason for a scene it says nothing can reach,
752
- * `SCENE_NAVIGATION_NOT_RUNNING` for a live-only scene with no game running,
753
- * `SCENE_CONTRACT_UNAVAILABLE` / `SCENE_NOT_IN_CONTRACT` (naming the ids the
754
- * game itself publishes) / `SCENE_SWITCH_FAILED` when the running game's own
755
- * navigation cannot take it, `SCENE_NOT_OPENABLE_LIVE` when this session has
756
- * no remount for a native swap-slot scene,
757
- * `SCENE_TABLE_UNAVAILABLE` before the adapter has loaded, and
758
- * `SCENE_DOCUMENT_NOT_MOUNTED` when the host has no document for a piece the
759
- * table says is openable — a host gap, not a table statement.
760
- */
761
- async open(id) {
762
- return this.command({ type: "open", id });
763
- }
764
- /**
765
- * Undo / redo one project transaction — the same queue the keyboard shortcut
766
- * drives. `moved` is false when there was nothing left in that direction.
767
- */
768
- async undo() {
769
- return this.command({ type: "undo" });
770
- }
771
- async redo() {
772
- return this.command({ type: "redo" });
773
- }
774
- /** Read the editor's actual current durable projection. */
775
- async currentView() {
776
- const data = await this.command({ type: "current-view" });
777
- return data.view;
778
- }
779
- /**
780
- * Capture the active center document exactly as presented to the user.
781
- *
782
- * A number is a SQUARE of that size (the default shape); `{width, height}`
783
- * asks for a shaped frame — a video-aspect look that needs no crop. Both are
784
- * bounded by the relay budget; see {@link CaptureDimensions}.
785
- */
786
- /** Photograph the editor PAGE itself — every panel as the person sees it, at
787
- * `scale` output pixels per CSS pixel (default `devicePixelRatio`), which is
788
- * what a 1 px border or a glyph edge is judged through. */
789
- async captureEditorChrome(options) {
790
- return this.command({
791
- type: "capture-editor-chrome",
792
- ...options?.scale === void 0 ? {} : { scale: options.scale },
793
- ...options?.region === void 0 ? {} : { region: options.region },
794
- ...options?.name === void 0 ? {} : { name: options.name }
795
- });
796
- }
797
- /** With a view, present and capture it in one request so document discovery
798
- * cannot retarget the capture between two client calls. */
799
- async captureActiveDocument(size, view) {
800
- return this.command({
801
- type: "capture-active-document",
802
- ...view ? { view } : {},
803
- ...typeof size === "number" ? { size } : {},
804
- ...typeof size === "object" && size !== null ? { width: size.width, height: size.height } : {}
805
- });
806
- }
807
- /**
808
- * Read or drive the ACTIVE center document's own DOM — the scoped
809
- * editor-chrome door, and the read/gesture half of the same subject
810
- * {@link captureActiveDocument} photographs. NOT play-mode gated, and NOT
811
- * page automation: a target outside the active document's container is
812
- * refused by name. Design and scope contract:
813
- * `packages/sdk/src/kit/editor-document-probe.ts`.
814
- */
815
- async documentProbe(step) {
816
- return this.command({ type: "document-probe", step });
817
- }
818
- /**
819
- * Run a wire-carried step against the ACTIVE document's published context
820
- * (`packages/sdk/src/kit/document-context-registry.ts`) — the REPL door over
821
- * an open document, in Edit mode. `src` is the step's own `toString()`;
822
- * same serialization contract as `page-script` (no closures survive).
823
- */
824
- /**
825
- * The Blender lane's doors (`blender-execute`, `blender-scene-info`,
826
- * `blender-object-info`, `blender-screenshot-view`, `blender-read-file`,
827
- * `blender-write-file`, `blender-list-files`, `blender-start`,
828
- * `blender-status`): Blender runs in the editor tab's worker, and
829
- * `volter blender-mcp` is transport onto these. `blender-status` is the only
830
- * one that creates nothing — it answers whether this tab already has a
831
- * session, which is how a caller survives an editor restart.
832
- */
833
- async blender(type, fields = {}) {
834
- return this.command({ type, ...fields }, { deadlineMs: BLENDER_DEADLINE_MS });
835
- }
836
- /**
837
- * Model Play's log (`model-play-log`, contributed by `@volter/play`): what the
838
- * running play script logged with `play.log`, and the runner's lifecycle entries, stamped
839
- * with simulation time and frame. `documentId` names the model document (the active Play's
840
- * when omitted); `since` keeps entries at or after that many simulation seconds; `kind`
841
- * keeps one kind.
842
- */
843
- async modelPlayLog(query = {}) {
844
- const { ok: _ok, ...reading } = await this.command({ type: "model-play-log", ...query });
845
- return reading;
846
- }
847
- async documentScript(src) {
848
- const outcome = await this.command({ type: "document-script", src });
849
- return outcome.result;
850
- }
851
- // --- Display (set semantics) ---
852
- async setGrid(enabled) {
853
- await this.command({ type: "set-grid", enabled });
854
- }
855
- async setHelpers(enabled) {
856
- await this.command({ type: "set-helpers", enabled });
857
- }
858
- async setStats(enabled) {
859
- await this.command({ type: "set-stats", enabled });
860
- }
861
- async setShadingMode(mode) {
862
- await this.command({ type: "set-shading-mode", mode });
863
- }
864
- async setHelperType(helperType, enabled) {
865
- await this.command({ type: "set-helper-type", helperType, enabled });
866
- }
867
- // --- Transform tools (set semantics) ---
868
- async setTransformMode(mode) {
869
- await this.command({ type: "set-transform-mode", mode });
870
- }
871
- async setTransformSpace(space) {
872
- await this.command({ type: "set-transform-space", space });
873
- }
874
- async setSnap(enabled) {
875
- await this.command({ type: "set-snap", enabled });
876
- }
877
- // --- Project management ---
878
- async createProject(name, location, template = "default", exampleId) {
879
- const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
880
- method: "POST",
881
- headers: { "Content-Type": "application/json" },
882
- body: JSON.stringify({ name, location, template, ...exampleId ? { exampleId } : {} })
883
- });
884
- const data = await this.readJson(res);
885
- if (!res.ok) {
886
- throw new Error(data.error ?? `Create project failed: ${res.status}`);
887
- }
888
- return { path: data.path, config: data.config };
889
- }
890
- async openProject(path) {
891
- const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
892
- method: "POST",
893
- headers: { "Content-Type": "application/json" },
894
- body: JSON.stringify({ path })
895
- });
896
- const body = await this.readJson(res);
897
- if (!res.ok) {
898
- throw new Error(body.error ?? `Open project failed: ${res.status}`);
899
- }
900
- }
901
- async getProject() {
902
- const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
903
- const data = await this.readJson(res);
904
- if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
905
- return data.project;
906
- }
907
- async listRecentProjects() {
908
- const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
909
- const data = await this.readJson(res);
910
- if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
911
- return data.projects;
912
- }
913
- // --- Registered project tools ---
914
- /** List tools explicitly registered in `package.json#volter.tools`.
915
- * The editor server loads callable metadata in Node; modules never enter the
916
- * editor browser merely because they were listed. */
917
- async listProjectTools() {
918
- const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools`);
919
- const body = await this.readJson(res);
920
- if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
921
- return body;
922
- }
923
- /** Execute one Node-hosted project tool through the shared validated
924
- * dispatcher. Write/destructive tools require `confirm:true`. */
925
- async runProjectTool(name, input = {}, options = {}) {
926
- const dispatcher = createDispatcher(0);
927
- try {
928
- const res = await this.httpFetch(
929
- `${this.baseUrl}/__editor/project-tools/run`,
930
- {
931
- method: "POST",
932
- headers: { "Content-Type": "application/json" },
933
- body: JSON.stringify({
934
- name,
935
- input,
936
- confirm: options.confirm === true,
937
- // Omitted (not null) when unset — the wire body is JSON and the tool
938
- // host reads absence as "the sole instance", same convention as the
939
- // relay's `instance`.
940
- ...options.instance !== void 0 ? { instance: options.instance } : {}
941
- })
942
- },
943
- dispatcher
944
- );
945
- const body = await this.readJson(res);
946
- if (!body || typeof body !== "object" || typeof body.ok !== "boolean") {
947
- throw new Error(`Project tool returned an invalid response (${res.status}).`);
948
- }
949
- return body;
950
- } finally {
951
- await dispatcher?.destroy?.();
952
- }
953
- }
954
- // --- First-party generation job activity ---
955
- /** Read the one project-local generation job ledger. Provider-native
956
- * request/result shapes remain on their registered operations. */
957
- async listGenerationJobs() {
958
- const res = await this.httpFetch(`${this.baseUrl}/__editor/generations`);
959
- const body = await this.readJson(res);
960
- if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
961
- return body;
962
- }
963
- /** Forget operational job state. Accepted provenance and project assets
964
- * are deliberately unaffected. */
965
- async forgetGenerationJob(id) {
966
- const res = await this.httpFetch(
967
- `${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`,
968
- {
969
- method: "DELETE"
970
- }
971
- );
972
- const body = await this.readJson(res);
973
- if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
974
- return body.removed === true;
975
- }
976
- // --- Logs ---
977
- async getLogEntries() {
978
- const res = await this.httpFetch(`${this.baseUrl}/__editor/log-entries`);
979
- const data = await this.readJson(res);
980
- if (!res.ok) return [];
981
- return data.entries;
982
- }
983
- // --- State ---
984
- /**
985
- * The document table the host resolved — every scene, prefab, page, model,
986
- * shot, take … the project's finders produced (`getState().adapter.scenes`
987
- * is the same projection). A command, so it answers wherever the control
988
- * channel reaches, not only where `/__editor/state` is served.
989
- */
990
- async documentTable() {
991
- return this.command({ type: "document-table" });
992
- }
993
- /**
994
- * Reload the editor's page and wait until the new document answers — the
995
- * host's `page-reload`, for a product with no game client (`@volter/game-live`'s
996
- * `page.reload()` is the same order with the same two witnesses). A tab's
997
- * epoch count rising proves a new document loaded; a host command answering
998
- * proves it can be driven. It is sent only once that tab reports its command
999
- * listener ready, because a command sent before is filed as a stall.
1000
- */
1001
- async reloadPage(timeoutMs = 6e4) {
1002
- const epochs = async () => {
1003
- const tabs = (await this.getState().catch(() => ({ tabs: [] }))).tabs ?? [];
1004
- const byTab = tabs;
1005
- return new Map(byTab.map((tab) => [tab.tabId8, tab]));
1006
- };
1007
- const pause = () => new Promise((settle) => setTimeout(settle, 250));
1008
- const before = await epochs();
1009
- const deadline = Date.now() + timeoutMs;
1010
- await this.command({ type: "page-reload" });
1011
- let reloaded = [];
1012
- while (reloaded.length === 0) {
1013
- if (Date.now() > deadline) throw new Error("The reload was ordered and no tab of this session reported a new page load.");
1014
- await pause();
1015
- const now = await epochs();
1016
- reloaded = [...now].filter(([tabId, tab]) => tab.epochCount > (before.get(tabId)?.epochCount ?? 0)).map(([tabId]) => tabId);
1017
- }
1018
- for (; ; ) {
1019
- if (Date.now() > deadline) throw new Error("A new page loaded, but its command listener did not become ready.");
1020
- const now = await epochs();
1021
- if (reloaded.some((tabId) => {
1022
- const listener = now.get(tabId)?.commandListener;
1023
- return typeof listener !== "string" || listener === "ready";
1024
- })) break;
1025
- await pause();
1026
- }
1027
- for (; ; ) {
1028
- try {
1029
- await this.command({ type: "current-view" });
1030
- return;
1031
- } catch (error) {
1032
- if (Date.now() > deadline) throw error;
1033
- await pause();
1034
- }
1035
- }
1036
- }
1037
- async getState() {
1038
- const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
1039
- const state = await this.readJson(res);
1040
- if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
1041
- return state;
1042
- }
1043
- /**
1044
- * The complete unresolved console set the session is holding right now.
1045
- *
1046
- * Command envelopes only carry COUNTS (`unresolvedConsole` on
1047
- * `commandResponseFor`). The named conditions live on GET `/__editor/console`.
1048
- * This is the method that turns "a command that exits before an envelope
1049
- * arrives" into a real reading: the CLI calls it at start and at exit
1050
- * through the same {@link onEnvelope} observer every other response uses.
1051
- * A session that does not answer within {@link CONSOLE_DRAIN_TIMEOUT_MS} is
1052
- * a thrown error the caller treats as "nothing learned", never a hang.
1053
- */
1054
- async getUnresolvedConsole(opts) {
1055
- const res = await this.httpFetch(
1056
- `${this.baseUrl}/__editor/console${opts?.all === true ? "?all=1" : ""}`,
1057
- { signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) }
1058
- );
1059
- if (!res.ok) {
1060
- throw new Error(`Failed to read unresolved console: ${res.status}`);
1061
- }
1062
- return await this.readJson(res, true);
1063
- }
1064
- /** Acknowledge one named console condition. The response is observed and
1065
- * hydrated through the same path as every other client response. */
1066
- async acknowledgeConsole(input) {
1067
- const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
1068
- method: "POST",
1069
- headers: { "Content-Type": "application/json" },
1070
- body: JSON.stringify(input),
1071
- signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS * 4)
1072
- });
1073
- const body = await this.readJson(res);
1074
- return body;
1075
- }
1076
- /**
1077
- * `fetch` for this client's plain routes, with the ONE thing Node's `fetch`
1078
- * will not do: name why it failed.
1079
- *
1080
- * `readJson` below already owns "the server answered the wrong thing"; this
1081
- * owns "nothing answered at all", which used to reach the caller as the bare
1082
- * `TypeError: fetch failed` with the real code buried on `.cause`. No retry
1083
- * here — these routes create projects, run tools and acknowledge console
1084
- * conditions, so repeating one is the caller's decision. The relayed
1085
- * `command` path above has its own opt-in retry for the read-only captures.
1086
- */
1087
- async httpFetch(url, init, dispatcher) {
1088
- try {
1089
- return this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, dispatcher);
1090
- } catch (error) {
1091
- if (error?.name === "TimeoutError") throw error;
1092
- const { code, detail } = describeFetchFailure(error);
1093
- throw new EditorCommandError(
1094
- `${init?.method ?? "GET"} ${url} never reached the editor: ${code}${detail ? ` (${detail})` : ""}. Nothing answered on that port \u2014 check the terminal that started this editor session and confirm the port this client resolved.`,
1095
- code,
1096
- false
1097
- );
1098
- }
1099
- }
1100
- /** Parse one JSON body and hand it to {@link onEnvelope}.
1101
- *
1102
- * A command/state envelope carries current counts but not the named set. If
1103
- * an observer is installed, do the bounded console GET before resolving the
1104
- * original request. That makes a subsequent `process.exit()` safe: the
1105
- * observer has already received every condition and occurrence count. */
1106
- async readJson(res, consoleComplete = false) {
1107
- const contentType = res.headers.get("content-type") ?? "";
1108
- if (!contentType.includes("application/json")) {
1109
- throw new Error(
1110
- `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 editor session.`
1111
- );
1112
- }
1113
- const body = await res.json();
1114
- this.observe(body, { unresolvedConsoleComplete: consoleComplete });
1115
- if (this.onEnvelope !== null && !consoleComplete && body !== null && typeof body === "object" && "unresolvedConsole" in body) {
1116
- try {
1117
- const consoleRes = await this.httpFetch(`${this.baseUrl}/__editor/console`, {
1118
- signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS)
1119
- });
1120
- if (consoleRes.ok) {
1121
- const consoleBody = await consoleRes.json();
1122
- this.observe(consoleBody, { unresolvedConsoleComplete: true });
1123
- }
1124
- } catch {
1125
- }
1126
- }
1127
- return body;
1128
- }
1129
- /** Hand one response body to {@link onEnvelope}, never letting it throw. */
1130
- observe(body, observation) {
1131
- if (this.onEnvelope === null) return;
1132
- try {
1133
- this.onEnvelope(body, observation);
1134
- } catch {
1135
- }
1136
- }
1137
- /**
1138
- * Whether an editor browser tab is connected to the server *right now*.
1139
- * Unlike {@link getState}, this reflects live SSE connections, not cached
1140
- * state — use it to check whether commands will actually reach an editor.
1141
- */
1142
- async isConnected() {
1143
- const state = await this.getState();
1144
- return state.connected === true;
1145
- }
1146
- async waitForState(predicate, timeoutMs = 1e4) {
1147
- const start = Date.now();
1148
- while (Date.now() - start < timeoutMs) {
1149
- const state = await this.getState();
1150
- if (predicate(state)) return state;
1151
- await new Promise((r) => setTimeout(r, 100));
1152
- }
1153
- throw new Error(`waitForState timed out after ${timeoutMs}ms`);
1154
- }
1155
- };
1
+ import {
2
+ EditorClient
3
+ } from "./chunk-7SZ3C4CC.js";
4
+ import {
5
+ EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
6
+ HttpSessionDiscovery,
7
+ withTimeout
8
+ } from "./chunk-USXOIBVX.js";
9
+ import {
10
+ servedProjectAnswer
11
+ } from "./chunk-Y5W77HYT.js";
12
+ import {
13
+ resolveProductForProject
14
+ } from "./chunk-SDIWRX5X.js";
15
+ import "./chunk-7WKVJGHZ.js";
16
+ import "./chunk-YPAOTPIF.js";
1156
17
 
1157
18
  // src/editor-document.ts
1158
19
  var LiveEditorDocument = class {
@@ -1220,9 +81,7 @@ var LiveEditorDocument = class {
1220
81
  }
1221
82
  /**
1222
83
  * A real pointer DRAG across one matched element — press at `from`, move,
1223
- * release at `to`. The gesture a direct-manipulation canvas needs; a
1224
- * zero-length drag is a click at that fraction, which `click` (always the
1225
- * center) cannot place.
84
+ * release at `to`. A zero-length drag presses and releases at one fraction.
1226
85
  *
1227
86
  * `from`, `to` and every point in `via` are `[x, y]` FRACTIONS OF THE
1228
87
  * MATCHED ELEMENT'S BOX, 0..1 from its top-left — NEVER pixels and never
@@ -1279,13 +138,10 @@ var LiveEditorDocument = class {
1279
138
  });
1280
139
  }
1281
140
  /**
1282
- * THE REPL over the open document: run `step` in the editor page against the
1283
- * object the ACTIVE document published as its context (the mesh document
1284
- * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
1285
- * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
1286
- * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
1287
- * `game.page`: the step's own source travels, so inline every value it
1288
- * needs and return plain data.
141
+ * Run `step` in the editor page against the active document's published
142
+ * context, with its document id in `info`. The document owns the context's
143
+ * API. The step's source is serialized, so inline every value it needs and
144
+ * return plain data.
1289
145
  */
1290
146
  async run(step) {
1291
147
  return this.#client.documentScript(step.toString());
@@ -1316,15 +172,8 @@ var EXTENSION_KIND = {
1316
172
  ".glsl": "source",
1317
173
  ".vert": "source",
1318
174
  ".frag": "source",
1319
- // PROJECT SCRIPTS ARE SOURCE. Without these the guess below falls through to
1320
- // `'json'`, the asset-document router sends the file to the generic JSON
1321
- // viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
1322
- // 'json'` is decided before any content routing), and the LIVE MODELING
1323
- // DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
1324
- // silently shows a text pane instead of the model. Only `kind: 'source'`
1325
- // reaches `SourceAssetViewer`, which is what content-routes a project script
1326
- // to `LiveModuleDocument`. The set matches that viewer's own
1327
- // `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
175
+ // Source kinds select SourceAssetViewer, which routes project scripts by content to
176
+ // LiveModuleDocument. Keep these extensions aligned with its isProjectScriptPath predicate.
1328
177
  ".ts": "source",
1329
178
  ".tsx": "source",
1330
179
  ".mts": "source",
@@ -1341,61 +190,25 @@ function inferAssetKind(path) {
1341
190
  return EXTENSION_KIND[ext] ?? "json";
1342
191
  }
1343
192
  var LiveEditor = class {
1344
- /** `#`-private, not `private`: `cyclotron eval --list` enumerates this object's
193
+ /** `#`-private, not `private`: `<command> eval --list` enumerates this object's
1345
194
  * real runtime members, and TypeScript's erased `private` would leave the
1346
195
  * raw `EditorClient` advertised beside them. */
1347
196
  #client;
1348
- /**
1349
- * The ACTIVE center document's own DOM: read it, click it, key it, paste
1350
- * into it. The one door onto editor chrome that is not play-mode gated, and
1351
- * deliberately scoped to that document alone —
1352
- * `packages/sdk/src/kit/editor-document-probe.ts` carries the design and the
1353
- * refusal contract. Screenshotting the same subject is
1354
- * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
1355
- */
197
+ /** Read and drive DOM within a named scope: the active document by default, or editor chrome
198
+ * such as header, shelf, rail, outliner, utility and area. Targets outside the scope are refused.
199
+ * See `@volter/sdk`'s editor-document-probe for scope and gesture contracts. */
1356
200
  document;
1357
201
  constructor(client) {
1358
202
  this.#client = client;
1359
203
  this.document = new LiveEditorDocument(client);
1360
204
  }
1361
- /**
1362
- * THE BLENDER LANE'S VERBS, from `cyclotron eval`.
1363
- *
1364
- * Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
1365
- * BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
1366
- * `blender-execute`, `blender-scene-info`, `blender-object-info`,
1367
- * `blender-screenshot-view`, `blender-read-file`, `blender-write-file`,
1368
- * `blender-list-files`, `blender-stop` and `blender-status`. They were
1369
- * reachable from `@volter/sdk` and through `volter blender-mcp` but from
1370
- * no GENERAL door, so driving a session meant writing an MCP client script
1371
- * per question — the same discovery failure `eval-surface.ts`'s header
1372
- * records, in a lane that had not noticed it yet.
1373
- *
1374
- * cyclotron eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
1375
- *
1376
- * `blender-status` is the only verb that creates nothing: it answers whether
1377
- * this tab already has a session without starting one.
1378
- */
1379
- async blender(type, fields = {}) {
1380
- return this.#client.blender(type, fields);
1381
- }
1382
- /**
1383
- * MODEL PLAY'S LOG, from `cyclotron eval` — the same read as
1384
- * `cyclotron play-log`. A play script writes it with
1385
- * `play.log(kind, facts)`; the runner adds `play-start`, `script-reload`,
1386
- * `script-error` and `play-stop`. Every entry carries `simT` (simulation
1387
- * seconds) and `tick` (the frame). Each model document keeps its own: the
1388
- * current run, or the last one after Stop; Play from a fresh copy empties it.
1389
- * `documentId` picks one (`documents` lists them); the active Play's by default.
1390
- *
1391
- * cyclotron eval "(await editor.modelPlayLog({ kind: 'death' })).entries"
1392
- */
1393
- async modelPlayLog(query) {
1394
- return this.#client.modelPlayLog(query);
205
+ /** A verb a composed package contributes (`<package>-<verb>`), waited on for its own row's budget. */
206
+ async contributedVerb(type, fields = {}) {
207
+ return this.#client.contributedVerb(type, fields);
1395
208
  }
1396
209
  /**
1397
210
  * The active authoring adapter's persistence destination — where a save would
1398
- * land (`status().savePath`). A read only: a three root has no scene document
211
+ * land (`status().savePath`). A read only: a stage root has no scene document
1399
212
  * to open, and its root is activated instead.
1400
213
  */
1401
214
  async scene() {
@@ -1414,19 +227,10 @@ var LiveEditor = class {
1414
227
  async currentView() {
1415
228
  return this.#client.currentView();
1416
229
  }
1417
- /**
1418
- * Capture the same center document the human is currently looking at.
1419
- *
1420
- * A number is a SQUARE of that size — the default, and the right shape for
1421
- * an unstaged look at a model. `{width, height}` asks for a shaped frame, so
1422
- * a video-aspect look needs no crop afterwards. Both are bounded by the
1423
- * relay budget (64-1024 per side, total no larger than a 1024 square); see
1424
- * `@volter/sdk`'s `CaptureDimensions`.
1425
- * Supply a view to present and photograph it in one editor request.
1426
- */
1427
- /** The active document's RENDER alone, at the size asked for: what the document draws,
1428
- * without what the viewport draws over it (its navigation gizmo, its readouts). To see a
1429
- * stage as the person does, use `captureEditorChrome({ region: 'document' })`. */
230
+ /** Capture the active document's render without viewport navigation gizmos or readouts.
231
+ * A number requests a square; `{width, height}` requests a shaped frame, bounded by
232
+ * `CaptureDimensions`. Supply a view to present and capture it in one request.
233
+ * Use `captureEditorChrome({ region: 'document' })` to include viewport chrome. */
1430
234
  async captureActiveDocument(size, view) {
1431
235
  return this.#client.captureActiveDocument(size, view);
1432
236
  }
@@ -1473,7 +277,7 @@ var LiveEditor = class {
1473
277
  /**
1474
278
  * WATCH THE AGENT LOOK AROUND THE MODEL.
1475
279
  *
1476
- * Swings the open Object3D document's camera — the camera the human's tab is
280
+ * Swings the open asset document's camera — the camera the human's tab is
1477
281
  * showing — around the framed subject by `azimuth`/`elevation` RADIANS,
1478
282
  * animated over `duration` seconds (default 0.6), and resolves when the move
1479
283
  * ends. This is deliberately not a jump cut: the point of the verb is that a
@@ -1517,21 +321,6 @@ var LiveEditor = class {
1517
321
  async view(preset) {
1518
322
  await this.#client.viewPreset(preset);
1519
323
  }
1520
- /**
1521
- * Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
1522
- *
1523
- * A workspace is a task-named LAYOUT MEMORY over the one dock
1524
- * (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
1525
- * standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
1526
- * Switching is an EXPLICIT act — nothing in the editor moves chrome on its
1527
- * own, opening a document included — and this is the session door to it,
1528
- * beside `Window → Workspace` and the registered actions.
1529
- *
1530
- * Resolves once the dock has finished rebuilding, so a capture taken
1531
- * immediately after photographs the arrangement that was asked for. Each
1532
- * workspace remembers the user's own hand-tuning per project, so switching
1533
- * away and back is lossless.
1534
- */
1535
324
  async workspace(id) {
1536
325
  await this.#client.setWorkspace(id);
1537
326
  }
@@ -1546,34 +335,18 @@ var LiveEditor = class {
1546
335
  }
1547
336
  /**
1548
337
  * A document stage's viewport PRESENTATION — its draw mode, lighting, backdrop and overlays
1549
- * (`@volter/sdk/kit/viewport-presentation`) — resolved. With `layer`, that choice is
338
+ * (`@volter/sdk/viewport-presentation`) — resolved. With `layer`, that choice is
1550
339
  * recorded for the view first, as a person's toolbar change would be, e.g.
1551
340
  * `presentation('model:src/models/cube.blend', { all: { lighting: { studioPreset: 'kit' } } })`.
1552
- * With a STRING, the named view of that id (`*.view.ts`, e.g. `'godot'`) is put on the view
1553
- * whole; the answer's `viewPresets` lists the ones registered.
1554
341
  */
1555
342
  async presentation(documentId, layer) {
1556
343
  return this.#client.viewportPresentation(documentId, layer);
1557
344
  }
1558
- /**
1559
- * Set the MATERIAL apart from the bundle that usually carries it.
1560
- * Appearance is palette × material, independent axes by ruling, so
1561
- * `style()` alone can never say whether a cost belongs to the blur or to
1562
- * the palette. This is the door that measures them apart; it answers with
1563
- * what the chrome wears afterwards (`style` is `null` when the mix matches
1564
- * no registered bundle).
1565
- */
1566
- async appearance(appearance) {
1567
- return this.#client.setAppearance(appearance);
1568
- }
1569
- /** Focus an editor panel: a viewport tab, the console, the build surface, or
345
+ /** Focus an editor panel: the console, the build surface, or
1570
346
  * any key the editor's static-panel registry holds — an unknown key refuses
1571
347
  * naming the ones it does. */
1572
348
  async showPanel(name) {
1573
349
  switch (name) {
1574
- case "viewport-edit":
1575
- await this.#client.showViewport("edit");
1576
- return;
1577
350
  case "console":
1578
351
  await this.#client.toggleConsole();
1579
352
  return;
@@ -1584,21 +357,16 @@ var LiveEditor = class {
1584
357
  }
1585
358
  /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
1586
359
  async openAsset(path, kind) {
1587
- if (path.toLowerCase().endsWith(".blend"))
1588
- throw new Error(
1589
- `${path} is a Blender Model document, not an asset: open it with editor.open("model:${path}").`
1590
- );
360
+ const document = (await this.#client.documentTable()).entries.find((entry) => entry.source?.path === path);
361
+ if (document)
362
+ throw new Error(`${path} is a ${document.kind} document, not an asset: open it with editor.open("${document.id}").`);
1591
363
  await this.#client.openAsset(path, kind ?? inferAssetKind(path));
1592
364
  }
1593
365
  /**
1594
366
  * SELECT a project asset — the browser's single click, which fills the
1595
367
  * Inspector without opening a document. `openAsset` is the double click.
1596
- *
1597
- * This is how a project's own `asset.inspector` section is reached: select
1598
- * the file it matches, then `inspect()` lists the verbs that section
1599
- * declares and `runAction(id)` runs one. Selecting a path nothing matches
1600
- * is not an error — the Inspector shows what it has, exactly as it does
1601
- * for a human.
368
+ * Selecting a path nothing matches is not an error — the Inspector shows
369
+ * what it has, exactly as it does for a human.
1602
370
  */
1603
371
  async selectAsset(path) {
1604
372
  await this.#client.selectAsset(path);
@@ -1611,8 +379,8 @@ var LiveEditor = class {
1611
379
  * where it stands under the scene's own lighting, reachable from here — or
1612
380
  * RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
1613
381
  * only in the calling Node process's memory and has never been written to
1614
- * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
1615
- * always produced, and the bytes form is lab-only.
382
+ * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging, and the
383
+ * bytes form is lab-only.
1616
384
  */
1617
385
  async assetPreview(source, options) {
1618
386
  return this.#client.captureAssetPreview(
@@ -1626,8 +394,6 @@ var LiveEditor = class {
1626
394
  * crops, rendered against the asset's own skeleton, with a contact sheet.
1627
395
  * Every source {@link assetPreview} takes works here, GLB bytes included:
1628
396
  * a shot set stages its own subject, so it needs no place to stand.
1629
- *
1630
- * Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
1631
397
  */
1632
398
  async assetPreviewShots(source, definition, options) {
1633
399
  return this.#client.captureShotSetPreview(
@@ -1648,33 +414,6 @@ var LiveEditor = class {
1648
414
  async shading(mode) {
1649
415
  await this.#client.setShadingMode(mode);
1650
416
  }
1651
- /**
1652
- * READ the inspector, as data — the serialized inspection subject
1653
- * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
1654
- * "The Inspection Model"). This is the Figma-Inspect analog: whatever a
1655
- * human would see in the inspector right now — the subject's identity, its
1656
- * verbs, and every identified section in display order, with a `fields`
1657
- * section's CURRENT VALUES at their scriptable `path`s.
1658
- *
1659
- * Reach for it whenever the next step depends on what an object actually
1660
- * IS: `await editor.select(id)` then `await editor.inspect()` answers "what
1661
- * properties does this thing have, and what are they set to" in one call,
1662
- * against the same model the panel renders — no scene-graph reads, no
1663
- * guessing at property names.
1664
- *
1665
- * When the inspector is showing NOTHING — nothing selected on a surface
1666
- * with no empty-state subject of its own, which is most of them — the answer
1667
- * is `{none: true}`, so "the human sees no inspector" and "the read failed"
1668
- * are never the same value. A surface whose empty space IS a real thing (an
1669
- * open Asset Lab document) still answers with that subject, and never with
1670
- * another surface's.
1671
- *
1672
- * A `custom` section body is a named opaque: the editor renders it with
1673
- * React, so the wire reports its identity rather than pretending to describe
1674
- * its rendering — plus, when the section can say what it DISPLAYS, a `data`
1675
- * payload in its own vocabulary (`transform` carries
1676
- * `{position, rotation, scale}`, rotation in Euler XYZ degrees).
1677
- */
1678
417
  async inspect() {
1679
418
  return this.#client.inspect();
1680
419
  }
@@ -1684,32 +423,11 @@ var LiveEditor = class {
1684
423
  return this.#client.runInspectionAction(actionId);
1685
424
  }
1686
425
  /**
1687
- * Run ONE command by id — the door to everything the command palette lists.
1688
- *
1689
- * ONE NAME (orchestrator ruling 2026-09-19). There were briefly TWO doors
1690
- * onto the one view-verb table — this one and `editor.viewVerb(view, verb)`,
1691
- * which addressed the same registry by its two halves. A second addressing
1692
- * of one table is a second name for one thing, and an agent reading
1693
- * `--list` had to choose between them with nothing to choose on. This door
1694
- * stays because it is strictly wider: it addresses a COMMAND ID, so under
1695
- * the frame it reaches everything the workbench knows — a `volter.action.<id>`
1696
- * editor action, one of VS Code's own — and not only a view. A VIEW is
1697
- * reached by spelling its verb's command id:
1698
- *
1699
- * await editor.command('volter.blender-uv-view.state')
1700
- * await editor.command('volter.blender-uv-view.zoom', { to: 600 })
426
+ * Run a command by id. Under the native frame this uses ICommandService and
427
+ * can invoke editor or VS Code commands. Standalone sessions resolve registered
428
+ * view verbs of the form `volter.<view>.<verb>` and refuse other ids.
1701
429
  *
1702
- * await editor.command('volter.blender-node-view.view-all')
1703
- * await editor.command('volter.blender-node-view.look', { node: 'Principled BSDF' })
1704
- *
1705
- * Under the Code-OSS frame this is the workbench's own command service, so
1706
- * any command id works — ours and VS Code's alike. Standalone the editor's `edit` command
1707
- * has no command service and answers the `volter.<view>.<verb>` shape off the
1708
- * SAME verb table the frame's commands call, refusing any other id by name.
1709
- * One table, two doors, exactly like the keymap's.
1710
- *
1711
- * Answers with whatever the command returned — a view verb's own state, or
1712
- * `null` for a command that returns nothing.
430
+ * Returns the command’s result, or `null` when it returns nothing.
1713
431
  */
1714
432
  async command(commandId, args) {
1715
433
  return this.#client.runCommand(commandId, args);
@@ -1724,11 +442,6 @@ var LiveEditor = class {
1724
442
  * helpers the menu items call, so there is one implementation of each op and
1725
443
  * not a second that can disagree with what a human gets.
1726
444
  *
1727
- * It exists because the menu is a POINTER surface: every one of these ops was
1728
- * reachable only by right-clicking a hierarchy row, which is nothing an agent
1729
- * can do — so for an ingest root, whose only authoring surface IS the editor,
1730
- * structure was closed entirely.
1731
- *
1732
445
  * `id`/`ids` default to the current selection. The answer carries the same
1733
446
  * per-edit `write` ack `setField` does, so `write.persisted` tells a saved
1734
447
  * restructure from a live-only one. An op the active adapter does not provide
@@ -1764,23 +477,14 @@ var LiveEditor = class {
1764
477
  *
1765
478
  * The companion to {@link inspect}: that one answers "what IS the selected
1766
479
  * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
1767
- * own output, not a fresh walk of the scene — the adapter's tree after the
1768
- * component marks fold implementation subtrees (bones, particle renderers,
1769
- * instanced pools), after the internals reveal, the document promotion, the
1770
- * child cap, the collapse state, the search filter and the selection scope.
1771
- *
1772
- * Works in play mode and edit mode; the answer says which (`playState`,
1773
- * `activeViewportTab`), because the two are different adapters and a tree
1774
- * that looks wrong is very often the wrong adapter's tree.
480
+ * own projection of component marks, document promotion, child caps,
481
+ * collapse state, search and selection scope onto the adapter tree.
1775
482
  *
1776
483
  * Prefer this over `status().entities`, which is deliberately a different
1777
484
  * question — the RAW adapter tree, unprojected. A panel that renders the
1778
485
  * wrong rows looks perfectly healthy in that facet.
1779
486
  *
1780
- * Each row carries `childCount` (what its caret opens), `internalChildCount`
1781
- * (what is folded behind "Reveal Internals") and `expandable` (whether the
1782
- * panel draws a caret at all), so "this subtree exists but nothing in the UI
1783
- * opens it" is a fact you can read rather than one you have to notice.
487
+ * Each row reports the projected child count and whether the panel draws a disclosure control.
1784
488
  *
1785
489
  * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
1786
490
  * tree would be a fabricated answer about a surface nobody is being shown.
@@ -1855,36 +559,6 @@ var LiveEditor = class {
1855
559
  }
1856
560
  };
1857
561
 
1858
- // src/tools.ts
1859
- var LiveTools = class {
1860
- /** `#`-private for the same reason `LiveEditor.#client` is. */
1861
- #client;
1862
- constructor(client) {
1863
- this.#client = client;
1864
- }
1865
- /** Enumerate the exact `package.json#volter.tools` catalog without executing it. */
1866
- async list() {
1867
- return this.#client.listProjectTools();
1868
- }
1869
- /** Return one tool's discoverable metadata, or `null` when it is not registered. */
1870
- async describe(name) {
1871
- const catalog = await this.list();
1872
- return catalog.tools.find((tool) => tool.name === name) ?? null;
1873
- }
1874
- /**
1875
- * Invoke the same validated callable used by the editor and CLI.
1876
- *
1877
- * `instance` names WHICH mounted instance the tool should drive when several
1878
- * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
1879
- * and a tool that drives the game binds
1880
- * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
1881
- * the tool then targets the sole live instance, exactly as before.
1882
- */
1883
- async run(name, input = {}, options = {}) {
1884
- return this.#client.runProjectTool(name, input, options);
1885
- }
1886
- };
1887
-
1888
562
  // src/lazy-proxy.ts
1889
563
  function isFunction(value) {
1890
564
  return typeof value === "function";
@@ -1924,355 +598,35 @@ function lazyChainProxy(resolveRoot, path = [], hint) {
1924
598
  }
1925
599
 
1926
600
  // src/session.ts
1927
- import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync } from "node:fs";
1928
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
601
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
602
+ import { dirname, join as join2, resolve } from "node:path";
1929
603
 
1930
604
  // ../project/src/manifest/locate.ts
1931
- import { existsSync } from "node:fs";
1932
605
  import { join } from "node:path";
1933
606
 
1934
607
  // ../project/src/manifest/filename.ts
1935
608
  var MANIFEST_FILENAME = "volter.project.json";
1936
- var REMOVED_MANIFEST_FILENAME = "volter.game.json";
1937
- function removedManifestFilenameMessage(where) {
1938
- 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.
1939
- Fix: rename it \u2014 \`git mv ${REMOVED_MANIFEST_FILENAME} ${MANIFEST_FILENAME}\`. The contents are unchanged; only the filename moved.`;
1940
- }
1941
609
 
1942
610
  // ../project/src/manifest/locate.ts
1943
- function assertNoRemovedManifestFilename(dir) {
1944
- if (existsSync(join(dir, MANIFEST_FILENAME))) return;
1945
- if (!existsSync(join(dir, REMOVED_MANIFEST_FILENAME))) return;
1946
- throw new Error(removedManifestFilenameMessage(join(dir, REMOVED_MANIFEST_FILENAME)));
1947
- }
1948
611
  function resolveManifestPath(dir) {
1949
- assertNoRemovedManifestFilename(dir);
1950
612
  return join(dir, MANIFEST_FILENAME);
1951
613
  }
1952
614
 
1953
- // ../sdk/src/session/product-locator.ts
1954
- import { existsSync as existsSync2, readFileSync } from "node:fs";
1955
- import { createRequire } from "node:module";
1956
- import { dirname, join as join2 } from "node:path";
1957
- import { pathToFileURL } from "node:url";
1958
- var PRODUCT_DECLARATION_KEY = "volter.product";
1959
- var PRODUCT_INSTALL_LINES = [
1960
- " npm install --save-dev @volter/cyclotron",
1961
- " npm install --save-dev @volter/game-editor"
1962
- ];
1963
- var PRODUCT_CREATE_LINES = [" npx @volter/editor-core create <name>"];
1964
- var PRODUCT_COLOR_THEMES = ["dark", "light"];
1965
- function readProductManifest(packageDir) {
1966
- const manifestPath = join2(packageDir, "package.json");
1967
- let manifest;
1968
- try {
1969
- manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
1970
- } catch {
1971
- return null;
1972
- }
1973
- const declared = manifest.volter?.product;
1974
- if (declared === void 0) return null;
1975
- const entry = declared.entry;
1976
- if (typeof entry !== "string" || !entry.startsWith("./"))
1977
- throw new Error(
1978
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "entry" is ${JSON.stringify(entry)}. It must be a ./-relative module \u2014 the product's ONE source entry, which is also its frame entry (e.g. "./src/index.ts").`
1979
- );
1980
- const resolved = join2(packageDir, entry);
1981
- if (!existsSync2(resolved))
1982
- throw new Error(
1983
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY}.entry ${JSON.stringify(entry)}, which does not exist (${resolved}).`
1984
- );
1985
- const colorTheme = declared.colorTheme;
1986
- if (typeof colorTheme !== "string" || !PRODUCT_COLOR_THEMES.includes(colorTheme))
1987
- throw new Error(
1988
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "colorTheme" is ${JSON.stringify(colorTheme)}. It must be ${PRODUCT_COLOR_THEMES.map(
1989
- (one) => JSON.stringify(one)
1990
- ).join(" or ")} \u2014 which way round this product paints, read before any of its code runs so the workbench's first frame is already the product's own background.`
1991
- );
1992
- const bin = manifest.bin;
1993
- const commands = bin !== null && typeof bin === "object" ? Object.keys(bin) : [];
1994
- if (commands.length !== 1)
1995
- throw new Error(
1996
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "bin" names ${commands.length === 0 ? "no command" : `${commands.length} commands`}. A product has exactly one command \u2014 the one every session message tells a person to run.`
1997
- );
1998
- const displayName = declared.displayName;
1999
- if (typeof displayName !== "string" || displayName.trim() === "")
2000
- throw new Error(
2001
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "displayName" is ${JSON.stringify(displayName)}. It must be the name a person sees (e.g. "Volter Editor").`
2002
- );
2003
- const name = typeof manifest.name === "string" ? manifest.name : packageDir;
2004
- const install = declared.install;
2005
- if (install !== void 0 && (typeof install !== "string" || install.trim() === ""))
2006
- throw new Error(
2007
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "install" is ${JSON.stringify(install)}. It is the one line that installs this product and starts a project (e.g. "npx ${name} create my-game"), or absent.`
2008
- );
2009
- return {
2010
- name,
2011
- version: typeof manifest.version === "string" ? manifest.version : "unknown",
2012
- command: commands[0],
2013
- displayName,
2014
- upgrade: declared.upgrade === true,
2015
- adapterRequired: declared.adapterRequired === true,
2016
- dir: packageDir,
2017
- entry,
2018
- colorTheme,
2019
- // A private source product may use an explicitly selected matching checkout.
2020
- // Public packages must always carry a downloadable, checksummed workbench.
2021
- workbench: manifest.private === true && declared.workbench === void 0 ? null : readProductWorkbench(manifestPath, declared.workbench),
2022
- install: typeof install === "string" ? install.trim() : `npx ${name} create my-game`
2023
- };
2024
- }
2025
- var SHA256_HEX = /^[0-9a-f]{64}$/;
2026
- function readProductWorkbench(manifestPath, declared) {
2027
- const example = ' "volter": { "product": { "workbench": { "release": "<product>-<fork sha 12>-<platform>", "tarballSha256": "<64 hex>" } } }\n`node scripts/workbench/build-release.mjs --publish --out <release dir>` cuts the release and prints exactly that block.';
2028
- if (typeof declared !== "object" || declared === null || Array.isArray(declared))
2029
- throw new Error(
2030
- `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "workbench" is ${JSON.stringify(declared)}. A product IS the Code-OSS workbench it was compiled into, so it names the published release those bytes are:
2031
- ${example}`
2032
- );
2033
- const platforms = declared;
2034
- if (!("release" in platforms) && !("tarballSha256" in platforms)) {
2035
- const machine = `${process.platform}-${process.arch}`;
2036
- const declaredHere = platforms[machine];
2037
- if (declaredHere === void 0)
2038
- throw new Error(
2039
- `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench declares releases for ${Object.keys(platforms).join(", ") || "no platform"} and this machine is ${machine}. A Code-OSS server package carries its own platform's node binary and native modules, so there is nothing this product can run in here until a ${machine} release is cut:
2040
- node scripts/workbench/build-release.mjs --product <id> --platform ${machine} --checkout <fork dir> --publish`
2041
- );
2042
- return readPlatformWorkbench(manifestPath, declaredHere, example);
2043
- }
2044
- return readPlatformWorkbench(manifestPath, declared, example);
2045
- }
2046
- function readPlatformWorkbench(manifestPath, declared, example) {
2047
- if (typeof declared !== "object" || declared === null || Array.isArray(declared))
2048
- throw new Error(
2049
- `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names ${JSON.stringify(declared)} for this platform, which is not a release:
2050
- ${example}`
2051
- );
2052
- const record = declared;
2053
- const extra = Object.keys(record).filter((key) => key !== "release" && key !== "tarballSha256");
2054
- if (extra.length > 0)
2055
- throw new Error(
2056
- `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench carries ${extra.map((key) => `"${key}"`).join(", ")}, which this declaration has no meaning for. It names the release and the pin, and nothing else:
2057
- ${example}`
2058
- );
2059
- const release = record["release"];
2060
- if (typeof release !== "string" || release.trim() === "")
2061
- throw new Error(
2062
- `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names no "release" \u2014 the GitHub Release tag its workbench was published under:
2063
- ${example}`
2064
- );
2065
- const tarballSha256 = record["tarballSha256"];
2066
- if (typeof tarballSha256 !== "string" || !SHA256_HEX.test(tarballSha256))
2067
- throw new Error(
2068
- `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench has tarballSha256 ${JSON.stringify(tarballSha256)}, which is not 64 hex characters. It is the PIN \u2014 the hash the downloaded tarball is checked against before a byte of it is extracted, so it is never approximate:
2069
- ${example}`
2070
- );
2071
- return { release, tarballSha256 };
2072
- }
2073
- function declaredVolterPackages(projectRoot) {
2074
- let manifest;
2075
- try {
2076
- manifest = JSON.parse(
2077
- readFileSync(join2(projectRoot, "package.json"), "utf8")
2078
- );
2079
- } catch {
2080
- return [];
2081
- }
2082
- return Object.keys({ ...manifest.dependencies ?? {}, ...manifest.devDependencies ?? {} }).filter((name) => name.startsWith("@volter/")).sort();
2083
- }
2084
- function locatePackage(projectRoot, name) {
2085
- const require2 = createRequire(pathToFileURL(join2(projectRoot, "package.json")));
2086
- try {
2087
- return dirname(require2.resolve(`${name}/package.json`));
2088
- } catch {
2089
- try {
2090
- let dir = dirname(require2.resolve(name));
2091
- for (; ; ) {
2092
- const candidate = join2(dir, "package.json");
2093
- try {
2094
- if (JSON.parse(readFileSync(candidate, "utf8")).name === name)
2095
- return dir;
2096
- } catch {
2097
- }
2098
- const parent = dirname(dir);
2099
- if (parent === dir) return null;
2100
- dir = parent;
2101
- }
2102
- } catch {
2103
- return null;
2104
- }
2105
- }
2106
- }
2107
- function declaredProducts(projectRoot) {
2108
- const found = [];
2109
- for (const name of declaredVolterPackages(projectRoot)) {
2110
- const dir = locatePackage(projectRoot, name);
2111
- if (dir === null) continue;
2112
- const product = readProductManifest(dir);
2113
- if (product !== null) found.push(product);
2114
- }
2115
- return found;
2116
- }
2117
- function resolveProductForProject(projectRoot) {
2118
- const products = declaredProducts(projectRoot);
2119
- if (products.length === 1) return products[0];
2120
- if (products.length === 0)
2121
- throw new Error(
2122
- `${projectRoot} declares no editor product, so there is no editor to open it in.
2123
- The installed product supplies the editor,
2124
- and which one runs is what the project installed, never a flag.
2125
- For THIS folder, install the one you are building with:
2126
- ${PRODUCT_INSTALL_LINES.join("\n")}
2127
- then run that product's \`edit\` command again. For a NEW project, the product creates it:
2128
- ${PRODUCT_CREATE_LINES.join("\n")}`
2129
- );
2130
- throw new Error(
2131
- `${projectRoot} declares ${products.length} editor products \u2014 ${products.map((product) => product.name).join(" and ")}.
2132
- One page runs one product, and nothing here may choose between them. Remove the one you
2133
- are not building with:
2134
- ` + products.map((product) => ` npm uninstall ${product.name}`).join("\n")
2135
- );
2136
- }
2137
-
2138
- // ../sdk/src/session/registry-format.ts
2139
- import {
2140
- mkdirSync,
2141
- readdirSync,
2142
- readFileSync as readFileSync2,
2143
- realpathSync,
2144
- unlinkSync,
2145
- writeFileSync
2146
- } from "node:fs";
2147
- import { homedir } from "node:os";
2148
- import { join as join3, resolve } from "node:path";
2149
- var EDITOR_SESSIONS_REGISTRY_FILE = join3(homedir(), ".volter", "editor-sessions.json");
2150
- function isEditorSessionEntry(v) {
2151
- if (typeof v !== "object" || v === null) return false;
2152
- const s = v;
2153
- const optionalIdentity = (value) => value === void 0 || value === null || typeof value === "string";
2154
- 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"]);
2155
- }
2156
- function normalizeEditorSessionEntry(session) {
2157
- return {
2158
- ...session,
2159
- sessionId: session.sessionId ?? null,
2160
- controlSecret: session.controlSecret ?? null,
2161
- repositoryId: session.repositoryId ?? null,
2162
- worktreeId: session.worktreeId ?? null,
2163
- worktreeRoot: session.worktreeRoot ?? null,
2164
- projectRelativePath: session.projectRelativePath ?? null,
2165
- branch: session.branch ?? null,
2166
- headCommit: session.headCommit ?? null,
2167
- baseCommit: session.baseCommit ?? null
2168
- };
2169
- }
2170
- function pidAlive(pid) {
2171
- try {
2172
- process.kill(pid, 0);
2173
- return true;
2174
- } catch (error) {
2175
- return error.code === "EPERM";
2176
- }
2177
- }
2178
- function readLiveRegisteredSessions() {
2179
- try {
2180
- const raw = JSON.parse(readFileSync2(EDITOR_SESSIONS_REGISTRY_FILE, "utf8"));
2181
- return Array.isArray(raw) ? raw.filter(isEditorSessionEntry).map(normalizeEditorSessionEntry).filter((s) => pidAlive(s.pid)) : [];
2182
- } catch {
2183
- return [];
2184
- }
2185
- }
2186
- function servedProjectAnswer(body) {
2187
- const b = body;
2188
- return {
2189
- path: b.project?.path ?? b.serving?.path ?? null,
2190
- manifestError: b.project ? null : b.serving?.error ?? null
2191
- };
2192
- }
2193
-
2194
- // ../sdk/src/session/discovery.ts
2195
- var EDITOR_SESSION_DISCOVERY_TIMEOUT_MS = 2e3;
2196
- var EDITOR_PROBE_TIMEOUT_MS = 1500;
2197
- var EditorTimeoutError = class extends Error {
2198
- constructor(label, ms) {
2199
- super(`${label} timed out after ${ms}ms`);
2200
- this.name = "EditorTimeoutError";
2201
- }
2202
- };
2203
- function withTimeout(promise, ms, label) {
2204
- return new Promise((resolvePromise, reject) => {
2205
- const timer = setTimeout(() => reject(new EditorTimeoutError(label, ms)), ms);
2206
- promise.then(
2207
- (v) => {
2208
- clearTimeout(timer);
2209
- resolvePromise(v);
2210
- },
2211
- (err) => {
2212
- clearTimeout(timer);
2213
- reject(err instanceof Error ? err : new Error(String(err)));
2214
- }
2215
- );
2216
- });
2217
- }
2218
- async function fetchJson(url, timeoutMs) {
2219
- const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
2220
- if (!res.ok) throw new Error(`GET /__editor/project returned HTTP ${res.status}`);
2221
- return await res.json();
2222
- }
2223
- function baseUrl(session) {
2224
- return session.url ?? `http://127.0.0.1:${session.port}`;
2225
- }
2226
- var HttpSessionDiscovery = class {
2227
- async listSessions(timeoutMs) {
2228
- const registered = readLiveRegisteredSessions();
2229
- const perProbeTimeout = Math.min(EDITOR_PROBE_TIMEOUT_MS, Math.max(200, timeoutMs));
2230
- const failures = [];
2231
- const probes = await Promise.all(
2232
- registered.map(async (s) => {
2233
- try {
2234
- const body = await fetchJson(
2235
- `${baseUrl({ port: s.port, project: null, pid: s.pid })}/__editor/project`,
2236
- perProbeTimeout
2237
- );
2238
- const served = servedProjectAnswer(body);
2239
- const info = {
2240
- port: s.port,
2241
- project: served.path,
2242
- pid: s.pid,
2243
- manifestError: served.manifestError
2244
- };
2245
- return info;
2246
- } catch (error) {
2247
- const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2248
- failures.push(`port ${s.port}: ${detail}`);
2249
- return void 0;
2250
- }
2251
- })
2252
- );
2253
- const sessions = probes.filter((p) => p !== void 0);
2254
- if (registered.length > 0 && sessions.length === 0) {
2255
- throw new Error(`Registered editor sessions could not be inspected: ${failures.join("; ")}`);
2256
- }
2257
- return sessions;
2258
- }
2259
- };
2260
-
2261
615
  // src/session.ts
2262
616
  function findProjectRootFrom(dir) {
2263
- let cur = resolve2(dir);
617
+ let cur = resolve(dir);
2264
618
  for (; ; ) {
2265
- if (existsSync3(resolveManifestPath(cur))) return cur;
2266
- const parent = dirname2(cur);
619
+ if (existsSync(resolveManifestPath(cur))) return cur;
620
+ const parent = dirname(cur);
2267
621
  if (parent === cur) return null;
2268
622
  cur = parent;
2269
623
  }
2270
624
  }
2271
625
  function canonicalPath(path) {
2272
626
  try {
2273
- return realpathSync2(path);
627
+ return realpathSync(path);
2274
628
  } catch {
2275
- return resolve2(path);
629
+ return resolve(path);
2276
630
  }
2277
631
  }
2278
632
  function sameDirectory(a, b) {
@@ -2285,10 +639,11 @@ function sameDirectory(a, b) {
2285
639
  }
2286
640
  }
2287
641
  var NO_SESSION_FOR_PROJECT = "VOLTER_NO_SESSION_FOR_PROJECT";
642
+ var SESSION_MANIFEST_UNREADABLE = "VOLTER_SESSION_MANIFEST_UNREADABLE";
2288
643
  function readProjectSession(projectRoot) {
2289
644
  try {
2290
645
  const value = JSON.parse(
2291
- readFileSync3(join4(projectRoot, ".volter", "session.json"), "utf8")
646
+ readFileSync(join2(projectRoot, ".volter", "session.json"), "utf8")
2292
647
  );
2293
648
  if (typeof value !== "object" || value === null) return null;
2294
649
  const hint = value;
@@ -2311,16 +666,20 @@ async function probeServedProject(port) {
2311
666
  return void 0;
2312
667
  }
2313
668
  }
2314
- function manifestRefusal(projectRoot, manifestError) {
2315
- return new Error(
2316
- `@volter/live: the editor session for ${projectRoot} is live, but its volter.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.
669
+ function manifestRefusal(projectRoot, manifestError, port) {
670
+ return Object.assign(
671
+ new Error(
672
+ `@volter/live: the editor session for ${projectRoot} is live, but its volter.project.json does not load, so there is no editor or project to drive \u2014 the editor page is showing this same error. Fix the manifest and retry; the session recovers on save, no restart needed.
2317
673
  ${manifestError}`
674
+ ),
675
+ // The session is still this project's, so a caller that only needs to END it (`close`) can.
676
+ { code: SESSION_MANIFEST_UNREADABLE, session: { port, projectRoot } }
2318
677
  );
2319
678
  }
2320
679
  function noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure) {
2321
680
  if (discoveryFailure !== null) {
2322
681
  return new Error(
2323
- `@volter/live: could not complete editor session discovery 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, ${sessionsCommandFor(projectRoot)} asks the same question directly.`
682
+ `@volter/live: could not complete editor session discovery 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. The step named above is the one that did not answer: that session's server holds its port but did not reply to the request. Its own log (logs/editor-<port>.log in the project it serves) says how far its start got; ${sessionsCommandFor(projectRoot)} asks the same question directly.`
2324
683
  );
2325
684
  }
2326
685
  if (sessions.length === 0) {
@@ -2356,7 +715,7 @@ async function resolveSession(projectDir = process.cwd(), deps = {}) {
2356
715
  } else {
2357
716
  const served = await probeServedProject(localHint.port);
2358
717
  if (served?.path != null && canonicalPath(served.path) === canonicalPath(projectRoot)) {
2359
- if (served.manifestError !== null) throw manifestRefusal(projectRoot, served.manifestError);
718
+ if (served.manifestError !== null) throw manifestRefusal(projectRoot, served.manifestError, localHint.port);
2360
719
  return { port: localHint.port, projectRoot };
2361
720
  }
2362
721
  }
@@ -2376,7 +735,7 @@ async function resolveSession(projectDir = process.cwd(), deps = {}) {
2376
735
  const canon = canonicalPath(projectRoot);
2377
736
  const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
2378
737
  if (!match) throw noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure);
2379
- if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError);
738
+ if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError, match.port);
2380
739
  return { port: match.port, projectRoot };
2381
740
  }
2382
741
  function editCommandFor(projectRoot) {
@@ -2400,15 +759,12 @@ function createLazySession(factory) {
2400
759
  ensure() {
2401
760
  promise ??= factory();
2402
761
  return promise;
2403
- },
2404
- reset() {
2405
- promise = null;
2406
762
  }
2407
763
  };
2408
764
  }
2409
765
 
2410
766
  // src/member-hints.ts
2411
- var CAMERA_HINT = "The viewport camera is posed through a presented view: await editor.present({ version: 1, viewport: { camera: { position: { x, y, z }, target: { x, y, z }, fov } } }) (stage space: metres, Y up; a Blender point (x, y, z) is { x, y: z, z: -y }), and read back with (await editor.currentView()).viewport.camera. From Cyclotron's shell: cyclotron camera --position x,y,z --target x,y,z [--fov n] (Blender coordinates). editor.frame() and editor.orbit() frame or swing around the subject instead.";
767
+ var CAMERA_HINT = "The viewport camera is posed through a presented view: await editor.present({ version: 1, viewport: { camera: { position: { x, y, z }, target: { x, y, z }, fov } } }) (stage space: metres, Y up), and read back with (await editor.currentView()).viewport.camera. editor.frame() and editor.orbit() frame or swing around the subject instead.";
2412
768
  var EDITOR_MEMBER_HINTS = {
2413
769
  camera: CAMERA_HINT,
2414
770
  setCamera: CAMERA_HINT,
@@ -2422,12 +778,12 @@ var EDITOR_MEMBER_HINTS = {
2422
778
  function editorMemberHint(member) {
2423
779
  return typeof member === "string" && Object.hasOwn(EDITOR_MEMBER_HINTS, member) ? EDITOR_MEMBER_HINTS[member] : void 0;
2424
780
  }
2425
- function withEditorMemberHints(target, binding = "editor") {
781
+ function withEditorMemberHints(target) {
2426
782
  return new Proxy(target, {
2427
783
  get(object, key) {
2428
784
  if (!(key in object)) {
2429
785
  const hint = editorMemberHint(key);
2430
- if (hint !== void 0) throw new TypeError(`${binding}.${String(key)} does not exist. ${hint}`);
786
+ if (hint !== void 0) throw new TypeError(`editor.${String(key)} does not exist. ${hint}`);
2431
787
  }
2432
788
  const value = Reflect.get(object, key, object);
2433
789
  return typeof value === "function" ? value.bind(object) : value;
@@ -2438,7 +794,7 @@ function withEditorMemberHints(target, binding = "editor") {
2438
794
  // src/index.ts
2439
795
  function bindTo(port) {
2440
796
  const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
2441
- return { editor: new LiveEditor(client), tools: new LiveTools(client) };
797
+ return { editor: new LiveEditor(client) };
2442
798
  }
2443
799
  function unconnectedBindings() {
2444
800
  return bindTo(0);
@@ -2449,19 +805,17 @@ async function connect(projectDir, deps) {
2449
805
  }
2450
806
  var lazySession = createLazySession(() => connect());
2451
807
  var editor = lazyChainProxy(() => lazySession.ensure().then((s) => s.editor), [], editorMemberHint);
2452
- var tools = lazyChainProxy(() => lazySession.ensure().then((s) => s.tools));
2453
808
  export {
2454
809
  LiveEditor,
2455
810
  LiveEditorDocument,
2456
- LiveTools,
2457
811
  NO_SESSION_FOR_PROJECT,
812
+ SESSION_MANIFEST_UNREADABLE,
2458
813
  connect,
2459
814
  editor,
2460
815
  editorMemberHint,
2461
816
  findProjectRootFrom,
2462
817
  inferAssetKind,
2463
818
  resolveSession,
2464
- tools,
2465
819
  unconnectedBindings,
2466
820
  withEditorMemberHints
2467
821
  };