@vgai/live 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/editor.d.ts +78 -55
  3. package/dist/editor.js +129 -76
  4. package/dist/game-client/bridge-heartbeat.d.ts +49 -0
  5. package/dist/game-client/bridge-heartbeat.js +46 -0
  6. package/dist/game-client/bridge-transport.d.ts +75 -0
  7. package/dist/game-client/bridge-transport.js +19 -0
  8. package/dist/game-client/client.d.ts +293 -0
  9. package/dist/game-client/client.js +706 -0
  10. package/dist/game-client/errors.d.ts +57 -0
  11. package/dist/game-client/errors.js +76 -0
  12. package/dist/game-client/events-matcher.d.ts +41 -0
  13. package/dist/game-client/events-matcher.js +68 -0
  14. package/dist/game-client/failure-block.d.ts +93 -0
  15. package/dist/game-client/failure-block.js +97 -0
  16. package/dist/game-client/fast-forward.d.ts +125 -0
  17. package/dist/game-client/fast-forward.js +122 -0
  18. package/dist/game-client/hidden-recovery.d.ts +85 -0
  19. package/dist/game-client/hidden-recovery.js +105 -0
  20. package/dist/game-client/index.d.ts +40 -0
  21. package/dist/game-client/index.js +26 -0
  22. package/dist/game-client/perf-sampling.d.ts +56 -0
  23. package/dist/game-client/perf-sampling.js +85 -0
  24. package/dist/game-client/relay-transport.d.ts +100 -0
  25. package/dist/game-client/relay-transport.js +237 -0
  26. package/dist/game-client/screenshot-target.d.ts +60 -0
  27. package/dist/game-client/screenshot-target.js +68 -0
  28. package/dist/game-client/state-cap.d.ts +7 -0
  29. package/dist/game-client/state-cap.js +21 -0
  30. package/dist/game-client/types.d.ts +128 -0
  31. package/dist/game-client/types.js +15 -0
  32. package/dist/game-client/wait-for.d.ts +155 -0
  33. package/dist/game-client/wait-for.js +229 -0
  34. package/dist/game.d.ts +47 -18
  35. package/dist/game.js +59 -16
  36. package/dist/index.d.ts +46 -21
  37. package/dist/index.js +51 -20
  38. package/dist/session.d.ts +4 -4
  39. package/dist/session.js +7 -7
  40. package/dist/tools.d.ts +12 -3
  41. package/dist/tools.js +15 -6
  42. package/package.json +10 -5
  43. package/src/editor.ts +142 -96
  44. package/src/game-client/bridge-heartbeat.ts +61 -0
  45. package/src/game-client/bridge-transport.ts +73 -0
  46. package/src/game-client/client.ts +836 -0
  47. package/src/game-client/errors.ts +96 -0
  48. package/src/game-client/events-matcher.ts +106 -0
  49. package/src/game-client/failure-block.ts +199 -0
  50. package/src/game-client/fast-forward.ts +175 -0
  51. package/src/game-client/hidden-recovery.ts +149 -0
  52. package/src/game-client/index.ts +98 -0
  53. package/src/game-client/perf-sampling.ts +94 -0
  54. package/src/game-client/relay-transport.ts +311 -0
  55. package/src/game-client/screenshot-target.ts +91 -0
  56. package/src/game-client/state-cap.ts +29 -0
  57. package/src/game-client/types.ts +137 -0
  58. package/src/game-client/wait-for.ts +327 -0
  59. package/src/game.ts +96 -16
  60. package/src/index.ts +68 -31
  61. package/src/session.ts +8 -10
  62. package/src/tools.ts +19 -6
package/src/game.ts CHANGED
@@ -1,36 +1,74 @@
1
1
  /**
2
2
  * `game` — the game-control half of `@vgai/live`'s `{ editor, game, page }`.
3
- * A `GameClient` (`@vgai/e2e`) bound to a `RelayTransport` on the resolved
3
+ * A `GameClient` (`./game-client/`) bound to a `RelayTransport` on the resolved
4
4
  * session's port — the SAME session-wire relay (`POST /__editor/command`,
5
5
  * `bridge-call`/`bridge-screenshot`/`page-script`) every `EditorClient`
6
6
  * method already uses. Never forked: this module only WIRES `GameClient` up,
7
7
  * it does not reimplement any of its methods (`state`/`waitFor`/`events`/
8
8
  * `input.hold`/`input.tap`/`screenshot`/`command`/`page` — whatever
9
9
  * `GameClient` exposes is exposed here, unchanged).
10
+ *
11
+ * When the editor has MORE THAN ONE instance mounted (multiplayer authoring —
12
+ * see the engine's play-mode `mountAdditionalInstance`), `game` alone REFUSES
13
+ * to guess which one a call addresses. `game.instances()` / `game.instance(id)`
14
+ * are the way past that: each returns a `GameClient` whose relay carries a
15
+ * specific mount id (`RelayTransport({ instance })`), so one script can drive
16
+ * several instances — two seats of a match, or one scene under two seeds —
17
+ * without ambiguity.
10
18
  */
