@vgai/sdk 0.4.0-canary.20260715.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/package.json +27 -0
  2. package/src/cinematic/capabilities-operations.ts +128 -0
  3. package/src/cinematic/cue-operations.ts +198 -0
  4. package/src/cinematic/gsap-operations.ts +126 -0
  5. package/src/cinematic/index.ts +59 -0
  6. package/src/cinematic/preview-operations.ts +279 -0
  7. package/src/cinematic/preview-transport.ts +244 -0
  8. package/src/cinematic/render-operations.ts +409 -0
  9. package/src/cinematic/render-transport.ts +238 -0
  10. package/src/cinematic/theatre-operations.ts +306 -0
  11. package/src/editor/camera-operations.ts +169 -0
  12. package/src/editor/console-operations.ts +87 -0
  13. package/src/editor/hierarchy-operations.ts +95 -0
  14. package/src/editor/index.ts +62 -0
  15. package/src/editor/open-operations.ts +209 -0
  16. package/src/editor/screenshot-operations.ts +99 -0
  17. package/src/editor/selection-operations.ts +144 -0
  18. package/src/editor/session-operations.ts +73 -0
  19. package/src/editor/source-location-operations.ts +106 -0
  20. package/src/editor/transport.ts +647 -0
  21. package/src/errors.ts +72 -0
  22. package/src/http/http-projection.ts +349 -0
  23. package/src/http/index.ts +11 -0
  24. package/src/index.ts +67 -0
  25. package/src/mcp/index.ts +16 -0
  26. package/src/mcp/mcp-projection.ts +288 -0
  27. package/src/operations.ts +83 -0
  28. package/src/play/control-operations.ts +205 -0
  29. package/src/play/debug-command-operations.ts +245 -0
  30. package/src/play/index.ts +66 -0
  31. package/src/play/input-operations.ts +316 -0
  32. package/src/play/lifecycle-operations.ts +271 -0
  33. package/src/play/log-operations.ts +279 -0
  34. package/src/play/run-ticks-operations.ts +141 -0
  35. package/src/play/state-operations.ts +210 -0
  36. package/src/play/status-operations.ts +160 -0
  37. package/src/play/transport.ts +728 -0
  38. package/src/project/asset-operations.ts +243 -0
  39. package/src/project/component-operations.ts +337 -0
  40. package/src/project/discovery-operations.ts +269 -0
  41. package/src/project/entity-operations.ts +366 -0
  42. package/src/project/index.ts +55 -0
  43. package/src/project/input-map-operations.ts +233 -0
  44. package/src/project/manifest-operations.ts +355 -0
  45. package/src/project/scene-operations.ts +426 -0
  46. package/src/project/shared.ts +299 -0
  47. package/src/registry.ts +285 -0
  48. package/src/render/capabilities/ffmpeg.ts +141 -0
  49. package/src/render/index.ts +15 -0
  50. package/src/render/render-cinematic.ts +1847 -0
  51. package/src/types.ts +101 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * B3 — `editor.*` operations
