@vgai/live 0.4.1 → 0.5.0-canary.20260719.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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/live",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.4.1",
5
+ "version": "0.5.0-canary.20260719.0",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -30,9 +30,9 @@
30
30
  "build": "tsc -p tsconfig.build.json"
31
31
  },
32
32
  "dependencies": {
33
- "@vgai/editor-sdk": "0.4.1",
34
- "@vgai/probe": "0.4.1",
35
- "@vgai/sdk": "0.4.1"
33
+ "@vgai/editor-sdk": "0.5.0-canary.20260719.0",
34
+ "@vgai/e2e": "0.5.0-canary.20260719.0",
35
+ "@vgai/sdk": "0.5.0-canary.20260719.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@playwright/test": ">=1.58.2 <2"
package/src/editor.ts CHANGED
@@ -13,11 +13,14 @@
13
13
  */
14
14
 
15
15
  import type {
16
+ ActiveDocumentCapture,
16
17
  AssetKind,
17
18
  AssetPreviewCapture,
18
19
  AssetPreviewOptions,
19
20
  EditorClient,
20
21
  EditorState,
22
+ EditorView,
23
+ PresentedEditorView,
21
24
  ShadingMode,
22
25
  ViewPreset,
23
26
  ViewportCapture,
@@ -99,6 +102,25 @@ export class LiveEditor {
99
102
  return state.savePath;
100
103
  }
101
104
 
105
+ /**
106
+ * Make the connected human editor show the same subject/view as the agent.
107
+ * The returned URL is a compact, shareable projection — not a serialized
108
+ * workspace or document payload.
109
+ */
110
+ async present(view: EditorView): Promise<PresentedEditorView> {
111
+ return this.client.present(view);
112
+ }
113
+
114
+ /** The human editor's actual active document, selection, camera and utility. */
115
+ async currentView(): Promise<EditorView> {
116
+ return this.client.currentView();
117
+ }
118
+
119
+ /** Capture the same center document the human is currently looking at. */
120
+ async captureActiveDocument(size?: number): Promise<ActiveDocumentCapture> {
121
+ return this.client.captureActiveDocument(size);
122
+ }
123
+
102
124
  /**
103
125
  * FILE mode — writes the scene file directly. NOT a live wire command:
104
126
  * there is no live-editor wire op for this yet (`vgai apply-diff`'s own
package/src/game.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `game` — the game-control half of `@vgai/live`'s `{ editor, game, page }`.
3
- * A `GameClient` (`@vgai/probe`) bound to a `RelayTransport` on the resolved
3
+ * A `GameClient` (`@vgai/e2e`) 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,
@@ -9,14 +9,14 @@
9
9
  * `GameClient` exposes is exposed here, unchanged).
10
10
  */
11
11
 
12
- import { GameClient, RelayTransport } from '@vgai/probe';
12
+ import { GameClient, RelayTransport } from '@vgai/e2e';
13
13
 
14
14
  /**
15
15
  * Builds a `GameClient` over a `RelayTransport({ port })`.
16
16
  *
17
17
  * Fence values (`fenceTick`/`fenceSeq`/`fenceSimSeconds`/`fenceWallMs`) are
18
18
  * placeholders — tick 0, seq 0, sim-seconds 0, "now" — rather than a real
19
- * play-mode-start snapshot. `@vgai/probe`'s own relay fixture
19
+ * play-mode-start snapshot. `@vgai/e2e`'s own relay fixture
20
20
  * (`relay-fixture.ts`'s `waitForRelayBridge`) waits for a FRESH play-mode
21
21
  * boot before constructing its `game` client, because a test always starts
22
22
  * from a known instant. A `connect()`ed live session has no such instant —
@@ -28,7 +28,7 @@ import { GameClient, RelayTransport } from '@vgai/probe';
28
28
  * always empty — relay mode has no separate page handle to listen on (the
29
29
  * same documented gap `relay-fixture.ts` carries).
30
30
  */
31
- export function createGameClient(port: number): GameClient {
31
+ export function createGameClient(port: number, artifactsDir?: string): GameClient {
32
32
  return new GameClient({
33
33
  transport: new RelayTransport({ port }),
34
34
  pageErrors: [],
@@ -38,5 +38,6 @@ export function createGameClient(port: number): GameClient {
38
38
  fenceSimSeconds: 0,
39
39
  fenceWallMs: Date.now(),
40
40
  warmSession: false,
41
+ artifactsDir,
41
42
  });
42
43
  }
package/src/index.ts CHANGED
@@ -31,15 +31,16 @@
31
31
  * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
32
32
  * PR #166) — write `step` as a literal `async (page) => {...}` and inline
33
33
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport
34
- * this binds to (the same one `vgai probe --in-editor` uses), `step` is
34
+ * this binds to (the same one `vgai e2e --in-editor` uses), `step` is
35
35
  * shipped to the editor dev server as `step.toString()` and reconstructed
36
36
  * there — closures over outer variables do NOT survive that trip. See
37
- * `@vgai/probe`'s `bridge-transport.ts` (`runPageScript`'s doc comment) and
37
+ * `@vgai/e2e`'s `bridge-transport.ts` (`runPageScript`'s doc comment) and
38
38
  * `relay-transport.ts` for the full honesty-boundary contract this wraps.
39
39
  */
40
40
 
41
+ import { join } from 'node:path';
42
+ import type { GameClient } from '@vgai/e2e';
41
43
  import { EditorClient } from '@vgai/editor-sdk';
42
- import type { GameClient } from '@vgai/probe';
43
44
  import { LiveEditor } from './editor.js';
44
45
  import { createGameClient } from './game.js';
45
46
  import { lazyChainProxy } from './lazy-proxy.js';
@@ -47,12 +48,19 @@ import { type ResolvedSession, resolveSession, type SessionResolutionDeps } from
47
48
  import { createLazySession } from './singleton.js';
48
49
  import { LiveTools } from './tools.js';
49
50
 
50
- export type { EditorClient } from '@vgai/editor-sdk';
51
- export type { GameClient } from '@vgai/probe';
51
+ export type { GameClient } from '@vgai/e2e';
52
+ export type {
53
+ ActiveDocumentCapture,
54
+ EditorClient,
55
+ EditorView,
56
+ EditorViewDocument,
57
+ PresentedEditorView,
58
+ } from '@vgai/editor-sdk';
52
59
  export type { ApplyDiffInput, ApplyDiffResult, PanelName } from './editor.js';
53
60
  export { inferAssetKind, LiveEditor } from './editor.js';
54
61
  export { createGameClient } from './game.js';
55
62
  export type {
63
+ ProjectSessionHint,
56
64
  ResolvedSession,
57
65
  SessionListingTransport,
58
66
  SessionResolutionDeps,
@@ -89,7 +97,10 @@ export async function connect(
89
97
  const resolved = await resolveSession(projectDir, deps);
90
98
  const client = new EditorClient({ url: `http://localhost:${resolved.port}` });
91
99
  const editor = new LiveEditor(client, resolved.projectRoot);
92
- const game = createGameClient(resolved.port);
100
+ const game = createGameClient(
101
+ resolved.port,
102
+ join(resolved.projectRoot, '.vgai', 'e2e', 'last-run'),
103
+ );
93
104
  const page: PageStep = (step) => game.page(step);
94
105
  const tools = new LiveTools(client);
95
106
  return { editor, game, page, tools, session: resolved };
package/src/session.ts CHANGED
@@ -4,9 +4,13 @@
4
4
  * session. `@vgai/live` never starts or stops a session (see the module doc
5
5
  * on `index.ts`); this only finds one.
6
6
  *
7
- * Two pieces, from two different sources:
7
+ * Three pieces, from two different sources:
8
8
  *
9
- * 1. Live-session DISCOVERY + PROBING (`HttpEditorTransport.listSessions`,
9
+ * 1. Exact-project discovery through `.vgai/session.json`, written by the
10
+ * editor inside the project and verified against `/__editor/project`.
11
+ * This is the fast path and cannot be delayed by unrelated sessions.
12
+ *
13
+ * 2. Global live-session DISCOVERY + PROBING (`HttpEditorTransport.listSessions`,
10
14
  * `withTimeout`, `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`) — reused
11
15
  * directly from `@vgai/sdk`'s `editor/transport.ts` (B3,
12
16
  * `resolveEditorSession`'s own sibling machinery). That module is a
@@ -16,7 +20,7 @@
16
20
  * exactly the hard, easy-to-get-subtly-wrong part worth reusing rather
17
21
  * than re-deriving.
18
22
  *
19
- * 2. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
23
+ * 3. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
20
24
  * imported) from `packages/vgai-cli/src/project-root.ts`. `@vgai/sdk`'s
21
25
  * `resolveEditorSession` takes an already-known `ctx.projectRoot`; it has
22
26
  * no "walk up from an arbitrary cwd to find the nearest vgai.game.json"
@@ -41,7 +45,7 @@
41
45
  * match means a clear, actionable error, never a guess.
42
46
  */
43
47
 
44
- import { existsSync } from 'node:fs';
48
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
45
49
  import { dirname, join, resolve } from 'node:path';
46
50
  import {
47
51
  EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
@@ -82,6 +86,67 @@ export interface SessionResolutionDeps {
82
86
  transport?: SessionListingTransport;
83
87
  /** Project-root discovery — defaults to the real fs walk (`findProjectRootFrom` above). Overridable for tests. */
84
88
  findProjectRootFrom?: (dir: string) => string | null;
89
+ /** Project-local session hint reader. Defaults to `.vgai/session.json`. */
90
+ readProjectSession?: (projectRoot: string) => ProjectSessionHint | null;
91
+ /** Exact-session verifier. Defaults to GET /__editor/project + canonical project matching. */
92
+ verifyProjectSession?: (hint: ProjectSessionHint, projectRoot: string) => Promise<boolean>;
93
+ }
94
+
95
+ export interface ProjectSessionHint {
96
+ port: number;
97
+ pid: number;
98
+ url: string;
99
+ startedAt: string;
100
+ }
101
+
102
+ function canonicalPath(path: string): string {
103
+ try {
104
+ return realpathSync(path);
105
+ } catch {
106
+ return resolve(path);
107
+ }
108
+ }
109
+
110
+ function readProjectSession(projectRoot: string): ProjectSessionHint | null {
111
+ try {
112
+ const value: unknown = JSON.parse(
113
+ readFileSync(join(projectRoot, '.vgai', 'session.json'), 'utf8'),
114
+ );
115
+ if (typeof value !== 'object' || value === null) return null;
116
+ const hint = value as Record<string, unknown>;
117
+ if (
118
+ typeof hint['port'] !== 'number' ||
119
+ !Number.isInteger(hint['port']) ||
120
+ hint['port'] <= 0 ||
121
+ typeof hint['pid'] !== 'number' ||
122
+ typeof hint['url'] !== 'string' ||
123
+ typeof hint['startedAt'] !== 'string'
124
+ ) {
125
+ return null;
126
+ }
127
+ return hint as unknown as ProjectSessionHint;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ async function verifyProjectSession(
134
+ hint: ProjectSessionHint,
135
+ projectRoot: string,
136
+ ): Promise<boolean> {
137
+ try {
138
+ const response = await fetch(`http://localhost:${hint.port}/__editor/project`, {
139
+ signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
140
+ });
141
+ if (!response.ok) return false;
142
+ const body = (await response.json()) as { project?: { path?: string } | null };
143
+ const servedProject = body.project?.path;
144
+ return (
145
+ servedProject !== undefined && canonicalPath(servedProject) === canonicalPath(projectRoot)
146
+ );
147
+ } catch {
148
+ return false;
149
+ }
85
150
  }
86
151
 
87
152
  /**
@@ -105,6 +170,19 @@ export async function resolveSession(
105
170
  );
106
171
  }
107
172
 
173
+ // The editor writes this exact-project hint at boot and removes it on
174
+ // shutdown. Prefer it over a global all-session scan: it is both faster and
175
+ // immune to an unrelated slow/dead registry entry consuming the discovery
176
+ // deadline. The live probe remains authoritative, so a stale file cannot
177
+ // attach us to the wrong project or port.
178
+ const localHint = (deps.readProjectSession ?? readProjectSession)(projectRoot);
179
+ if (
180
+ localHint &&
181
+ (await (deps.verifyProjectSession ?? verifyProjectSession)(localHint, projectRoot))
182
+ ) {
183
+ return { port: localHint.port, projectRoot };
184
+ }
185
+
108
186
  let sessions: EditorSessionInfo[];
109
187
  try {
110
188
  sessions = await withTimeout(
@@ -116,8 +194,8 @@ export async function resolveSession(
116
194
  sessions = [];
117
195
  }
118
196
 
119
- const canon = resolve(projectRoot);
120
- const match = sessions.find((s) => s.project !== null && resolve(s.project) === canon);
197
+ const canon = canonicalPath(projectRoot);
198
+ const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
121
199
  if (!match) {
122
200
  const otherCount = sessions.length;
123
201
  throw new Error(