@nodetool-ai/agents 0.8.2 → 0.8.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 (189) hide show
  1. package/dist/.nodetool-build-stamp +1 -1
  2. package/dist/apify/assets.d.ts.map +1 -1
  3. package/dist/apify/assets.js +4 -0
  4. package/dist/apify/assets.js.map +1 -1
  5. package/dist/capabilities/agent-activity.js +1 -1
  6. package/dist/capabilities/assets.d.ts.map +1 -1
  7. package/dist/capabilities/assets.js +40 -10
  8. package/dist/capabilities/assets.js.map +1 -1
  9. package/dist/capabilities/collections.d.ts +4 -6
  10. package/dist/capabilities/collections.d.ts.map +1 -1
  11. package/dist/capabilities/collections.js +16 -11
  12. package/dist/capabilities/collections.js.map +1 -1
  13. package/dist/capabilities/derive-shot.d.ts +45 -0
  14. package/dist/capabilities/derive-shot.d.ts.map +1 -0
  15. package/dist/capabilities/derive-shot.js +82 -0
  16. package/dist/capabilities/derive-shot.js.map +1 -0
  17. package/dist/capabilities/documents.d.ts +2 -1
  18. package/dist/capabilities/documents.d.ts.map +1 -1
  19. package/dist/capabilities/documents.js +12 -6
  20. package/dist/capabilities/documents.js.map +1 -1
  21. package/dist/capabilities/email.d.ts +2 -1
  22. package/dist/capabilities/email.d.ts.map +1 -1
  23. package/dist/capabilities/email.js +5 -3
  24. package/dist/capabilities/email.js.map +1 -1
  25. package/dist/capabilities/game-asset-browser.d.ts +11 -0
  26. package/dist/capabilities/game-asset-browser.d.ts.map +1 -0
  27. package/dist/capabilities/game-asset-browser.js +152 -0
  28. package/dist/capabilities/game-asset-browser.js.map +1 -0
  29. package/dist/capabilities/game.d.ts +30 -0
  30. package/dist/capabilities/game.d.ts.map +1 -1
  31. package/dist/capabilities/game.js +215 -137
  32. package/dist/capabilities/game.js.map +1 -1
  33. package/dist/capabilities/game.specs.d.ts +2 -0
  34. package/dist/capabilities/game.specs.d.ts.map +1 -1
  35. package/dist/capabilities/game.specs.js +20 -5
  36. package/dist/capabilities/game.specs.js.map +1 -1
  37. package/dist/capabilities/game3d.d.ts.map +1 -1
  38. package/dist/capabilities/game3d.js +4 -3
  39. package/dist/capabilities/game3d.js.map +1 -1
  40. package/dist/capabilities/generations.d.ts.map +1 -1
  41. package/dist/capabilities/generations.js +7 -21
  42. package/dist/capabilities/generations.js.map +1 -1
  43. package/dist/capabilities/lazy-tool-core.d.ts +23 -0
  44. package/dist/capabilities/lazy-tool-core.d.ts.map +1 -0
  45. package/dist/capabilities/lazy-tool-core.js +56 -0
  46. package/dist/capabilities/lazy-tool-core.js.map +1 -0
  47. package/dist/capabilities/lazy-tool.d.ts +2 -21
  48. package/dist/capabilities/lazy-tool.d.ts.map +1 -1
  49. package/dist/capabilities/lazy-tool.js +3 -55
  50. package/dist/capabilities/lazy-tool.js.map +1 -1
  51. package/dist/capabilities/legacy-tools.js +1 -1
  52. package/dist/capabilities/legacy-tools.js.map +1 -1
  53. package/dist/capabilities/media.d.ts.map +1 -1
  54. package/dist/capabilities/media.js +13 -1
  55. package/dist/capabilities/media.js.map +1 -1
  56. package/dist/capabilities/media.specs.d.ts.map +1 -1
  57. package/dist/capabilities/media.specs.js +17 -0
  58. package/dist/capabilities/media.specs.js.map +1 -1
  59. package/dist/capabilities/metadata.d.ts +62 -0
  60. package/dist/capabilities/metadata.d.ts.map +1 -0
  61. package/dist/capabilities/metadata.js +114 -0
  62. package/dist/capabilities/metadata.js.map +1 -0
  63. package/dist/capabilities/model3d.d.ts.map +1 -1
  64. package/dist/capabilities/model3d.js +4 -0
  65. package/dist/capabilities/model3d.js.map +1 -1
  66. package/dist/capabilities/registry.d.ts +4 -19
  67. package/dist/capabilities/registry.d.ts.map +1 -1
  68. package/dist/capabilities/registry.js +48 -117
  69. package/dist/capabilities/registry.js.map +1 -1
  70. package/dist/capabilities/run-agent.js +1 -1
  71. package/dist/capabilities/settings.d.ts.map +1 -1
  72. package/dist/capabilities/settings.js +4 -2
  73. package/dist/capabilities/settings.js.map +1 -1
  74. package/dist/capabilities/storyboards.d.ts +11 -1
  75. package/dist/capabilities/storyboards.d.ts.map +1 -1
  76. package/dist/capabilities/storyboards.js +466 -4
  77. package/dist/capabilities/storyboards.js.map +1 -1
  78. package/dist/capabilities/storyboards.specs.d.ts +6 -0
  79. package/dist/capabilities/storyboards.specs.d.ts.map +1 -1
  80. package/dist/capabilities/storyboards.specs.js +131 -2
  81. package/dist/capabilities/storyboards.specs.js.map +1 -1
  82. package/dist/capabilities/threads.js +1 -1
  83. package/dist/capabilities/threads.js.map +1 -1
  84. package/dist/capabilities/timeline-bridge.d.ts +9 -0
  85. package/dist/capabilities/timeline-bridge.d.ts.map +1 -1
  86. package/dist/capabilities/timeline-bridge.js +60 -68
  87. package/dist/capabilities/timeline-bridge.js.map +1 -1
  88. package/dist/capabilities/timeline-operations.d.ts +52 -0
  89. package/dist/capabilities/timeline-operations.d.ts.map +1 -0
  90. package/dist/capabilities/timeline-operations.js +413 -0
  91. package/dist/capabilities/timeline-operations.js.map +1 -0
  92. package/dist/capabilities/timelines.d.ts +3 -38
  93. package/dist/capabilities/timelines.d.ts.map +1 -1
  94. package/dist/capabilities/timelines.js +30 -413
  95. package/dist/capabilities/timelines.js.map +1 -1
  96. package/dist/capabilities/timelines.specs.d.ts.map +1 -1
  97. package/dist/capabilities/timelines.specs.js +5 -1
  98. package/dist/capabilities/timelines.specs.js.map +1 -1
  99. package/dist/capabilities/web.d.ts.map +1 -1
  100. package/dist/capabilities/web.js +4 -0
  101. package/dist/capabilities/web.js.map +1 -1
  102. package/dist/capabilities/workflows.d.ts +3 -1
  103. package/dist/capabilities/workflows.d.ts.map +1 -1
  104. package/dist/capabilities/workflows.js +192 -14
  105. package/dist/capabilities/workflows.js.map +1 -1
  106. package/dist/capabilities/workflows.specs.d.ts +2 -0
  107. package/dist/capabilities/workflows.specs.d.ts.map +1 -1
  108. package/dist/capabilities/workflows.specs.js +65 -1
  109. package/dist/capabilities/workflows.specs.js.map +1 -1
  110. package/dist/codeact/sandbox-packages.d.ts +0 -7
  111. package/dist/codeact/sandbox-packages.d.ts.map +1 -1
  112. package/dist/codeact/sandbox-packages.js +1 -10
  113. package/dist/codeact/sandbox-packages.js.map +1 -1
  114. package/dist/evals/surfaces/game.d.ts +8 -2
  115. package/dist/evals/surfaces/game.d.ts.map +1 -1
  116. package/dist/evals/surfaces/game.js +460 -2
  117. package/dist/evals/surfaces/game.js.map +1 -1
  118. package/dist/game-authoring.d.ts +4895 -59
  119. package/dist/game-authoring.d.ts.map +1 -1
  120. package/dist/game-build.d.ts +3 -0
  121. package/dist/game-build.d.ts.map +1 -0
  122. package/dist/game-build.js +3 -0
  123. package/dist/game-build.js.map +1 -0
  124. package/dist/game-examples.d.ts +1355 -2
  125. package/dist/game-examples.d.ts.map +1 -1
  126. package/dist/generation-cancel.d.ts +31 -0
  127. package/dist/generation-cancel.d.ts.map +1 -0
  128. package/dist/generation-cancel.js +28 -0
  129. package/dist/generation-cancel.js.map +1 -0
  130. package/dist/host-access-gate.d.ts +29 -0
  131. package/dist/host-access-gate.d.ts.map +1 -0
  132. package/dist/host-access-gate.js +39 -0
  133. package/dist/host-access-gate.js.map +1 -0
  134. package/dist/host-modules/dispatcher.d.ts +7 -1
  135. package/dist/host-modules/dispatcher.d.ts.map +1 -1
  136. package/dist/host-modules/dispatcher.js +13 -2
  137. package/dist/host-modules/dispatcher.js.map +1 -1
  138. package/dist/host-modules/docx.d.ts.map +1 -1
  139. package/dist/host-modules/docx.js +2 -2
  140. package/dist/host-modules/docx.js.map +1 -1
  141. package/dist/host-modules/epub.d.ts.map +1 -1
  142. package/dist/host-modules/epub.js +2 -2
  143. package/dist/host-modules/epub.js.map +1 -1
  144. package/dist/host-modules/fabric.d.ts.map +1 -1
  145. package/dist/host-modules/fabric.js +1 -1
  146. package/dist/host-modules/fabric.js.map +1 -1
  147. package/dist/host-modules/limits.d.ts +10 -8
  148. package/dist/host-modules/limits.d.ts.map +1 -1
  149. package/dist/host-modules/limits.js +14 -9
  150. package/dist/host-modules/limits.js.map +1 -1
  151. package/dist/host-modules/mammoth.d.ts.map +1 -1
  152. package/dist/host-modules/mammoth.js +2 -2
  153. package/dist/host-modules/mammoth.js.map +1 -1
  154. package/dist/host-modules/ocr.d.ts.map +1 -1
  155. package/dist/host-modules/ocr.js +1 -1
  156. package/dist/host-modules/ocr.js.map +1 -1
  157. package/dist/host-modules/pdf.d.ts.map +1 -1
  158. package/dist/host-modules/pdf.js +2 -2
  159. package/dist/host-modules/pdf.js.map +1 -1
  160. package/dist/host-modules/pdflib.d.ts.map +1 -1
  161. package/dist/host-modules/pdflib.js +2 -2
  162. package/dist/host-modules/pdflib.js.map +1 -1
  163. package/dist/host-modules/pptx.d.ts.map +1 -1
  164. package/dist/host-modules/pptx.js +2 -2
  165. package/dist/host-modules/pptx.js.map +1 -1
  166. package/dist/host-modules/pptxgen.d.ts.map +1 -1
  167. package/dist/host-modules/pptxgen.js +2 -2
  168. package/dist/host-modules/pptxgen.js.map +1 -1
  169. package/dist/host-modules/tfjs.d.ts.map +1 -1
  170. package/dist/host-modules/tfjs.js +4 -4
  171. package/dist/host-modules/tfjs.js.map +1 -1
  172. package/dist/host-modules/xlsx.d.ts.map +1 -1
  173. package/dist/host-modules/xlsx.js +2 -2
  174. package/dist/host-modules/xlsx.js.map +1 -1
  175. package/dist/index.d.ts +3 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +4 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/js-sandbox.d.ts +7 -1
  180. package/dist/js-sandbox.d.ts.map +1 -1
  181. package/dist/js-sandbox.js +20 -4
  182. package/dist/js-sandbox.js.map +1 -1
  183. package/dist/timeline-code-bake.js +2 -2
  184. package/dist/timeline-code-bake.js.map +1 -1
  185. package/dist/tools/builtin-tools.d.ts +4 -3
  186. package/dist/tools/builtin-tools.d.ts.map +1 -1
  187. package/dist/tools/builtin-tools.js +11 -3
  188. package/dist/tools/builtin-tools.js.map +1 -1
  189. package/package.json +122 -41
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { createLogger, getManagedWorkspaceDir, managedWorkspaceKey, workspaceStorageKind } from "@nodetool-ai/config";
5
- import { AmbiguousGameIdError, Asset, Game, Prediction, Project, Workspace } from "@nodetool-ai/models";
5
+ import { AmbiguousGameIdError, MissingGameDraftSourceError, Asset, Game, Prediction, Project, Workspace } from "@nodetool-ai/models";
6
6
  import { anyGameAssetBinding as gameAssetBinding, gameAssetBinding as legacyAssetBinding, gameAssetBinding3D, anyGameDocument as gameDocument, gameInputFrame, shortResourceId } from "@nodetool-ai/protocol";