3
+ * (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B3). Registered
4
+ * separately from B1's `registerBuiltinOperations` (`../operations.ts`) and
5
+ * B2's `registerProjectOperations` (`../project/index.ts`) — the default
6
+ * `operations` singleton (`../index.ts`) calls all three.
7
+ */
8
+
9
+ export * from './camera-operations.js';
10
+ export * from './console-operations.js';
11
+ export * from './hierarchy-operations.js';
12
+ export * from './open-operations.js';
13
+ export * from './screenshot-operations.js';
14
+ export * from './selection-operations.js';
15
+ export * from './session-operations.js';
16
+ export * from './source-location-operations.js';
17
+ export type {
18
+ CameraPose,
19
+ CameraSetRequest,
20
+ ConsoleSubscriptionMetadata,
21
+ EditorCommandResult,
22
+ EditorSessionInfo,
23
+ EditorTransport,
24
+ HierarchyNodeInfo,
25
+ HierarchySummary,
26
+ OidSourceEntry,
27
+ ScreenshotResult,
28
+ } from './transport.js';
29
+ export {
30
+ EDITOR_COMMAND_TIMEOUT_MS,
31
+ EDITOR_NOT_RUNNING_ERROR,
32
+ EDITOR_PROBE_TIMEOUT_MS,
33
+ EDITOR_READ_TIMEOUT_MS,
34
+ EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
35
+ EditorTimeoutError,
36
+ getTransport,
37
+ HttpEditorTransport,
38
+ resolveEditorSession,
39
+ withTimeout,
40
+ } from './transport.js';
41
+
42
+ import type { OperationRegistry } from '../registry.js';
43
+ import { registerCameraOperations } from './camera-operations.js';
44
+ import { registerConsoleOperations } from './console-operations.js';
45
+ import { registerHierarchyOperations } from './hierarchy-operations.js';
46
+ import { registerOpenOperations } from './open-operations.js';
47
+ import { registerScreenshotOperations } from './screenshot-operations.js';
48
+ import { registerSelectionOperations } from './selection-operations.js';
49
+ import { registerSessionOperations } from './session-operations.js';
50
+ import { registerSourceLocationOperations } from './source-location-operations.js';
51
+
52
+ /** Register every B3 `editor.*` operation onto `registry`. */
53
+ export function registerEditorOperations(registry: OperationRegistry): void {
54
+ registerSessionOperations(registry);
55
+ registerSelectionOperations(registry);
56
+ registerHierarchyOperations(registry);
57
+ registerCameraOperations(registry);
58
+ registerSourceLocationOperations(registry);
59
+ registerOpenOperations(registry);
60
+ registerScreenshotOperations(registry);
61
+ registerConsoleOperations(registry);
62
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * `editor.scene.open` / `editor.asset.open` / `editor.story.open`
3
+ * (B3, §8 B3 "open scene/asset/story").
4
+ *
5
+ * scene/asset open are genuinely wired end to end: `{type:'open-scene'}` /
6
+ * `{type:'open-asset-tab'}` are existing, already-handled cases in the
7
+ * browser's `handleCommand` switch (`command-listener.ts`).
8
+ *
9
+ * story open remains an HONEST GAP (see `./transport.ts` module jsdoc), and
10
+ * still does after the B3-followup false-ack fix: the editor has no
11
+ * Storybook/CSF story-preview surface at all today (no story runner, no
12
+ * `*.stories.tsx` mount path anywhere in `packages/editor/src`) — there is
13
+ * nothing for a browser-side command handler to open. Sending the command
14
+ * anyway would previously have fallen through `handleCommand`'s switch to a
15
+ * silent `{ok:true}` (a LIE — the editor did nothing); that fallthrough is
16
+ * now a `default:` case returning a structured `{ok:false}` instead, so even
17
+ * an accidental send would fail loudly rather than lie. This op still never
18
+ * sends the command at all, on principle: `HttpEditorTransport.openStory`
19
+ * always returns `undefined` and the op throws the declared
20
+ * `STORY_OPEN_UNSUPPORTED` error. A test transport can implement `openStory`
21
+ * for real, proving the success path/schema round-trips.
22
+ */
23
+
24
+ import { z } from 'zod';
25
+ import { OperationError } from '../errors.js';
26
+ import { defineOperation, type OperationRegistry } from '../registry.js';
27
+ import {
28
+ EDITOR_COMMAND_TIMEOUT_MS,
29
+ EDITOR_NOT_RUNNING_ERROR,
30
+ getTransport,
31
+ resolveEditorSession,
32
+ withTimeout,
33
+ } from './transport.js';
34
+
35
+ const COMMAND_FAILED_ERROR = {
36
+ code: 'COMMAND_FAILED',
37
+ summary: 'The connected editor rejected or failed to execute the relayed open command.',
38
+ data: z.object({ message: z.string() }),
39
+ } as const;
40
+
41
+ const STORY_OPEN_UNSUPPORTED_ERROR = {
42
+ code: 'STORY_OPEN_UNSUPPORTED',
43
+ summary:
44
+ 'The connected editor has no wire-level command to open a CSF story today (a documented ' +
45
+ 'gap — see transport.ts). Never silently reported as success.',
46
+ data: z.object({}),
47
+ } as const;
48
+
49
+ const OkResult = z.object({ ok: z.literal(true) });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // editor.scene.open
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const EditorSceneOpenInput = z.object({
56
+ path: z
57
+ .string()
58
+ .describe('Project-relative path to a .vscn.json scene file to open in the editor.'),
59
+ });
60
+
61
+ export const editorSceneOpen = defineOperation({
62
+ name: 'editor.scene.open',
63
+ summary: 'Open a scene file in a connected editor session.',
64
+ description:
65
+ 'Relays {type:"open-scene", path} through POST /__editor/command — an existing, already-handled case.',
66
+ input: EditorSceneOpenInput,
67
+ result: OkResult,
68
+ errors: [EDITOR_NOT_RUNNING_ERROR, COMMAND_FAILED_ERROR],
69
+ requires: { editor: true },
70
+ host: 'editor-browser',
71
+ mutates: true,
72
+ supportsDryRun: false,
73
+ permission: { risk: 'write', summary: 'Changes which scene the live editor has loaded.' },
74
+ async impl(input, ctx) {
75
+ const transport = getTransport(ctx);
76
+ const session = await resolveEditorSession(ctx, transport);
77
+ const result = await withTimeout(
78
+ transport.sendCommand(
79
+ session,
80
+ { type: 'open-scene', path: input.path },
81
+ EDITOR_COMMAND_TIMEOUT_MS,
82
+ ),
83
+ EDITOR_COMMAND_TIMEOUT_MS,
84
+ 'editor.scene.open',
85
+ ).catch((err: unknown) => ({
86
+ ok: false,
87
+ error: err instanceof Error ? err.message : String(err),
88
+ }));
89
+ if (!result.ok) {
90
+ throw new OperationError('COMMAND_FAILED', result.error ?? 'open-scene command failed', {
91
+ message: result.error ?? 'open-scene command failed',
92
+ });
93
+ }
94
+ return { ok: true as const };
95
+ },
96
+ });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // editor.asset.open
100
+ // ---------------------------------------------------------------------------
101
+
102
+ const AssetKindSchema = z.enum([
103
+ 'model',
104
+ 'image',
105
+ 'audio',
106
+ 'animation',
107
+ 'json',
108
+ 'scene',
109
+ 'prefab',
110
+ 'material',
111
+ ]);
112
+
113
+ const EditorAssetOpenInput = z.object({
114
+ path: z.string().describe('Project-relative asset path to open as an editor tab.'),
115
+ kind: AssetKindSchema,
116
+ });
117
+
118
+ export const editorAssetOpen = defineOperation({
119
+ name: 'editor.asset.open',
120
+ summary: 'Open an asset as a tab in a connected editor session.',
121
+ description:
122
+ 'Relays {type:"open-asset-tab", path, kind} through POST /__editor/command — an existing, already-handled case.',
123
+ input: EditorAssetOpenInput,
124
+ result: OkResult,
125
+ errors: [EDITOR_NOT_RUNNING_ERROR, COMMAND_FAILED_ERROR],
126
+ requires: { editor: true },
127
+ host: 'editor-browser',
128
+ mutates: true,
129
+ supportsDryRun: false,
130
+ permission: { risk: 'write', summary: 'Opens an asset tab in the live editor.' },
131
+ async impl(input, ctx) {
132
+ const transport = getTransport(ctx);
133
+ const session = await resolveEditorSession(ctx, transport);
134
+ const result = await withTimeout(
135
+ transport.sendCommand(
136
+ session,
137
+ { type: 'open-asset-tab', path: input.path, kind: input.kind },
138
+ EDITOR_COMMAND_TIMEOUT_MS,
139
+ ),
140
+ EDITOR_COMMAND_TIMEOUT_MS,
141
+ 'editor.asset.open',
142
+ ).catch((err: unknown) => ({
143
+ ok: false,
144
+ error: err instanceof Error ? err.message : String(err),
145
+ }));
146
+ if (!result.ok) {
147
+ throw new OperationError('COMMAND_FAILED', result.error ?? 'open-asset-tab command failed', {
148
+ message: result.error ?? 'open-asset-tab command failed',
149
+ });
150
+ }
151
+ return { ok: true as const };
152
+ },
153
+ });
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // editor.story.open
157
+ // ---------------------------------------------------------------------------
158
+
159
+ const EditorStoryOpenInput = z.object({
160
+ path: z.string().describe('Project-relative path to a *.stories.tsx/*.stories.ts module.'),
161
+ });
162
+
163
+ export const editorStoryOpen = defineOperation({
164
+ name: 'editor.story.open',
165
+ summary: 'Open a Storybook CSF story in a connected editor session.',
166
+ description:
167
+ 'The live editor has no wire-level command for this today (see module jsdoc) — this always ' +
168
+ 'throws the declared STORY_OPEN_UNSUPPORTED error against the real transport rather than ' +
169
+ 'reporting a fabricated success; a test transport can implement it for real.',
170
+ input: EditorStoryOpenInput,
171
+ result: OkResult,
172
+ errors: [EDITOR_NOT_RUNNING_ERROR, STORY_OPEN_UNSUPPORTED_ERROR],
173
+ requires: { editor: true },
174
+ host: 'editor-browser',
175
+ mutates: true,
176
+ supportsDryRun: false,
177
+ permission: {
178
+ risk: 'write',
179
+ summary: 'Would open a story tab in the live editor, when supported.',
180
+ },
181
+ async impl(input, ctx) {
182
+ const transport = getTransport(ctx);
183
+ const session = await resolveEditorSession(ctx, transport);
184
+ const result = await withTimeout(
185
+ transport.openStory(session, input.path, EDITOR_COMMAND_TIMEOUT_MS),
186
+ EDITOR_COMMAND_TIMEOUT_MS,
187
+ 'editor.story.open',
188
+ ).catch(() => undefined);
189
+ if (!result) {
190
+ throw new OperationError(
191
+ 'STORY_OPEN_UNSUPPORTED',
192
+ 'The connected editor has no wire-level command to open a story today.',
193
+ {},
194
+ );
195
+ }
196
+ if (!result.ok) {
197
+ throw new OperationError('COMMAND_FAILED', result.error ?? 'open-story command failed', {
198
+ message: result.error ?? 'open-story command failed',
199
+ });
200
+ }
201
+ return { ok: true as const };
202
+ },
203
+ });
204
+
205
+ export function registerOpenOperations(registry: OperationRegistry): void {
206
+ registry.register(editorSceneOpen);
207
+ registry.register(editorAssetOpen);
208
+ registry.register(editorStoryOpen);
209
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `editor.viewport.screenshot.capture` (B3, §8 B3 "capture viewport screenshot").
3
+ *
4
+ * B3-followup: wired to a REAL on-demand capture. `HttpEditorTransport.getScreenshot`
5
+ * now tries the `capture-viewport` relay command first — a real, already-handled
6
+ * case in the browser's `command-listener.ts` `handleCommand` switch that renders
7
+ * the LIVE viewport (via `EditorStore.captureViewportImage`, the same offscreen
8
+ * render-target technique the periodic autosave thumbnail already used) and
9
+ * returns fresh PNG bytes through the command-result payload. This only fails to
10
+ * be "fresh" when no browser tab is attached to answer the relay, or the viewport
11
+ * hasn't bound a renderer/scene/camera yet (e.g. immediately after page load) —
12
+ * in that case it falls back to `GET /__editor/project-thumbnail?path=<project>`,
13
+ * the LAST-SAVED thumbnail PNG (`editor-server.ts`'s `/__editor/save-thumbnail`/
14
+ * `/__editor/project-thumbnail` pair). The result schema's `fresh` field names
15
+ * which path answered — never silently presented as equivalent.
16
+ */
17
+
18
+ import { z } from 'zod';
19
+ import { OperationError } from '../errors.js';
20
+ import { defineOperation, type OperationRegistry } from '../registry.js';
21
+ import {
22
+ EDITOR_NOT_RUNNING_ERROR,
23
+ EDITOR_READ_TIMEOUT_MS,
24
+ getTransport,
25
+ resolveEditorSession,
26
+ withTimeout,
27
+ } from './transport.js';
28
+
29
+ const SCREENSHOT_UNAVAILABLE_ERROR = {
30
+ code: 'SCREENSHOT_UNAVAILABLE',
31
+ summary:
32
+ 'Neither a fresh on-demand capture nor a saved thumbnail is available for this session ' +
33
+ "(no browser tab attached to answer the relay, the viewport hasn't bound a renderer/scene/" +
34
+ 'camera yet, and no thumbnail has been saved), or the read timed out.',
35
+ data: z.object({}),
36
+ } as const;
37
+
38
+ const EditorViewportScreenshotCaptureInput = z
39
+ .object({})
40
+ .describe(
41
+ 'No input — captures a fresh viewport image for the target editor session, falling back to ' +
42
+ 'its last-saved thumbnail when a fresh capture is unavailable.',
43
+ );
44
+
45
+ const EditorViewportScreenshotCaptureResult = z
46
+ .object({
47
+ base64: z.string().describe('Base64-encoded image bytes.'),
48
+ mimeType: z.string().describe('Image MIME type.'),
49
+ fresh: z
50
+ .boolean()
51
+ .describe(
52
+ 'True when this is an on-demand capture of the CURRENT viewport (the `capture-viewport` ' +
53
+ 'relay command); false when it is the last-saved thumbnail fallback (the editor UI ' +
54
+ "saves one on its own cadence — this project's most recent one, not necessarily current).",
55
+ ),
56
+ })
57
+ .describe('A viewport image — fresh when possible, the last-saved thumbnail otherwise.');
58
+
59
+ export const editorViewportScreenshotCapture = defineOperation({
60
+ name: 'editor.viewport.screenshot.capture',
61
+ summary: "Capture the target editor session's live viewport (falls back to its saved thumbnail).",
62
+ description:
63
+ 'Relays {type:"capture-viewport"} through POST /__editor/command for a fresh render of the ' +
64
+ 'live viewport; falls back to GET /__editor/project-thumbnail?path=<project> (the last-saved ' +
65
+ 'snapshot) when no browser tab can answer the relay or the viewport has not bound a camera ' +
66
+ 'yet. The result schema names which path answered via `fresh`.',
67
+ input: EditorViewportScreenshotCaptureInput,
68
+ result: EditorViewportScreenshotCaptureResult,
69
+ errors: [EDITOR_NOT_RUNNING_ERROR, SCREENSHOT_UNAVAILABLE_ERROR],
70
+ requires: { editor: true },
71
+ host: 'editor-browser',
72
+ mutates: false,
73
+ supportsDryRun: false,
74
+ permission: {
75
+ risk: 'read',
76
+ summary: "Reads the live editor viewport (render only) or the project's saved thumbnail file.",
77
+ },
78
+ async impl(_input, ctx) {
79
+ const transport = getTransport(ctx);
80
+ const session = await resolveEditorSession(ctx, transport);
81
+ const shot = await withTimeout(
82
+ transport.getScreenshot(session, EDITOR_READ_TIMEOUT_MS),
83
+ EDITOR_READ_TIMEOUT_MS,
84
+ 'editor.viewport.screenshot.capture',
85
+ ).catch(() => undefined);
86
+ if (!shot) {
87
+ throw new OperationError(
88
+ 'SCREENSHOT_UNAVAILABLE',
89
+ 'No fresh capture or saved thumbnail is available for this session, or the read timed out.',
90
+ {},
91
+ );
92
+ }
93
+ return shot;
94
+ },
95
+ });
96
+
97
+ export function registerScreenshotOperations(registry: OperationRegistry): void {
98
+ registry.register(editorViewportScreenshotCapture);
99
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `editor.selection.get` / `editor.selection.set` (B3, §8 B3 "selection read/set").
3
+ *
4
+ * Both genuinely wired end to end against the real editor:
5
+ * - get: `GET /__editor/state`'s `selectedEntityId`/`selectedEntityIds` —
6
+ * always populated by the browser's `collectState` (`command-listener.ts`).
7
+ * - set: `POST /__editor/command` with `{type:'select', id}` or
8
+ * `{type:'select-multiple', ids}` — existing, already-handled cases in
9
+ * the browser's `handleCommand` switch.
10
+ */
11
+
12
+ import { z } from 'zod';
13
+ import { OperationError } from '../errors.js';
14
+ import { defineOperation, type OperationRegistry } from '../registry.js';
15
+ import {
16
+ EDITOR_COMMAND_TIMEOUT_MS,
17
+ EDITOR_NOT_RUNNING_ERROR,
18
+ EDITOR_READ_TIMEOUT_MS,
19
+ getTransport,
20
+ resolveEditorSession,
21
+ withTimeout,
22
+ } from './transport.js';
23
+
24
+ const SELECTION_UNAVAILABLE_ERROR = {
25
+ code: 'SELECTION_UNAVAILABLE',
26
+ summary: 'A session is connected but did not answer the selection-state read in time.',
27
+ data: z.object({}),
28
+ } as const;
29
+
30
+ const COMMAND_FAILED_ERROR = {
31
+ code: 'COMMAND_FAILED',
32
+ summary: 'The connected editor rejected or failed to execute the relayed command.',
33
+ data: z.object({ message: z.string() }),
34
+ } as const;
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // editor.selection.get
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const EditorSelectionGetInput = z
41
+ .object({})
42
+ .describe('No input — reads the current selection from the target editor session.');
43
+
44
+ const EditorSelectionGetResult = z
45
+ .object({
46
+ selectedEntityId: z
47
+ .string()
48
+ .nullable()
49
+ .describe('Primary selected entity id, or null when none.'),
50
+ selectedEntityIds: z.array(z.string()).describe('Every currently selected entity id.'),
51
+ })
52
+ .describe('Live selection snapshot from the resolved editor session.');
53
+
54
+ export const editorSelectionGet = defineOperation({
55
+ name: 'editor.selection.get',
56
+ summary: 'Read the current entity selection from a connected editor session.',
57
+ description:
58
+ 'Resolves a target session (deterministic multi-session rule — see resolveEditorSession) ' +
59
+ "then reads GET /__editor/state's selectedEntityId/selectedEntityIds. Fails " +
60
+ 'EDITOR_NOT_RUNNING (never hangs — bounded by a named timeout) when no session is available.',
61
+ input: EditorSelectionGetInput,
62
+ result: EditorSelectionGetResult,
63
+ errors: [EDITOR_NOT_RUNNING_ERROR, SELECTION_UNAVAILABLE_ERROR],
64
+ requires: { editor: true },
65
+ host: 'editor-browser',
66
+ mutates: false,
67
+ supportsDryRun: false,
68
+ permission: { risk: 'read', summary: 'Reads live editor selection state only.' },
69
+ async impl(_input, ctx) {
70
+ const transport = getTransport(ctx);
71
+ const session = await resolveEditorSession(ctx, transport);
72
+ const selection = await withTimeout(
73
+ transport.getSelection(session, EDITOR_READ_TIMEOUT_MS),
74
+ EDITOR_READ_TIMEOUT_MS,
75
+ 'editor.selection.get',
76
+ ).catch(() => undefined);
77
+ if (!selection) {
78
+ throw new OperationError(
79
+ 'SELECTION_UNAVAILABLE',
80
+ 'The connected editor session did not answer the selection-state read in time.',
81
+ {},
82
+ );
83
+ }
84
+ return selection;
85
+ },
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // editor.selection.set
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const EditorSelectionSetInput = z
93
+ .object({
94
+ ids: z.array(z.string()).describe('Entity ids to select. Empty array clears the selection.'),
95
+ })
96
+ .describe('Set the editor selection to exactly these entity ids.');
97
+
98
+ const EditorSelectionSetResult = z.object({ ok: z.literal(true) });
99
+
100
+ export const editorSelectionSet = defineOperation({
101
+ name: 'editor.selection.set',
102
+ summary: 'Set the entity selection in a connected editor session.',
103
+ description:
104
+ 'Relays {type:"select"} (single id) or {type:"select-multiple"} (0 or many ids) through ' +
105
+ "POST /__editor/command — existing, already-handled cases in the browser's command listener.",
106
+ input: EditorSelectionSetInput,
107
+ result: EditorSelectionSetResult,
108
+ errors: [EDITOR_NOT_RUNNING_ERROR, COMMAND_FAILED_ERROR],
109
+ requires: { editor: true },
110
+ host: 'editor-browser',
111
+ mutates: true,
112
+ supportsDryRun: false,
113
+ permission: {
114
+ risk: 'write',
115
+ summary: 'Changes the live editor selection only (no file/document writes).',
116
+ },
117
+ async impl(input, ctx) {
118
+ const transport = getTransport(ctx);
119
+ const session = await resolveEditorSession(ctx, transport);
120
+ const command =
121
+ input.ids.length === 1
122
+ ? { type: 'select', id: input.ids[0] }
123
+ : { type: 'select-multiple', ids: input.ids };
124
+ const result = await withTimeout(
125
+ transport.sendCommand(session, command, EDITOR_COMMAND_TIMEOUT_MS),
126
+ EDITOR_COMMAND_TIMEOUT_MS,
127
+ 'editor.selection.set',
128
+ ).catch((err: unknown) => ({
129
+ ok: false,
130
+ error: err instanceof Error ? err.message : String(err),
131
+ }));
132
+ if (!result.ok) {
133
+ throw new OperationError('COMMAND_FAILED', result.error ?? 'select command failed', {
134
+ message: result.error ?? 'select command failed',
135
+ });
136
+ }
137
+ return { ok: true as const };
138
+ },
139
+ });
140
+
141
+ export function registerSelectionOperations(registry: OperationRegistry): void {
142
+ registry.register(editorSelectionGet);
143
+ registry.register(editorSelectionSet);
144
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `editor.session.list` (B3, §8 B3 "session discovery").
3
+ *
4
+ * Host is `node`, not `editor-browser` — a deliberate, documented departure
5
+ * from the task brief's "host editor-browser for every op" default. B1's own
6
+ * definition of `editor-browser` (`../types.ts`) is "invoked through the
7
+ * existing POST /__editor/command SSE relay ... which broadcasts to the
8
+ * browser editor and awaits its callback". Session discovery never
9
+ * broadcasts anything to a browser tab — it reads the local session
10
+ * registry file and probes each candidate with a plain `GET
11
+ * /__editor/project` (answered by the Node dev server itself, live browser
12
+ * tab or not). That is Node-process-local work, the same shape as B2's
13
+ * `project.*` discovery ops, so `host: 'node'` is the honest fit; see
14
+ * `../transport.ts`'s module jsdoc for the full transport inventory.
15
+ *
16
+ * Unlike every other `editor.*` op, an EMPTY result is success here (this
17
+ * IS the discovery operation other ops use to decide EDITOR_NOT_RUNNING) —
18
+ * so `requires` is `{}` and there is no EDITOR_NOT_RUNNING error declared.
19
+ */
20
+
21
+ import { z } from 'zod';
22
+ import { defineOperation, type OperationRegistry } from '../registry.js';
23
+ import { EDITOR_SESSION_DISCOVERY_TIMEOUT_MS, getTransport } from './transport.js';
24
+
25
+ const EditorSessionListInput = z
26
+ .object({})
27
+ .describe('No input — lists every live, probe-verified editor session.');
28
+
29
+ const EditorSessionListResult = z.object({
30
+ sessions: z
31
+ .array(
32
+ z.object({
33
+ port: z.number().describe('Port the dev server is bound to.'),
34
+ project: z.string().nullable().describe('Canonical project root currently open, or null.'),
35
+ pid: z
36
+ .number()
37
+ .nullable()
38
+ .describe('Dev-server process id, when known from the local session registry.'),
39
+ }),
40
+ )
41
+ .describe('Every live session that answered a liveness probe, in no particular order.'),
42
+ });
43
+
44
+ export const editorSessionList = defineOperation({
45
+ name: 'editor.session.list',
46
+ summary: 'List every live, probe-verified editor dev-server session on this machine.',
47
+ description:
48
+ 'Reads the local session registry (~/.vgai/editor-sessions.json, the format contract owned ' +
49
+ 'by packages/editor/server/session-registry.ts), PID-liveness-filters it, then probe-' +
50
+ 'verifies each survivor with GET /__editor/project (bounded by a named timeout — see ' +
51
+ 'transport.ts). This is the SAME discovery every other editor.* op uses internally to pick ' +
52
+ "a target session (see resolveEditorSession's deterministic rule).",
53
+ input: EditorSessionListInput,
54
+ result: EditorSessionListResult,
55
+ errors: [],
56
+ requires: {},
57
+ host: 'node',
58
+ mutates: false,
59
+ supportsDryRun: false,
60
+ permission: {
61
+ risk: 'read',
62
+ summary: 'Reads the local session registry file + probes localhost ports; no writes.',
63
+ },
64
+ async impl(_input, ctx) {
65
+ const transport = getTransport(ctx);
66
+ const sessions = await transport.listSessions(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS);
67
+ return { sessions };
68
+ },
69
+ });
70
+
71
+ export function registerSessionOperations(registry: OperationRegistry): void {
72
+ registry.register(editorSessionList);
73
+ }