@vgai/sdk 0.5.2 → 0.5.4

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.
@@ -1,6 +1,5 @@
1
1
  /**
2
- * `project.discover` and `project.manifest.*` (B2,
3
- * docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B2 "project
2
+ * `project.discover` and `project.manifest.*` (B2, "project
4
3
  * discovery/status" + "manifest read/validate/update").
5
4
  *
6
5
  * REUSE, not reimplementation: `project.manifest.validate`/`.update` both
@@ -33,7 +32,7 @@ import {
33
32
  writeFileAtomic,
34
33
  } from './shared.js';
35
34
 
36
- const MANIFEST_FILENAME = 'vgai.game.json';
35
+ const MANIFEST_FILENAME = 'vgai.project.json';
37
36
 
38
37
  // ---------------------------------------------------------------------------
39
38
  // project.discover — richer, real project-native discovery (B1's
@@ -49,7 +48,7 @@ const ProjectDiscoverResult = z
49
48
  projectRoot: z.string().describe('Absolute path this discovery ran against.'),
50
49
  hasManifest: z
51
50
  .boolean()
52
- .describe('True when vgai.game.json exists directly under projectRoot.'),
51
+ .describe('True when vgai.project.json exists directly under projectRoot.'),
53
52
  manifestName: z
54
53
  .string()
55
54
  .optional()
@@ -58,9 +57,6 @@ const ProjectDiscoverResult = z
58
57
  .array(z.string())
59
58
  .optional()
60
59
  .describe('Adapter-root ids declared in the manifest, when present/valid.'),
61
- sceneFiles: z
62
- .array(z.string())
63
- .describe('Every *.vscn.json found under the project (project-relative).'),
64
60
  inputMapFiles: z
65
61
  .array(z.string())
66
62
  .describe('Every *.inputmap.json found under the project (project-relative).'),
@@ -114,7 +110,6 @@ export const projectDiscover = defineTool({
114
110
  hasManifest,
115
111
  ...(manifestName !== undefined ? { manifestName } : {}),
116
112
  ...(rootIds !== undefined ? { rootIds } : {}),
117
- sceneFiles: listProjectFiles(projectRoot, (p) => p.endsWith('.vscn.json')),
118
113
  inputMapFiles: listProjectFiles(projectRoot, (p) => p.endsWith('.inputmap.json')),
119
114
  storyFiles: listProjectFiles(
120
115
  projectRoot,
@@ -133,11 +128,11 @@ export const projectDiscover = defineTool({
133
128
 
134
129
  const ProjectManifestReadInput = z
135
130
  .object({})
136
- .describe('No input — reads vgai.game.json under ctx.projectRoot.');
131
+ .describe('No input — reads vgai.project.json under ctx.projectRoot.');
137
132
 
138
133
  const ProjectManifestReadResult = z
139
134
  .object({
140
- path: z.string().describe('Absolute path to vgai.game.json.'),
135
+ path: z.string().describe('Absolute path to vgai.project.json.'),
141
136
  contentHash: z
142
137
  .string()
143
138
  .describe(
@@ -153,7 +148,7 @@ const ProjectManifestReadResult = z
153
148
 
154
149
  export const projectManifestRead = defineTool({
155
150
  name: 'project.manifest.read',
156
- summary: 'Read the raw vgai.game.json manifest for the project.',
151
+ summary: 'Read the raw vgai.project.json manifest for the project.',
157
152
  description:
158
153
  'File-native read. Returns the raw parsed JSON plus a content hash for optimistic-concurrency updates.',
159
154
  input: ProjectManifestReadInput,
@@ -163,7 +158,7 @@ export const projectManifestRead = defineTool({
163
158
  host: 'node',
164
159
  mutates: false,
165
160
  supportsDryRun: false,
166
- permission: { risk: 'read', summary: 'Reads vgai.game.json; no writes.' },
161
+ permission: { risk: 'read', summary: 'Reads vgai.project.json; no writes.' },
167
162
  async impl(_input, ctx) {
168
163
  const projectRoot = requireProjectRoot(ctx);
169
164
  const path = resolveProjectPath(projectRoot, MANIFEST_FILENAME);
@@ -181,7 +176,7 @@ const ProjectManifestValidateInput = z
181
176
  manifest: z
182
177
  .unknown()
183
178
  .optional()
184
- .describe('Validate this raw value instead of re-reading vgai.game.json from disk.'),
179
+ .describe('Validate this raw value instead of re-reading vgai.project.json from disk.'),
185
180
  })
186
181
  .describe('Validate the on-disk manifest, or a given candidate document.');
187
182
 
@@ -209,7 +204,7 @@ export const projectManifestValidate = defineTool({
209
204
  supportsDryRun: false,
210
205
  permission: {
211
206
  risk: 'read',
212
- summary: 'Reads vgai.game.json (or validates a given document); no writes.',
207
+ summary: 'Reads vgai.project.json (or validates a given document); no writes.',
213
208
  },
214
209
  async impl(input, ctx) {
215
210
  let raw: unknown;
@@ -264,7 +259,7 @@ const ProjectManifestUpdateInput = z
264
259
  dryRun: DryRunField,
265
260
  baseHash: BaseHashField,
266
261
  })
267
- .describe('Patch a subset of top-level vgai.game.json fields.');
262
+ .describe('Patch a subset of top-level vgai.project.json fields.');
268
263
 
269
264
  const ProjectManifestUpdateResult = mutationResultSchema(
270
265
  z
@@ -274,7 +269,7 @@ const ProjectManifestUpdateResult = mutationResultSchema(
274
269
 
275
270
  export const projectManifestUpdate = defineTool({
276
271
  name: 'project.manifest.update',
277
- summary: 'Patch name/version/resolution on vgai.game.json, dry-run capable, conflict-checked.',
272
+ summary: 'Patch name/version/resolution on vgai.project.json, dry-run capable, conflict-checked.',
278
273
  description:
279
274
  'Merges `patch` onto the raw on-disk manifest, re-validates the FULL merged document through ' +
280
275
  'GameManifestSchema (the real schema, cross-field rules included) before ever writing, and ' +
@@ -296,7 +291,7 @@ export const projectManifestUpdate = defineTool({
296
291
  host: 'node',
297
292
  mutates: true,
298
293
  supportsDryRun: true,
299
- permission: { risk: 'write', summary: 'Writes vgai.game.json.' },
294
+ permission: { risk: 'write', summary: 'Writes vgai.project.json.' },
300
295
  async impl(input, ctx) {
301
296
  const projectRoot = requireProjectRoot(ctx);
302
297
  const path = resolveProjectPath(projectRoot, MANIFEST_FILENAME);
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Shared plumbing for B2's `project.*` operations
3
- * (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B2).
2
+ * Shared plumbing for B2's `project.*` operations.
4
3
  *
5
4
  * Every `project.*` op is file-native (host: 'node', requires: { project: true })
6
5
  * and runs with NO editor process — it resolves `ctx.projectRoot`, reads/writes
7
6
  * plain files on disk, and reuses the engine's existing Zod schemas +
8
- * `applyDiff` for anything scene-shaped (see `../../../engine/src/scene/scene-apply.ts`).
7
+ * schemas. (WO-8: this also said it reused the engine's `applyDiff` for anything
8
+ * scene-shaped — `engine/src/scene/scene-apply.ts` and the twelve
9
+ * `project.scene.*` ops that used it are deleted with the `.vscn.json` format.)
9
10
  *
10
11
  * Two cross-cutting decisions (pinned by the B2 task brief) live here so every
11
12
  * operation module applies them identically:
@@ -38,7 +39,6 @@ import {
38
39
  writeFileSync,
39
40
  } from 'node:fs';
40
41
  import { dirname, isAbsolute, join, relative, resolve, win32 } from 'node:path';
41
- import type { SceneEntity } from '@vgai/engine/scene/scene-types';
42
42
  import { z } from 'zod';
43
43
  import { ToolError } from '../errors.js';
44
44
  import type { ToolErrorDefinition } from '../registry.js';
@@ -274,41 +274,6 @@ export function mutationResultSchema<T extends z.ZodType>(afterSchema: T) {
274
274
  });
275
275
  }
276
276
 
277
- // ---------------------------------------------------------------------------
278
- // Scene entity tree walk (read-side helper shared by entity/component ops)
279
- // ---------------------------------------------------------------------------
280
-
281
- export const ENTITY_NOT_FOUND_ERROR: ToolErrorDefinition = {
282
- code: 'ENTITY_NOT_FOUND',
283
- summary: 'No entity with the given id exists in the scene.',
284
- data: z.object({ id: z.string() }),
285
- };
286
-
287
- /** Depth-first search for an entity by its stable `id` — the SAME identity `applyDiff` addresses by (never an array index). */
288
- export function findEntityById(entities: SceneEntity[], id: string): SceneEntity | undefined {
289
- for (const e of entities) {
290
- if (e.id === id) return e;
291
- if (e.children) {
292
- const found = findEntityById(e.children, id);
293
- if (found) return found;
294
- }
295
- }
296
- return undefined;
297
- }
298
-
299
- /** Require an entity by id, throwing the declared `ENTITY_NOT_FOUND` shape otherwise. */
300
- export function requireEntityById(entities: SceneEntity[], id: string): SceneEntity {
301
- const found = findEntityById(entities, id);
302
- if (!found) {
303
- throw new ToolError(
304
- 'ENTITY_NOT_FOUND',
305
- `No entity with id "${id}" exists in this scene.`,
306
- { id },
307
- );
308
- }
309
- return found;
310
- }
311
-
312
277
  // ---------------------------------------------------------------------------