11
19
 
12
- import { GameClient, RelayTransport } from '@vgai/e2e';
20
+ import { GameClient, RelayTransport } from './game-client/index.js';
21
+
22
+ /** A `GameClient` addressing ONE mounted instance, tagged with the mount `id`
23
+ * it drives. `id` is the whole point of the handle to a CALLER: it is what
24
+ * `tools.run(name, args, { instance: handle.id })` passes to scope a
25
+ * node-hosted tool (e.g. `project.autoplay`) to this seat — the two-seat
26
+ * multiplayer verification pattern. Without it the caller has a client it can
27
+ * drive but no id to hand a tool, so the seat-scoped path silently degrades to
28
+ * the sole-instance one. */
29
+ export type AddressedGameClient = GameClient & { readonly id: string };
30
+
31
+ /** `game` plus the instance-addressing surface (see this module's doc). The
32
+ * base `GameClient` methods are unchanged; a bare call still targets the sole
33
+ * instance and refuses when several are live. */
34
+ export interface LiveGame extends GameClient {
35
+ /**
36
+ * A handle addressing ONE mounted instance by its mount id — a `GameClient`
37
+ * whose every relay call carries `instance: id`, and which exposes that `id`
38
+ * back (`handle.id === id`). Synchronous: it builds a client, it does not
39
+ * contact the editor. (Through the bare top-level `game` singleton it
40
+ * resolves a session first and so returns a promise — the same proxy
41
+ * limitation the singleton documents; the `connect()`/`vgai eval` form is
42
+ * synchronous.)
43
+ */
44
+ instance(id: string): AddressedGameClient;
45
+ /**
46
+ * Handles for every instance currently mounted, in mount order — each tagged
47
+ * with its own `id`. One round trip (`list-instances`) to enumerate, then one
48
+ * addressed `GameClient` per id. `[]` when nothing is mounted (not playing) —
49
+ * the honest answer.
50
+ */
51
+ instances(): Promise<AddressedGameClient[]>;
52
+ }
13
53
 
14
54
  /**
15
- * Builds a `GameClient` over a `RelayTransport({ port })`.
55
+ * Build a `GameClient` on `port`, optionally addressing one mounted instance.
16
56
  *
17
57
  * Fence values (`fenceTick`/`fenceSeq`/`fenceSimSeconds`/`fenceWallMs`) are
18
58
  * placeholders — tick 0, seq 0, sim-seconds 0, "now" — rather than a real
19
- * play-mode-start snapshot. `@vgai/e2e`'s own relay fixture
20
- * (`relay-fixture.ts`'s `waitForRelayBridge`) waits for a FRESH play-mode
21
- * boot before constructing its `game` client, because a test always starts
22
- * from a known instant. A `connect()`ed live session has no such instant
23
- * it may attach to an ALREADY-RUNNING game session so there is no single
24
- * "test start" to fence from. Practical effect: `game.events.expect(...)`
25
- * sees the WHOLE event history since play mode started (not a per-call
26
- * window); every other method (`state`/`waitFor`/`input`/`command`/
27
- * `screenshot`) is unaffected by the fence. `pageErrors`/`consoleErrors` are
28
- * always empty — relay mode has no separate page handle to listen on (the
29
- * same documented gap `relay-fixture.ts` carries).
59
+ * play-mode-start snapshot. A caller that owns its own browser can fence off
60
+ * a FRESH play-mode boot, because it starts from a known instant. A
61
+ * `connect()`ed live session has no such instant it may attach to an
62
+ * ALREADY-RUNNING game session so there is no single "run start" to fence
63
+ * from. Practical effect: `game.events.expect(...)` sees the WHOLE event
64
+ * history since play mode started (not a per-call window); every other method
65
+ * (`state`/`waitFor`/`input`/`command`/`screenshot`) is unaffected by the
66
+ * fence. `pageErrors`/`consoleErrors` are always empty — relay mode has no
67
+ * separate page handle to listen on.
30
68
  */