7
7
  import { autoplayNativeGame, MAX_GAME_ROUTE_TICKS, createScriptedGameSession, createTopDownRoomGame, createNative3DGame, decodePreparedGameCollider3D, validateAnyGame, trackGameAuthoringEdits, anyGameDocumentOp as gameDocumentOp, GameOpError } from "@nodetool-ai/game-runtime";
8
8
  import { workspaceFromRow } from "@nodetool-ai/execution/service";
@@ -13,7 +13,8 @@ import { persistOutput } from "../tools/asset-persist.js";
13
13
  import { getExampleGameBundle, installExampleGameAssets, listExampleGames } from "../game-examples.js";
14
14
  import { previewGameAuthoring, applyGameAuthoring } from "../game-authoring.js";
15
15
  import { gameDocumentDigest } from "../game-code-bake.js";
16
- import { previewGameAuthoringSpec, applyGameAuthoringSpec } from "./game.specs.js";
16
+ import { previewGameAuthoringSpec, applyGameAuthoringSpec, browseGameAssetsSpec } from "./game.specs.js";
17
+ import { browseGameAssets } from "./game-asset-browser.js";
17
18
  const log = createLogger("capabilities.game");
18
19
  const MAX_PLAYTEST_TICKS = MAX_GAME_ROUTE_TICKS;