313
278
  // Project-wide file discovery (shared by project.discover / story / theatre /
314
279
  // xstate discovery ops — all "find files matching X under the project" ops).
package/src/registry.ts CHANGED
@@ -137,8 +137,14 @@ export type ToolOutcome<TResult = unknown> =
137
137
  * forwarding unvalidated data).
138
138
  * 3. Anything else — an `ToolError` with an undeclared code, a plain
139
139
  * `Error`, or a non-Error throw — normalized into INTERNAL_ERROR. The
140
- * raw exception/message is tucked into `data.message`, never used as
141
- * the identifying `code`.
140
+ * raw exception/message is never used as the identifying `code`, but it
141
+ * IS carried in `message` as well as `data.message`: every projection
142
+ * (the CLI's `vgai run`, the oclif commands, `vgai screenshot`'s module
143
+ * lane) shows `error.message` and only some of them dump `data`, so a
144
+ * `message` that said nothing but "threw an unstructured exception"
145
+ * hid the one sentence the caller needed ("No editor connected — open
146
+ * the editor in a browser tab, then retry.") behind whichever surface
147
+ * happened to print the whole outcome.
142
148
  */
143
149
  function normalizeThrown(def: ToolDefinition, err: unknown): StructuredOperationError {
144
150
  if (isOperationError(err)) {
@@ -173,7 +179,7 @@ function normalizeThrown(def: ToolDefinition, err: unknown): StructuredOperation
173
179
  const message = err instanceof Error ? err.message : String(err);
174
180
  return {
175
181
  code: CORE_ERROR_CODES.INTERNAL_ERROR,
176
- message: `"${def.name}" threw an unstructured exception.`,
182
+ message: `"${def.name}" failed: ${message}`,
177
183
  data: { message },
178
184
  };
179
185
  }
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * The render pipeline core (I0/I2-I8, A1) — relocated INTO `@vgai/sdk` so the
3
- * dependency direction is CLI -> SDK -> engine, never SDK -> CLI (per
4
- * `docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md` §3.4: the SDK is the
5
- * authoritative operation core; CLI/HTTP/MCP are projections on top of it).
6
- * Formerly `packages/vgai-cli/src/render-cinematic.ts` +
3
+ * dependency direction is CLI -> SDK -> engine, never SDK -> CLI (the SDK is
4
+ * the authoritative operation core; CLI/HTTP/MCP are projections on top of
5
+ * it). Formerly `packages/vgai-cli/src/render-cinematic.ts` +
7
6
  * `packages/vgai-cli/src/capabilities/ffmpeg.ts`, imported by the SDK's own
8
7
  * `cinematic.*` operations (`../cinematic/`) via a deep-src `@vgai/cli`
9
8
  * tsconfig path alias — a package-layering inversion once B6 makes the CLI
@@ -1,6 +1,5 @@
1
1
  /**
2
- * `vgai render-cinematic` — the I0 crux deliverable
3
- * (`docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md` §18 "I0 — recording
2
+ * `vgai render-cinematic` — the I0 crux deliverable ("I0 — recording
4
3
  * spike", §15 Workstream I). Produces a REAL, deterministic, playable video
5
4
  * file from an authored Theatre-driven cinematic: launches headless
6
5
  * Chromium (SwiftShader) against a render-mode page (`?vgai-render=1`,
@@ -81,8 +80,7 @@ export function defaultSimulateCinematicViteConfig(
81
80
  export type RenderCinematicFormat = 'mp4' | 'webm' | 'png';
82
81
  export type RenderCinematicBackground = 'opaque' | 'transparent';
83
82
  /**
84
- * I6 audio mode (`docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md` §15 I6 +
85
- * the audio-capture half of §13 G4):
83
+ * I6 audio mode (+ the audio-capture half of §13 G4):
86
84
  * - `'auto'` (default) — mux audio iff the render page declares a Tone
87
85
  * score (`window.__vgaiRenderAudio.hasAudio === true`,
88
86
  * `@engine/runtime/render-audio-control`); a page with no score renders
@@ -890,19 +888,21 @@ async function readExcludedFromDeterminism(page: Page): Promise<string[]> {
890
888
  * the ACTUAL fixed gameplay substep `dt` the harness is using, instead of
891
889
  * this pipeline assuming every fixture matches its own `1/60` default. Only
892
890
  * called for a `resolved.simulate: true` request (see the call site in
893
- * {@link renderCinematic}). Returns `undefined` — rather than throwing — for
894
- * a render-mode page that predates this harness method (back-compat: an
895
- * older/unmodified real game's own render-mode page might not have it yet);
896
- * {@link buildSimulateManifest} falls back to the documented `1/60` engine
897
- * default in that case, with an honest comment, never a silent guess dressed
898
- * up as a measured value.
891
+ * {@link renderCinematic}).
892
+ *
893
+ * `simulateFixedDt()` is a REQUIRED member of `VgaiRenderHarness` and every
894
+ * render page is built from this repo, so there is no "older page" case: the
895
+ * former `typeof === 'function'` probe (and the `?? 1/60` fallback it fed in
896
+ * {@link buildSimulateManifest}) could only ever have converted a genuinely
897
+ * broken harness into a plausible-looking guess. Both are deleted — a
898
+ * missing method now throws where it happens.
899
899
  */
900
- async function readSimulateFixedDt(page: Page): Promise<number | undefined> {
901
- return page.evaluate(() => {
902
- const harness = (globalThis as unknown as { __vgaiRender: { simulateFixedDt?: () => number } })
903
- .__vgaiRender;
904
- return typeof harness.simulateFixedDt === 'function' ? harness.simulateFixedDt() : undefined;
905
- });
900
+ async function readSimulateFixedDt(page: Page): Promise<number> {
901
+ return page.evaluate(() =>
902
+ (
903
+ globalThis as unknown as { __vgaiRender: { simulateFixedDt(): number } }
904
+ ).__vgaiRender.simulateFixedDt(),
905
+ );
906
906
  }
907
907
 
908
908
  /**
@@ -1558,25 +1558,26 @@ async function preparePipelineOutputs(
1558
1558
  * (`RenderControlHarnessOptions.simulateFixedDt`) — silently wrong for any
1559
1559
  * fixture that set a different substep dt. `actualFixedDt` is
1560
1560
  * {@link readSimulateFixedDt}'s result — the REAL value read off the live
1561
- * harness right before the page closed. The `?? 1 / 60` fallback below is
1562
- * ONLY reached for a render-mode page that predates the
1563
- * `simulateFixedDt()` harness method (an older/unmodified page) — an
1564
- * honestly-commented "best known default", not a value ever claimed to be
1565
- * measured.
1561
+ * harness right before the page closed, always measured, never defaulted.
1562
+ * It is `undefined` only for a non-`--simulate` request, where this function
1563
+ * returns `undefined` before reading it.
1566
1564
  */
1567
1565
  function buildSimulateManifest(
1568
1566
  resolved: ResolvedRenderCinematicRequest,
1569
1567
  actualFixedDt: number | undefined,
1570
1568
  ): RenderManifestSimulate | undefined {
1571
1569
  if (!resolved.simulate) return undefined;
1570
+ if (actualFixedDt === undefined) {
1571
+ throw new Error(
1572
+ 'renderCinematic: --simulate requested but the render harness reported no ' +
1573
+ 'simulateFixedDt() — the page is not a real @engine/runtime/render-control harness.',
1574
+ );
1575
+ }
1572
1576
  return {
1573
1577
  seed: resolved.seed,
1574
1578
  substeps: resolved.simulateSubsteps,
1575
1579
  warmupSubsteps: resolved.simulateWarmupSubsteps,
1576
- // Honest fallback only: the harness didn't expose `simulateFixedDt()`
1577
- // (predates I7), so this is the engine-wide documented default
1578
- // (`render-control.ts`'s own `1/60`), NOT a measured value.
1579
- fixedTimestep: actualFixedDt ?? 1 / 60,
1580
+ fixedTimestep: actualFixedDt,
1580
1581
  };
1581
1582
  }
1582
1583
 
@@ -1815,9 +1816,7 @@ export async function renderCinematic(
1815
1816
  const excludedFromDeterminism = await readExcludedFromDeterminism(page);
1816
1817
  // I7 fold-in (I5 nit) — the ACTUAL harness dt, read before the page/
1817
1818
  // browser closes, same constraint as the two calls above. `undefined`
1818
- // for a non-`--simulate` request (never called) or a render-mode page
1819
- // that predates this harness method (back-compat fallback in
1820
- // `buildSimulateManifest`).
1819
+ // only for a non-`--simulate` request, where it is never called.
1821
1820
  const actualSimulateFixedDt = resolved.simulate ? await readSimulateFixedDt(page) : undefined;
1822
1821
 
1823
1822
  await page.context().close();
package/src/types.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Shared vocabulary for the operation registry (B1,
3
- * docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B1 / §5.7 / §3.4).
2
+ * Shared vocabulary for the operation registry (B1).
4
3
  */
5
4
 
6
5
  /** The four namespaces every operation name must live under (§5.7). */
@@ -53,7 +52,7 @@ export interface ProjectOutputFile {
53
52
  path: string;
54
53
  content: string | Uint8Array;
55
54
  mediaType?: string;
56
- role?: 'asset' | 'prefab' | 'provenance' | 'other';
55
+ role?: 'asset' | 'provenance' | 'other';
57
56
  }
58
57
 
59
58
  /** JSON-safe summary returned after an atomic generated-output transaction. */
@@ -123,5 +122,10 @@ export interface ToolContext {
123
122
  signal?: AbortSignal;
124
123
  /** Atomic writer for generated project assets (Node project operations). */
125
124
  projectOutputs?: ProjectOutputWriter;
125
+ /** WHICH mounted instance a game-driving tool should address, when the
126
+ * editor has several live (multiplayer authoring). A tool that drives the
127
+ * game binds `game.instance(ctx.instance)` from it; omitted means the sole
128
+ * live instance. */
129
+ instance?: string;
126
130
  [key: string]: unknown;
127
131
  }