@vgai/live 0.5.13 → 0.5.15

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/src/index.ts CHANGED
@@ -71,6 +71,8 @@ export type {
71
71
  export { LiveEditorDocument } from './editor-document.js';
72
72
  export { createGameClient, createLiveGame, type LiveGame } from './game.js';
73
73
  export * from './game-client/index.js';
74
+ export type { GameplayRecordingResult } from './recording.js';
75
+ export { LiveGameplayRecording } from './recording.js';
74
76
  export type {
75
77
  ProjectSessionHint,
76
78
  ResolvedSession,
@@ -82,8 +84,18 @@ export type { LazySession } from './singleton.js';
82
84
  export { createLazySession } from './singleton.js';
83
85
  export { LiveTools } from './tools.js';
84
86
 
85
- /** A `game.page(step)`-shaped call — see this module's doc comment for the closure-capture limitation. */
86
- export type PageStep = GameClient['page'];
87
+ /**
88
+ * The `page` binding: a `game.page(step)`-shaped CALL (see this module's doc
89
+ * comment for the closure-capture limitation) that also carries the one page
90
+ * verb no step can express — `page.reload()`.
91
+ *
92
+ * `reload` is a property rather than a step because a step that navigates
93
+ * destroys the channel its own result would return on: `page(p =>
94
+ * p.evaluate(() => location.reload()))` was the only spelling available, and
95
+ * it resolves on a send while failing outright outside play mode. See
96
+ * `GameClient.reloadPage`.
97
+ */
98
+ export type PageStep = GameClient['page'] & { reload: GameClient['reloadPage'] };
87
99
 
88
100
  export interface LiveSession extends LiveBindings {
89
101
  /** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
@@ -97,7 +109,9 @@ export interface LiveBindings {
97
109
  * several mounted instances (multiplayer authoring). A bare `game` call
98
110
  * targets the sole instance and refuses when several are live. */
99
111
  game: LiveGame;
100
- /** `GameClient.page` bound to `game` — see this module's own doc comment for the wire limitation. */
112
+ /** `GameClient.page` bound to `game`, plus `page.reload()` — see this
113
+ * module's own doc comment for the wire limitation, and {@link PageStep}
114
+ * for why reload is a property rather than a step. */
101
115
  page: PageStep;
102
116
  /** Registered project callables: enumerate, inspect, and invoke. */
103
117
  tools: LiveTools;
@@ -112,7 +126,8 @@ function bindTo(port: number, projectRoot: string): LiveBindings {
112
126
  const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
113
127
  const editor = new LiveEditor(client);
114
128
  const game = createLiveGame(port, join(projectRoot, '.vgai', 'last-run'));
115
- const page: PageStep = (step) => game.page(step);
129
+ const step: GameClient['page'] = (fn) => game.page(fn);
130
+ const page: PageStep = Object.assign(step, { reload: () => game.reloadPage() });
116
131
  const tools = new LiveTools(client);
117
132
  return { editor, game, page, tools };
118
133
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `editor.recording` — real-time gameplay evidence through the existing live
3
+ * editor session. The browser owns MediaRecorder and the game pixels/audio;
4
+ * the project server streams the standard WebM to disk while this namespace
5
+ * exposes the start/stop controls and optional final copy destination.
6
+ *
7
+ * Recording deliberately is not a CLI workflow of its own. Agents start and
8
+ * stop it through the one general door (`vgai eval`) and use ordinary ffmpeg,
9
+ * ffprobe, and filesystem tools to review or trim the result.
10
+ */
11
+
12
+ import { copyFile, mkdir } from 'node:fs/promises';
13
+ import { dirname, extname, isAbsolute, resolve } from 'node:path';
14
+ import type {
15
+ EditorClient,
16
+ GameplayRecordingOptions,
17
+ GameplayRecordingStarted,
18
+ } from '@vgai/editor-sdk';
19
+
20
+ export interface GameplayRecordingResult extends GameplayRecordingStarted {
21
+ /** Absolute path to the standard WebM written on stop. */
22
+ path: string;
23
+ durationMs: number;
24
+ droppedFrames: number;
25
+ frameErrors: number;
26
+ }
27
+
28
+ function recordingPath(destination: string): string {
29
+ if (destination.trim() === '') throw new Error('recording destination must not be empty');
30
+ const withExtension = extname(destination) === '' ? `${destination}.webm` : destination;
31
+ return isAbsolute(withExtension) ? withExtension : resolve(process.cwd(), withExtension);
32
+ }
33
+
34
+ export class LiveGameplayRecording {
35
+ readonly #client: EditorClient;
36
+
37
+ constructor(client: EditorClient) {
38
+ this.#client = client;
39
+ }
40
+
41
+ /** Start one clean game recording. The page owns it until `stop()`, even
42
+ * when start and stop are issued by separate `vgai eval` processes. */
43
+ async start(options: GameplayRecordingOptions = {}): Promise<GameplayRecordingStarted> {
44
+ return this.#client.startGameplayRecording(options);
45
+ }
46
+
47
+ /** Finalize the active recording, write it to disk, and return honest media
48
+ * metadata without echoing the large base64 transport payload to stdout. */
49
+ async stop(destination?: string): Promise<GameplayRecordingResult> {
50
+ const capture = await this.#client.stopGameplayRecording();
51
+ const path = destination === undefined ? capture.path : recordingPath(destination);
52
+ if (path !== capture.path) {
53
+ await mkdir(dirname(path), { recursive: true });
54
+ await copyFile(capture.path, path);
55
+ }
56
+ return {
57
+ path,
58
+ startedAt: capture.startedAt,
59
+ mimeType: capture.mimeType,
60
+ width: capture.width,
61
+ height: capture.height,
62
+ fps: capture.fps,
63
+ audio: capture.audio,
64
+ durationMs: capture.durationMs,
65
+ droppedFrames: capture.droppedFrames,
66
+ frameErrors: capture.frameErrors,
67
+ layers: capture.layers,
68
+ };
69
+ }
70
+ }