31
- export function createGameClient(port: number, artifactsDir?: string): GameClient {
69
+ function gameClientFor(port: number, artifactsDir?: string, instance?: string): GameClient {
32
70
  return new GameClient({
33
- transport: new RelayTransport({ port }),
71
+ transport: new RelayTransport(instance === undefined ? { port } : { port, instance }),
34
72
  pageErrors: [],
35
73
  consoleErrors: [],
36
74
  fenceTick: 0,
@@ -41,3 +79,45 @@ export function createGameClient(port: number, artifactsDir?: string): GameClien
41
79
  artifactsDir,
42
80
  });
43
81
  }
82
+
83
+ /** The unaddressed `game` client — targets the sole live instance and refuses
84
+ * when several are mounted. Kept as a named export for callers/tests that
85
+ * want just the base client. */
86
+ export function createGameClient(port: number, artifactsDir?: string): GameClient {
87
+ return gameClientFor(port, artifactsDir);
88
+ }
89
+
90
+ /** Query the editor's live instance ids over the session wire.
91
+ *
92
+ * `list-instances` is a session-level command (`command-listener.ts`), NOT a
93
+ * `bridge-call` method: it answers "which instances exist", and the
94
+ * bridge-call path RESOLVES an instance (refusing when several are live), so
95
+ * routing this through it would hit the exact ambiguity it throws on. The
96
+ * response spreads `data` at the top level, so the ids arrive as
97
+ * `body.instances`. */
98
+ async function listInstanceIds(port: number): Promise<string[]> {
99
+ const res = await fetch(`http://127.0.0.1:${port}/__editor/command`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ type: 'list-instances' }),
103
+ });
104
+ const body = (await res.json()) as { ok?: boolean; instances?: unknown; error?: string };
105
+ if (!body.ok || !Array.isArray(body.instances)) {
106
+ throw new Error(`@vgai/live: list-instances failed — ${body.error ?? 'no instances in reply'}`);
107
+ }
108
+ return body.instances.map(String);
109
+ }
110
+
111
+ /** Build the `LiveGame` — the base `game` client plus its instance-addressing
112
+ * surface. */
113
+ export function createLiveGame(port: number, artifactsDir?: string): LiveGame {
114
+ const base = gameClientFor(port, artifactsDir);
115
+ // Tag each addressed handle with the mount id it drives, so a caller can pass
116
+ // `handle.id` to `tools.run(..., { instance })`. The id is already known here
117
+ // (it is what parameterizes the relay); attaching it just hands it back.
118
+ const instance = (id: string): AddressedGameClient =>
119
+ Object.assign(gameClientFor(port, artifactsDir, id), { id });
120
+ const instances = async (): Promise<AddressedGameClient[]> =>
121
+ (await listInstanceIds(port)).map(instance);
122
+ return Object.assign(base, { instance, instances });
123
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
- * `@vgai/live` — `{ editor, game, page, tools }` over the session wire
3
- * (docs/SHARED-SESSION-SPEC.md, Wave 2). Editor control consolidates HERE
4
- * from the ~19 `vgai` CLI verbs: a plain node/tsx script gets
5
- * `import { editor, game, page } from '@vgai/live'` instead of shelling out
6
- * to the CLI — semantics are discoverable from THIS module's types, not from
7
- * CLI usage text.
2
+ * `@vgai/live` — `{ editor, game, page, tools }` over the session wire.
3
+ * Editor control consolidates HERE from the ~19 `vgai` CLI verbs: a plain
4
+ * node/tsx script gets `import { editor, game, page } from '@vgai/live'`
5
+ * instead of shelling out to the CLI semantics are discoverable from THIS
6
+ * module's types, not from CLI usage text.
8
7
  *
9
8
  * `@vgai/live` never starts or stops a session — the CLI keeps that
10
9
  * (`create`/`edit`/`sessions`/`close`). `connect()` (and the lazy
@@ -30,25 +29,24 @@
30
29
  *
31
30
  * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
32
31
  * PR #166) — write `step` as a literal `async (page) => {...}` and inline
33
- * every value it needs. KNOWN WIRE LIMITATION: under the relay transport
34
- * this binds to (the same one `vgai e2e --in-editor` uses), `step` is
35
- * shipped to the editor dev server as `step.toString()` and reconstructed
36
- * there closures over outer variables do NOT survive that trip. See
37
- * `@vgai/e2e`'s `bridge-transport.ts` (`runPageScript`'s doc comment) and
38
- * `relay-transport.ts` for the full honesty-boundary contract this wraps.
32
+ * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
33
+ * binds to, `step` is shipped to the editor dev server as `step.toString()`
34
+ * and reconstructed there closures over outer variables do NOT survive that
35
+ * trip. See `game-client/bridge-transport.ts` (`runPageScript`'s doc comment)
36
+ * and `game-client/relay-transport.ts` for the full honesty-boundary contract
37
+ * this wraps.
39
38
  */
40
39
 
41
40
  import { join } from 'node:path';
42
- import type { GameClient } from '@vgai/e2e';
43
41
  import { EditorClient } from '@vgai/editor-sdk';
44
42
  import { LiveEditor } from './editor.js';
45
- import { createGameClient } from './game.js';
43
+ import { createLiveGame, type LiveGame } from './game.js';
44
+ import type { GameClient } from './game-client/index.js';
46
45
  import { lazyChainProxy } from './lazy-proxy.js';
47
46
  import { type ResolvedSession, resolveSession, type SessionResolutionDeps } from './session.js';
48
47
  import { createLazySession } from './singleton.js';
49
48
  import { LiveTools } from './tools.js';
50
49
 
51
- export type { GameClient } from '@vgai/e2e';
52
50
  export type {
53
51
  ActiveDocumentCapture,
54
52
  EditorClient,
@@ -56,9 +54,10 @@ export type {
56
54
  EditorViewDocument,
57
55
  PresentedEditorView,
58
56
  } from '@vgai/editor-sdk';
59
- export type { ApplyDiffInput, ApplyDiffResult, PanelName } from './editor.js';
57
+ export type { PanelName } from './editor.js';
60
58
  export { inferAssetKind, LiveEditor } from './editor.js';
61
- export { createGameClient } from './game.js';
59
+ export { createGameClient, createLiveGame, type LiveGame } from './game.js';
60
+ export * from './game-client/index.js';
62
61
  export type {
63
62
  ProjectSessionHint,
64
63
  ResolvedSession,
@@ -73,17 +72,63 @@ export { LiveTools } from './tools.js';
73
72
  /** A `game.page(step)`-shaped call — see this module's doc comment for the closure-capture limitation. */
74
73
  export type PageStep = GameClient['page'];
75
74
 
76
- export interface LiveSession {
75
+ export interface LiveSession extends LiveBindings {
76
+ /** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
77
+ session: ResolvedSession;
78
+ }
79
+
80
+ /** The callable half of a session: everything `vgai eval` binds except `session` itself. */
81
+ export interface LiveBindings {
77
82
  editor: LiveEditor;
78
- game: GameClient;
83
+ /** The game client, plus `instances()`/`instance(id)` for addressing one of
84
+ * several mounted instances (multiplayer authoring). A bare `game` call
85
+ * targets the sole instance and refuses when several are live. */
86
+ game: LiveGame;
79
87
  /** `GameClient.page` bound to `game` — see this module's own doc comment for the wire limitation. */
80
88
  page: PageStep;
81
89
  /** Registered project callables: enumerate, inspect, and invoke. */
82
90
  tools: LiveTools;
83
- /** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
84
- session: ResolvedSession;
85
91
  }
86
92
 
93
+ /**
94
+ * Construct the binding graph for an editor on `port`. Pure — every object
95
+ * here is built from a URL/port and contacts nothing until a method is
96
+ * awaited, which is what lets `unconnectedBindings()` below reuse it.
97
+ */
98
+ function bindTo(port: number, projectRoot: string): LiveBindings {
99
+ const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
100
+ const editor = new LiveEditor(client);
101
+ const game = createLiveGame(port, join(projectRoot, '.vgai', 'last-run'));
102
+ const page: PageStep = (step) => game.page(step);
103
+ const tools = new LiveTools(client);
104
+ return { editor, game, page, tools };
105
+ }
106
+
107
+ /**
108
+ * The SAME `{ editor, game, page, tools }` objects `connect()` returns —
109
+ * real classes, real instance fields — bound to a port nothing listens on.
110
+ *
111
+ * This exists for INTROSPECTION, and specifically for `vgai eval --list`
112
+ * (`packages/vgai-cli/src/eval-surface.ts`), which must answer "what could I
113
+ * call?" before `vgai edit` is running. Reading a CLASS instead is what
114
+ * shipped first, and it silently omitted `game.input` and `game.events`:
115
+ * those are instance fields the constructor assigns, so they exist on no
116
+ * prototype and a class walk cannot see them — an agent reading the listing
117
+ * concluded there was no honest input door. Objects, not classes, is the fix,
118
+ * and going through `bindTo` is what keeps it honest: the probe is assembled
119
+ * by the same function `connect()` uses, so a binding added there is listed
120
+ * here with no second place to remember.
121
+ *
122
+ * Awaiting any method on these objects will fail (connection refused). Use
123
+ * `connect()` for that.
124
+ */
125
+ export function unconnectedBindings(): LiveBindings {
126
+ return bindTo(UNCONNECTED_PORT, process.cwd());
127
+ }
128
+
129
+ /** Port 0 is never a listening editor — an accidental call fails loudly rather than reaching a stranger's server. */
130
+ const UNCONNECTED_PORT = 0;
131
+
87
132
  /**
88
133
  * Resolve `projectDir` (default `process.cwd()`) to its live `vgai edit`
89
134
  * session and bind `{ editor, game, page }` to it. `deps` is an advanced/
@@ -95,15 +140,7 @@ export async function connect(
95
140
  deps?: SessionResolutionDeps,
96
141
  ): Promise<LiveSession> {
97
142
  const resolved = await resolveSession(projectDir, deps);
98
- const client = new EditorClient({ url: `http://localhost:${resolved.port}` });
99
- const editor = new LiveEditor(client, resolved.projectRoot);
100
- const game = createGameClient(
101
- resolved.port,
102
- join(resolved.projectRoot, '.vgai', 'e2e', 'last-run'),
103
- );
104
- const page: PageStep = (step) => game.page(step);
105
- const tools = new LiveTools(client);
106
- return { editor, game, page, tools, session: resolved };
143
+ return { ...bindTo(resolved.port, resolved.projectRoot), session: resolved };
107
144
  }
108
145
 
109
146
  // ---------------------------------------------------------------------------
@@ -123,7 +160,7 @@ export function __resetLiveSingletonForTests(): void {
123
160
  export const editor: LiveEditor = lazyChainProxy<LiveEditor>(() =>
124
161
  lazySession.ensure().then((s) => s.editor),
125
162
  );
126
- export const game: GameClient = lazyChainProxy<GameClient>(() =>
163
+ export const game: LiveGame = lazyChainProxy<LiveGame>(() =>
127
164
  lazySession.ensure().then((s) => s.game),
128
165
  );
129
166
  export const page: PageStep = lazyChainProxy<PageStep>(() =>
package/src/session.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  * 3. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
24
24
  * imported) from `packages/vgai-cli/src/project-root.ts`. `@vgai/sdk`'s
25
25
  * `resolveEditorSession` takes an already-known `ctx.projectRoot`; it has
26
- * no "walk up from an arbitrary cwd to find the nearest vgai.game.json"
26
+ * no "walk up from an arbitrary cwd to find the nearest vgai.project.json"
27
27
  * step of its own, which `connect(projectDir?)` needs. `@vgai/cli` is
28
28
  * `"private": true` with no `exports` field to import through (and
29
29
  * depending on it from here would invert the intended CLI -> SDK ->
@@ -58,13 +58,13 @@ import {
58
58
  /**
59
59
  * Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
60
60
  * module's doc comment above for why it's duplicated rather than imported).
61
- * Nearest ancestor of `dir` (inclusive) containing a `vgai.game.json`, or
61
+ * Nearest ancestor of `dir` (inclusive) containing a `vgai.project.json`, or
62
62
  * `null` if none.
63
63
  */
64
64
  export function findProjectRootFrom(dir: string): string | null {
65
65
  let cur = resolve(dir);
66
66
  for (;;) {
67
- if (existsSync(join(cur, 'vgai.game.json'))) return cur;
67
+ if (existsSync(join(cur, 'vgai.project.json'))) return cur;
68
68
  const parent = dirname(cur);
69
69
  if (parent === cur) return null;
70
70
  cur = parent;
@@ -77,7 +77,7 @@ export type SessionListingTransport = Pick<EditorTransport, 'listSessions'>;
77
77
  export interface ResolvedSession {
78
78
  /** Editor dev-server port the resolved session is listening on. */
79
79
  port: number;
80
- /** Absolute project root — the nearest ancestor of the requested directory containing `vgai.game.json`. */
80
+ /** Absolute project root — the nearest ancestor of the requested directory containing `vgai.project.json`. */
81
81
  projectRoot: string;
82
82
  }
83
83
 
@@ -135,7 +135,7 @@ async function verifyProjectSession(
135
135
  projectRoot: string,
136
136
  ): Promise<boolean> {
137
137
  try {
138
- const response = await fetch(`http://localhost:${hint.port}/__editor/project`, {
138
+ const response = await fetch(`http://127.0.0.1:${hint.port}/__editor/project`, {
139
139
  signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
140
140
  });
141
141
  if (!response.ok) return false;
@@ -154,7 +154,7 @@ async function verifyProjectSession(
154
154
  * running `vgai edit` session. Throws a descriptive error (never hangs
155
155
  * indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
156
156
  * silently attaches to an unrelated project's session — see the module doc
157
- * above) when no vgai.game.json is found, or no live session covers it.
157
+ * above) when no vgai.project.json is found, or no live session covers it.
158
158
  */
159
159
  export async function resolveSession(
160
160
  projectDir: string = process.cwd(),
@@ -166,7 +166,7 @@ export async function resolveSession(
166
166
  const projectRoot = findRoot(projectDir);
167
167
  if (projectRoot === null) {
168
168
  throw new Error(
169
- `@vgai/live: no vgai.game.json found in ${projectDir} or any parent directory — is this a vgai project?`,
169
+ `@vgai/live: no vgai.project.json found in ${projectDir} or any parent directory — is this a vgai project?`,
170
170
  );
171
171
  }
172
172
 
@@ -195,9 +195,7 @@ export async function resolveSession(
195
195
  }
196
196
 
197
197
  const canon = canonicalPath(projectRoot);
198
- const match = sessions.find(
199
- (s) => s.kind !== 'e2e' && s.project !== null && canonicalPath(s.project) === canon,
200
- );
198
+ const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
201
199
  if (!match) {
202
200
  const otherCount = sessions.length;
203
201
  throw new Error(
package/src/tools.ts CHANGED
@@ -8,11 +8,16 @@ import type {
8
8
  } from '@vgai/editor-sdk';
9
9
 
10
10
  export class LiveTools {
11
- constructor(private readonly client: EditorClient) {}
11
+ /** `#`-private for the same reason `LiveEditor.#client` is. */
12
+ readonly #client: EditorClient;
13
+
14
+ constructor(client: EditorClient) {
15
+ this.#client = client;
16
+ }
12
17
 
13
18
  /** Enumerate the exact `package.json#vgai.tools` catalog without executing it. */
14
19
  async list(): Promise<ProjectToolCatalog> {
15
- return this.client.listProjectTools();
20
+ return this.#client.listProjectTools();
16
21
  }
17
22
 
18
23
  /** Return one tool's discoverable metadata, or `null` when it is not registered. */
@@ -21,12 +26,20 @@ export class LiveTools {
21
26
  return catalog.tools.find((tool) => tool.name === name) ?? null;
22
27
  }
23
28
 
24
- /** Invoke the same validated callable used by the editor and CLI. */
25
- async call(
29
+ /**
30
+ * Invoke the same validated callable used by the editor and CLI.
31
+ *
32
+ * `instance` names WHICH mounted instance the tool should drive when several
33
+ * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
34
+ * and a tool that drives the game (e.g. `project.autoplay`) binds
35
+ * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
36
+ * the tool then targets the sole live instance, exactly as before.
37
+ */
38
+ async run(
26
39
  name: string,
27
40
  input: unknown = {},
28
- options: { confirm?: boolean } = {},
41
+ options: { confirm?: boolean; instance?: string } = {},
29
42
  ): Promise<ProjectToolOutcome> {
30
- return this.client.runProjectTool(name, input, options);
43
+ return this.#client.runProjectTool(name, input, options);
31
44
  }
32
45
  }