19
20
  const MAX_CAPTURE_FRAMES = 8;
@@ -121,7 +122,8 @@ function outline(document) {
121
122
  inputActions: document.inputActions,
122
123
  collisionLayers: document.collisionLayers,
123
124
  renderEffects: document.renderEffects,
124
- hudEffectOrder: document.hudEffectOrder
125
+ hudEffectOrder: document.hudEffectOrder,
126
+ audio: document.audio
125
127
  },
126
128
  scenes: document.scenes.map((scene) => ({
127
129
  id: scene.id,
@@ -184,6 +186,8 @@ async function captureFrames(run, user, document, ticks, inputs, seed, options)
184
186
  return cancelled || wallTimeLimited;
185
187
  };
186
188
  const { captureGameFrame } = await import("@nodetool-ai/game-renderer/node");
189
+ const { GameParticles2D } = await import("@nodetool-ai/game-renderer");
190
+ const particles = new GameParticles2D(document.tickRate);
187
191
  const diagnostics = [];
188
192
  const storage = run.context.assetStorage;
189
193
  const assetCache = new Map();
@@ -221,7 +225,9 @@ async function captureFrames(run, user, document, ticks, inputs, seed, options)
221
225
  if (budgetReached()) {
222
226
  break;
223
227
  }
224
- simulatedTicks = session.step(inputs[simulatedTicks] ?? { pressed: [], justPressed: [] }).tick;
228
+ const step = session.step(inputs[simulatedTicks] ?? { pressed: [], justPressed: [] });
229
+ particles.tick(step.frame, session.takePresentationEvents());
230
+ simulatedTicks = step.tick;
225
231
  }
226
232
  if (budgetReached()) {
227
233
  break;
@@ -238,11 +244,11 @@ async function captureFrames(run, user, document, ticks, inputs, seed, options)
238
244
  const start = diagnostics.length;
239
245
  let png;
240
246
  try {
241
- png = await captureGameFrame(frame, { resolveAsset, scale, effects: document.renderEffects, hudEffectOrder: document.hudEffectOrder, backend: document.renderEffects?.length ? "webgpu" : "canvas2d", onDiagnostic: (message) => diagnostics.push(message) });
247
+ png = await captureGameFrame(frame, { resolveAsset, scale, particles, effects: document.renderEffects, hudEffectOrder: document.hudEffectOrder, backend: document.renderEffects?.length ? "webgpu" : "canvas2d", onDiagnostic: (message) => diagnostics.push(message) });
242
248
  }
243
249
  catch (error) {
244
250
  diagnostics.push(`WebGPU capture failed: ${error instanceof Error ? error.message : String(error)}`);
245
- png = await captureGameFrame(frame, { resolveAsset, scale, backend: "canvas2d", onDiagnostic: (message) => diagnostics.push(message) });
251
+ png = await captureGameFrame(frame, { resolveAsset, scale, particles, backend: "canvas2d", onDiagnostic: (message) => diagnostics.push(message) });
246
252
  }
247
253
  if (budgetReached()) {
248
254
  break;
@@ -415,7 +421,7 @@ export async function createNativeGame(user, projectId, name, source, dimension
415
421
  return { error: "Project workspace is unavailable" };
416
422
  const id = randomUUID().replace(/-/g, "");
417
423
  const revision = randomUUID().replace(/-/g, "");
418
- const document = validateSource(source ?? (dimension === "3d" ? createNative3DGame(id) : { ...createTopDownRoomGame(id), schemaVersion: 2 }), id, revision);
424
+ const document = validateSource(source ?? (dimension === "3d" ? createNative3DGame(id) : { ...createTopDownRoomGame(id), schemaVersion: 4, engineVersion: "3" }), id, revision);
419
425
  if ("error" in document)
420
426
  return document;
421
427
  if (source && ((document.schemaVersion === 3) !== (dimension === "3d"))) {
@@ -559,6 +565,9 @@ const install = {
559
565
  if (parsed.data.mediaKind === "font" && !parsed.data.fontFormat) {
560
566
  return { error: "Font binding needs a TrueType or OpenType format" };
561
567
  }
568
+ if (parsed.data.mediaKind === "hdri") {
569
+ return { error: "HDRI candidates cannot be installed until HDRI preparation verifies their bytes and dimensions" };
570
+ }
562
571
  const workspace = await workspaceOf(user, game);
563
572
  if (!workspace)
564
573
  return { error: "Game workspace is unavailable" };
@@ -1085,7 +1094,7 @@ const generateAsset = {
1085
1094
  if (prepared.baseline !== undefined && frames?.[0]) {
1086
1095
  binding.pivot = { x: 0.5, y: (prepared.baseline + 1) / frames[0].height };
1087
1096
  }
1088
- if (draft.document.schemaVersion === 2) {
1097
+ if (draft.document.schemaVersion !== 1) {
1089
1098
  binding.preparation = imagePreparationMetadata(settings.data);
1090
1099
  binding.originalDimensions = { width: prepared.originalWidth, height: prepared.originalHeight };
1091
1100
  if (prepared.trim) {
@@ -1123,7 +1132,7 @@ const generateAsset = {
1123
1132
  binding.digest = digest;
1124
1133
  binding.width = width;
1125
1134
  binding.height = height;
1126
- if (draft.document.schemaVersion === 2) {
1135
+ if (draft.document.schemaVersion !== 1 && draft.document.schemaVersion !== 3) {
1127
1136
  binding.provenance = typeof inputFile === "string" ? `import:${inputFile}`
1128
1137
  : makeLut ? "generated:color-cube" : kind === "sfx" ? `node:${args["node_type"]}` : `${provider}:${model}:${generated.generation_id ?? ""}`;
1129
1138
  }
@@ -1139,6 +1148,25 @@ const generateAsset = {
1139
1148
  return { game_id: shortResourceId(game.id), slot, binding: parsedBinding.data, candidate_workspace_id: game.workspace_id,
1140
1149
  draft_updated_at: draft.game.draft_updated_at, installed: false, next: "Call install_native_game_asset with this binding to install the candidate." };
1141
1150
  }
1151
+ if (args["install"] === false) {
1152
+ const parsedBinding = legacyAssetBinding.safeParse(binding);
1153
+ if (!parsedBinding.success) {
1154
+ return { error: "Invalid asset binding", diagnostics: parsedBinding.error.issues };
1155
+ }
1156
+ const staged = { game_id: shortResourceId(game.id), slot, binding: parsedBinding.data, candidate_workspace_id: game.workspace_id,
1157
+ draft_updated_at: draft.game.draft_updated_at, installed: false, generation_id: generated.generation_id,
1158
+ next: "Call install_native_game_asset with this binding to install the candidate." };
1159
+ if (frames) {
1160
+ staged.frames = frames;
1161
+ }
1162
+ if (tiles) {
1163
+ staged.tiles = tiles;
1164
+ }
1165
+ if (lutSize) {
1166
+ staged.lut_size = lutSize;
1167
+ }
1168
+ return staged;
1169
+ }
1142
1170
  const installed = await install.impl(run, { game_id: game.id, slot, binding, base_updated_at: draft.game.draft_updated_at });
1143
1171
  if (typeof installed !== "object" || installed === null) {
1144
1172
  return { generation_id: generated.generation_id, result: installed };
@@ -1165,141 +1193,177 @@ const generateAsset = {
1165
1193
  return { generation_id: generated.generation_id, ...installed, ...extras };
1166
1194
  }
1167
1195
  };
1168
- const buildGame = {
1169
- spec: gameSpecs[5],
1170
- impl: async (run, args) => {
1171
- const user = userId(run);
1172
- const id = args["game_id"];
1173
- if (!user || typeof id !== "string")
1174
- return { error: "game_id is required in a user session" };
1175
- const game = await ownedGame(user, id);
1176
- if (!game)
1177
- return { error: "Game not found" };
1178
- const workspace = await workspaceOf(user, game);
1179
- if (!workspace)
1180
- return { error: "Game workspace is unavailable" };
1181
- const revision = typeof args["revision"] === "string" ? args["revision"] : game.current_revision;
1182
- const document = await readDocument(workspace, game, revision);
1183
- if (!document)
1184
- return { error: "Game revision not found" };
1185
- if (Object.keys(document.assets).length > MAX_BUILD_ASSETS) {
1186
- return { error: `A standalone build supports at most ${MAX_BUILD_ASSETS} assets` };
1196
+ const BUILD_CONTENT_TYPES = {
1197
+ js: "text/javascript", css: "text/css", json: "application/json", html: "text/html", png: "image/png", jpg: "image/jpeg",
1198
+ webp: "image/webp", wav: "audio/wav", ogg: "audio/ogg", mp3: "audio/mpeg", ttf: "font/ttf", otf: "font/otf",
1199
+ glb: "model/gltf-binary", wasm: "application/wasm", md: "text/markdown", toml: "text/plain", rs: "text/plain",
1200
+ ico: "image/vnd.microsoft.icon", icns: "image/icns"
1201
+ };
1202
+ /**
1203
+ * Builds an owned, published game revision into its project workspace: a static web player under
1204
+ * `game-builds/<game-id>/<revision>`, or a Tauri desktop project under `game-builds/<game-id>/<revision>-tauri-<settings>`.
1205
+ * The capability and the editor's export download share this path.
1206
+ */
1207
+ export async function buildOwnedNativeGame(request) {
1208
+ const user = request.userId;
1209
+ const target = request.target ?? "web";
1210
+ const game = await ownedGame(user, request.gameId);
1211
+ if (!game)
1212
+ return { error: "Game not found" };
1213
+ const workspace = await workspaceOf(user, game);
1214
+ if (!workspace)
1215
+ return { error: "Game workspace is unavailable" };
1216
+ const revision = request.revision ?? game.current_revision;
1217
+ const document = await readDocument(workspace, game, revision);
1218
+ if (!document)
1219
+ return { error: "Game revision not found" };
1220
+ if (Object.keys(document.assets).length > MAX_BUILD_ASSETS) {
1221
+ return { error: `A standalone build supports at most ${MAX_BUILD_ASSETS} assets` };
1222
+ }
1223
+ const expectedDigests = new Map();
1224
+ for (const binding of Object.values(document.assets)) {
1225
+ if (binding.assetId.startsWith("builtin:"))
1226
+ continue;
1227
+ const digests = expectedDigests.get(binding.assetId) ?? new Set();
1228
+ digests.add(binding.digest);
1229
+ expectedDigests.set(binding.assetId, digests);
1230
+ }
1231
+ if ([...expectedDigests.values()].some((digests) => digests.size !== 1)) {
1232
+ return { error: "One source asset has conflicting digests in this revision" };
1233
+ }
1234
+ let buildPath = `game-builds/${game.id}/${revision}`;
1235
+ let entryPath = `${buildPath}/index.html`;
1236
+ let tauriApp;
1237
+ let metadata;
1238
+ if (target === "tauri") {
1239
+ tauriApp = { name: game.name };
1240
+ const { tauriAppMetadata } = await import("@nodetool-ai/game-renderer/build-tauri");
1241
+ try {
1242
+ metadata = tauriAppMetadata(document, tauriApp);
1187
1243
  }
1188
- const expectedDigests = new Map();
1189
- for (const binding of Object.values(document.assets)) {
1190
- if (binding.assetId.startsWith("builtin:"))
1191
- continue;
1192
- const digests = expectedDigests.get(binding.assetId) ?? new Set();
1193
- digests.add(binding.digest);
1194
- expectedDigests.set(binding.assetId, digests);
1244
+ catch (error) {
1245
+ return { error: error instanceof Error ? error.message : String(error) };
1195
1246
  }
1196
- if ([...expectedDigests.values()].some((digests) => digests.size !== 1)) {
1197
- return { error: "One source asset has conflicting digests in this revision" };
1247
+ // The app name comes from the game row, which can change without a new revision.
1248
+ const settings = createHash("sha256").update(JSON.stringify(metadata)).digest("hex").slice(0, 12);
1249
+ buildPath = `game-builds/${game.id}/${revision}-tauri-${settings}`;
1250
+ entryPath = `${buildPath}/src-tauri/tauri.conf.json`;
1251
+ }
1252
+ const done = { game_id: game.id, revision, target, build_path: buildPath, entry_path: entryPath, workspace, app: metadata ?? null };
1253
+ if (await workspace.exists(entryPath)) {
1254
+ return { ...done, cached: true };
1255
+ }
1256
+ const storage = request.storage;
1257
+ const cache = new Map();
1258
+ let totalAssetBytes = 0;
1259
+ const resolveAsset = async (assetId) => {
1260
+ const cached = cache.get(assetId);
1261
+ if (cached)
1262
+ return cached;
1263
+ if (!/^[a-f0-9]{32}$/.test(assetId))
1264
+ return null;
1265
+ const asset = await Asset.find(user, assetId);
1266
+ if (!asset || !storage)
1267
+ return null;
1268
+ const extension = MEDIA_EXTENSIONS[asset.content_type];
1269
+ if (!extension || (asset.size !== null && asset.size > MAX_BUILD_ASSET_BYTES))
1270
+ return null;
1271
+ const keys = assetKeyCandidates(user, `${asset.id}.${extension}`);
1272
+ let bytes = null;
1273
+ for (const key of keys) {
1274
+ bytes = await storage.retrieve(storage.uriForKey(key));
1275
+ if (bytes)
1276
+ break;
1198
1277
  }
1199
- const buildPath = `game-builds/${game.id}/${revision}`;
1200
- const indexPath = `${buildPath}/index.html`;
1201
- if (await workspace.exists(indexPath)) {
1202
- return { game_id: game.id, revision, build_path: buildPath, entry_path: indexPath, cached: true };
1278
+ if (!bytes || bytes.byteLength > MAX_BUILD_ASSET_BYTES)
1279
+ return null;
1280
+ const digest = createHash("sha256").update(bytes).digest("hex");
1281
+ if (!expectedDigests.get(assetId)?.has(digest)) {
1282
+ throw new Error(`Asset digest changed for ${assetId}`);
1203
1283
  }
1204
- const storage = run.context.assetStorage;
1205
- const cache = new Map();
1206
- let totalAssetBytes = 0;
1207
- const resolveAsset = async (assetId) => {
1208
- const cached = cache.get(assetId);
1209
- if (cached)
1210
- return cached;
1211
- if (!/^[a-f0-9]{32}$/.test(assetId))
1212
- return null;
1213
- const asset = await Asset.find(user, assetId);
1214
- if (!asset || !storage)
1215
- return null;
1216
- const extension = MEDIA_EXTENSIONS[asset.content_type];
1217
- if (!extension || (asset.size !== null && asset.size > MAX_BUILD_ASSET_BYTES))
1218
- return null;
1219
- const keys = assetKeyCandidates(user, `${asset.id}.${extension}`);
1220
- let bytes = null;
1221
- for (const key of keys) {
1222
- bytes = await storage.retrieve(storage.uriForKey(key));
1223
- if (bytes)
1224
- break;
1225
- }
1226
- if (!bytes || bytes.byteLength > MAX_BUILD_ASSET_BYTES)
1227
- return null;
1228
- const digest = createHash("sha256").update(bytes).digest("hex");
1229
- if (!expectedDigests.get(assetId)?.has(digest)) {
1230
- throw new Error(`Asset digest changed for ${assetId}`);
1231
- }
1232
- totalAssetBytes += bytes.byteLength;
1233
- if (totalAssetBytes > MAX_BUILD_BYTES) {
1234
- throw new Error(`A standalone build supports at most ${MAX_BUILD_BYTES} asset bytes`);
1235
- }
1236
- const resolved = { bytes, mimeType: asset.content_type };
1237
- cache.set(assetId, resolved);
1238
- return resolved;
1239
- };
1240
- const scratch = await workspace.scratchDir();
1241
- const staging = await mkdtemp(join(scratch, ".native-game-build-"));
1242
- try {
1243
- if (document.schemaVersion === 3) {
1244
- const { buildStandaloneGame3D } = await import("@nodetool-ai/game-renderer/build3d");
1245
- await buildStandaloneGame3D({ document, outputDir: join(staging, "player"), resolveAsset, signal: run.context.signal });
1246
- }
1247
- else {
1248
- const { buildStandaloneGame } = await import("@nodetool-ai/game-renderer/build");
1249
- await buildStandaloneGame({ document, outputDir: join(staging, "player"), resolveAsset });
1250
- }
1251
- const files = [];
1252
- const pending = [""];
1253
- let totalBytes = 0;
1254
- while (pending.length > 0) {
1255
- const directory = pending.pop() ?? "";
1256
- for (const entry of await readdir(join(staging, "player", directory), { withFileTypes: true })) {
1257
- const path = directory ? `${directory}/${entry.name}` : entry.name;
1258
- if (entry.isDirectory()) {
1259
- pending.push(path);
1260
- }
1261
- else if (entry.isFile()) {
1262
- const bytes = await readFile(join(staging, "player", path));
1263
- totalBytes += bytes.byteLength;
1264
- if (files.length >= MAX_BUILD_FILES || totalBytes > MAX_BUILD_BYTES) {
1265
- throw new Error("Standalone build exceeds file or byte limits");
1266
- }
1267
- files.push({ path, bytes });
1284
+ totalAssetBytes += bytes.byteLength;
1285
+ if (totalAssetBytes > MAX_BUILD_BYTES) {
1286
+ throw new Error(`A standalone build supports at most ${MAX_BUILD_BYTES} asset bytes`);
1287
+ }
1288
+ const resolved = { bytes, mimeType: asset.content_type };
1289
+ cache.set(assetId, resolved);
1290
+ return resolved;
1291
+ };
1292
+ const scratch = await workspace.scratchDir();
1293
+ const staging = await mkdtemp(join(scratch, ".native-game-build-"));
1294
+ try {
1295
+ const output = join(staging, "player");
1296
+ if (tauriApp) {
1297
+ const { buildTauriGameProject } = await import("@nodetool-ai/game-renderer/build-tauri");
1298
+ await buildTauriGameProject({ document, outputDir: output, resolveAsset, app: tauriApp, signal: request.signal });
1299
+ }
1300
+ else if (document.schemaVersion === 3) {
1301
+ const { buildStandaloneGame3D } = await import("@nodetool-ai/game-renderer/build3d");
1302
+ await buildStandaloneGame3D({ document, outputDir: output, resolveAsset, signal: request.signal });
1303
+ }
1304
+ else {
1305
+ const { buildStandaloneGame } = await import("@nodetool-ai/game-renderer/build");
1306
+ await buildStandaloneGame({ document, outputDir: output, resolveAsset });
1307
+ }
1308
+ const files = [];
1309
+ const pending = [""];
1310
+ let totalBytes = 0;
1311
+ while (pending.length > 0) {
1312
+ const directory = pending.pop() ?? "";
1313
+ for (const entry of await readdir(join(output, directory), { withFileTypes: true })) {
1314
+ const path = directory ? `${directory}/${entry.name}` : entry.name;
1315
+ if (entry.isDirectory()) {
1316
+ pending.push(path);
1317
+ }
1318
+ else if (entry.isFile()) {
1319
+ const bytes = await readFile(join(output, path));
1320
+ totalBytes += bytes.byteLength;
1321
+ if (files.length >= MAX_BUILD_FILES || totalBytes > MAX_BUILD_BYTES) {
1322
+ throw new Error("Standalone build exceeds file or byte limits");
1268
1323
  }
1324
+ files.push({ path, bytes });
1269
1325
  }
1270
1326
  }
1271
- if (!files.some((file) => file.path === "index.html")) {
1272
- throw new Error("Standalone player entry is missing");
1273
- }
1274
- for (const file of files.filter((item) => item.path !== "index.html")) {
1275
- const contentType = file.path.endsWith(".js") ? "text/javascript"
1276
- : file.path.endsWith(".css") ? "text/css"
1277
- : file.path.endsWith(".json") ? "application/json"
1278
- : file.path.endsWith(".png") ? "image/png"
1279
- : file.path.endsWith(".jpg") ? "image/jpeg"
1280
- : file.path.endsWith(".webp") ? "image/webp"
1281
- : file.path.endsWith(".wav") ? "audio/wav"
1282
- : file.path.endsWith(".ogg") ? "audio/ogg"
1283
- : file.path.endsWith(".mp3") ? "audio/mpeg"
1284
- : file.path.endsWith(".ttf") ? "font/ttf"
1285
- : file.path.endsWith(".otf") ? "font/otf"
1286
- : file.path.endsWith(".glb") ? "model/gltf-binary"
1287
- : file.path.endsWith(".wasm") ? "application/wasm"
1288
- : "application/octet-stream";
1289
- await workspace.write(`${buildPath}/${file.path}`, file.bytes, contentType);
1290
- }
1291
- const index = files.find((file) => file.path === "index.html");
1292
- if (!index)
1293
- throw new Error("Standalone player entry is missing");
1294
- await workspace.write(indexPath, index.bytes, "text/html");
1295
- return { game_id: game.id, revision, build_path: buildPath, entry_path: indexPath, cached: false };
1296
- }
1297
- catch (error) {
1298
- return { error: error instanceof Error ? error.message : String(error) };
1299
- }
1300
- finally {
1301
- await rm(staging, { recursive: true, force: true });
1302
1327
  }
1328
+ const entryFile = entryPath.slice(buildPath.length + 1);
1329
+ const entry = files.find((file) => file.path === entryFile);
1330
+ if (!entry)
1331
+ throw new Error("Standalone build entry is missing");
1332
+ const contentType = (path) => BUILD_CONTENT_TYPES[path.slice(path.lastIndexOf(".") + 1)] ?? "application/octet-stream";
1333
+ // The entry file is written last, so its presence marks a complete build for the cache check above.
1334
+ for (const file of files.filter((item) => item !== entry)) {
1335
+ await workspace.write(`${buildPath}/${file.path}`, file.bytes, contentType(file.path));
1336
+ }
1337
+ await workspace.write(entryPath, entry.bytes, contentType(entry.path));
1338
+ return { ...done, cached: false };
1339
+ }
1340
+ catch (error) {
1341
+ return { error: error instanceof Error ? error.message : String(error) };
1342
+ }
1343
+ finally {
1344
+ await rm(staging, { recursive: true, force: true });
1345
+ }
1346
+ }
1347
+ const buildGame = {
1348
+ spec: gameSpecs[5],
1349
+ impl: async (run, args) => {
1350
+ const user = userId(run);
1351
+ const id = args["game_id"];
1352
+ if (!user || typeof id !== "string")
1353
+ return { error: "game_id is required in a user session" };
1354
+ const target = args["target"] ?? "web";
1355
+ if (target !== "web" && target !== "tauri")
1356
+ return { error: "target must be web or tauri" };
1357
+ const revision = args["revision"];
1358
+ const result = await buildOwnedNativeGame({ userId: user, gameId: id, revision: typeof revision === "string" ? revision : undefined,
1359
+ target, storage: run.context.assetStorage, signal: run.context.signal });
1360
+ if ("error" in result)
1361
+ return { error: result.error };
1362
+ const report = { game_id: result.game_id, revision: result.revision, target: result.target, build_path: result.build_path,
1363
+ entry_path: result.entry_path, cached: result.cached };
1364
+ return target === "tauri"
1365
+ ? { ...report, next: "The project builds desktop installers with `npm install && npm run build` in a checkout that has Rust and the Tauri prerequisites." }
1366
+ : report;
1303
1367
  }
1304
1368
  };
1305
1369
  const listExamples = {
@@ -1423,10 +1487,24 @@ const autoplay = {
1423
1487
  return { game_id: shortResourceId(game.id), revision: document.revision, ...route, win_tick: winTick, reached_targets: reachedTargets, level_stats: levelStats, state: snapshot };
1424
1488
  }
1425
1489
  };
1490
+ function withDraftRecovery(capability) {
1491
+ return { ...capability, impl: async (run, args) => {
1492
+ try {
1493
+ return await capability.impl(run, args);
1494
+ }
1495
+ catch (error) {
1496
+ if (error instanceof MissingGameDraftSourceError) {
1497
+ return { error: error.message, code: "game_draft_source_unavailable" };
1498
+ }
1499
+ throw error;
1500
+ }
1501
+ } };
1502
+ }
1426
1503
  export const module = {
1427
1504
  module: "game",
1428
1505
  exports: [create, get, save, install, playtest, buildGame, edit, capture, generateAsset, listExamples, getExample, installExample, autoplay,
1429
1506
  { spec: previewGameAuthoringSpec, impl: previewGameAuthoring },
1430
- { spec: applyGameAuthoringSpec, impl: applyGameAuthoring }]
1507
+ { spec: applyGameAuthoringSpec, impl: applyGameAuthoring },
1508
+ { spec: browseGameAssetsSpec, impl: browseGameAssets }].map(withDraftRecovery)
1431
1509
  };
1432
1510
  //# sourceMappingURL=game.js.map