summer-engine 3.4.1 → 3.5.1

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 (63) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +5 -1
  3. package/.codex-plugin/plugin.json +5 -1
  4. package/.cursor-plugin/plugin.json +5 -1
  5. package/.factory-plugin/plugin.json +5 -1
  6. package/CHANGELOG.md +31 -0
  7. package/README.md +5 -1
  8. package/dist/cli/commands/setup.js +24 -4
  9. package/dist/core/capabilities/capture-gameplay.d.ts +72 -7
  10. package/dist/core/capabilities/capture-gameplay.js +386 -25
  11. package/dist/core/capabilities/export-engine-version.d.ts +7 -0
  12. package/dist/core/capabilities/export-engine-version.js +9 -0
  13. package/dist/core/capabilities/export-game.d.ts +4 -0
  14. package/dist/core/capabilities/export-game.js +50 -7
  15. package/dist/core/capabilities/image-generation.d.ts +12 -2
  16. package/dist/core/capabilities/image-generation.js +31 -2
  17. package/dist/core/capabilities/publish-build.d.ts +4 -0
  18. package/dist/core/capabilities/publish-build.js +84 -37
  19. package/dist/core/capabilities/runtime-control.d.ts +2 -2
  20. package/dist/core/capabilities/summer-bundle.d.ts +9 -0
  21. package/dist/core/capabilities/summer-bundle.js +19 -0
  22. package/dist/core/capabilities/tool-dispatch.js +13 -2
  23. package/dist/core/library-read.d.ts +7 -0
  24. package/dist/core/library-read.js +59 -7
  25. package/dist/core/library-search.d.ts +2 -0
  26. package/dist/installer/agent-config.d.ts +26 -1
  27. package/dist/installer/agent-config.js +180 -5
  28. package/dist/installer/agent-table.d.ts +13 -0
  29. package/dist/installer/agent-table.js +120 -0
  30. package/dist/mcp/tools/creator-tools.js +32 -3
  31. package/dist/mcp/tools/generate-tools.js +11 -30
  32. package/dist/mcp/tools/library-tools.d.ts +2 -1
  33. package/dist/mcp/tools/library-tools.js +1 -0
  34. package/gemini-extension.json +5 -1
  35. package/integrations/README.md +6 -0
  36. package/library/references/mcp-tools-reference/mcp-tools-reference.md +1 -1
  37. package/library/skills/README.md +11 -1
  38. package/library/skills/export-and-ship/SKILL.md +1 -1
  39. package/library/skills/export-and-ship/resource.yaml +2 -1
  40. package/library/skills/grow-analytics/SKILL.md +21 -0
  41. package/library/skills/grow-analytics/resource.yaml +27 -0
  42. package/library/skills/publish-your-game/SKILL.md +21 -0
  43. package/library/skills/publish-your-game/resource.yaml +29 -0
  44. package/library/skills/store-art/SKILL.md +23 -0
  45. package/library/skills/store-art/resource.yaml +30 -0
  46. package/library/skills/store-listing/SKILL.md +21 -0
  47. package/library/skills/store-listing/resource.yaml +25 -0
  48. package/library/skills/summer-analytics/resource.yaml +1 -0
  49. package/library/tools/capture-gameplay/resource.yaml +66 -1
  50. package/library/tools/export-game/resource.yaml +3 -0
  51. package/library/tools/generate-image/resource.yaml +4 -2
  52. package/package.json +1 -1
  53. package/registry/generated/counts.json +2 -2
  54. package/registry/generated/gemini-extension.json +5 -1
  55. package/registry/generated/index.json +153 -10
  56. package/registry/generated/marketplace.claude.json +1 -1
  57. package/registry/generated/plugin.claude.json +5 -1
  58. package/registry/generated/plugin.codex.json +5 -1
  59. package/registry/generated/plugin.cursor.json +5 -1
  60. package/registry/generated/plugin.factory.json +5 -1
  61. package/registry/generated/skills-index.md +11 -1
  62. package/registry/generated/skills-registry.json +52 -0
  63. package/registry/schemas/skill.schema.json +5 -0
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
+ /** Image generation can take over a minute on the larger models; wait this long for the server. */
3
+ export declare const IMAGE_GENERATION_TIMEOUT_MS = 180000;
2
4
  /** Shared MCP/CLI image request contract; optional flags preserve omission. */
3
5
  export declare const imageGenerationArgsSchema: z.ZodObject<{
4
6
  prompt: z.ZodString;
5
- model: z.ZodDefault<z.ZodString>;
7
+ model: z.ZodOptional<z.ZodString>;
6
8
  style: z.ZodDefault<z.ZodString>;
7
9
  referenceImageUrl: z.ZodOptional<z.ZodString>;
8
10
  removeBackground: z.ZodOptional<z.ZodBoolean>;
@@ -10,13 +12,15 @@ export declare const imageGenerationArgsSchema: z.ZodObject<{
10
12
  width: z.ZodOptional<z.ZodNumber>;
11
13
  height: z.ZodOptional<z.ZodNumber>;
12
14
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
15
+ out: z.ZodOptional<z.ZodString>;
13
16
  }, "strip", z.ZodTypeAny, {
14
17
  prompt: string;
15
- model: string;
16
18
  style: string;
17
19
  width?: number | undefined;
18
20
  height?: number | undefined;
19
21
  options?: Record<string, any> | undefined;
22
+ out?: string | undefined;
23
+ model?: string | undefined;
20
24
  referenceImageUrl?: string | undefined;
21
25
  removeBackground?: boolean | undefined;
22
26
  aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | undefined;
@@ -25,9 +29,15 @@ export declare const imageGenerationArgsSchema: z.ZodObject<{
25
29
  width?: number | undefined;
26
30
  height?: number | undefined;
27
31
  options?: Record<string, any> | undefined;
32
+ out?: string | undefined;
28
33
  model?: string | undefined;
29
34
  style?: string | undefined;
30
35
  referenceImageUrl?: string | undefined;
31
36
  removeBackground?: boolean | undefined;
32
37
  aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | undefined;
33
38
  }>;
39
+ /**
40
+ * Save a generated image where the caller asked (a folder or a file path), or
41
+ * under TMPDIR/summer-gen. Returns the path, or null when the download failed.
42
+ */
43
+ export declare function saveGeneratedImage(url: string, out?: string, prefix?: string): Promise<string | null>;
@@ -1,11 +1,17 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, extname, join, resolve } from "node:path";
1
4
  import { z } from "zod";
5
+ /** Image generation can take over a minute on the larger models; wait this long for the server. */
6
+ export const IMAGE_GENERATION_TIMEOUT_MS = 180_000;
7
+ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp"]);
2
8
  /** Shared MCP/CLI image request contract; optional flags preserve omission. */
3
9
  export const imageGenerationArgsSchema = z.object({
4
10
  prompt: z.string().describe("Description of the image to generate"),
5
11
  model: z
6
12
  .string()
7
- .default("nano-banana-2")
8
- .describe("Model name or full provider ID"),
13
+ .optional()
14
+ .describe("Model name or full provider ID. Omit it and the server picks one that reaches the requested size (nano-banana-2 when no size is asked)."),
9
15
  style: z
10
16
  .string()
11
17
  .default("realistic")
@@ -37,4 +43,27 @@ export const imageGenerationArgsSchema = z.object({
37
43
  .record(z.any())
38
44
  .optional()
39
45
  .describe("Extra model params (seed, negative_prompt, ...). The result lists any it did not use in ignoredOptions."),
46
+ out: z
47
+ .string()
48
+ .optional()
49
+ .describe("Where to save the image on this machine: a folder, or a file path ending in .png, .jpg or .webp. Default: <TMPDIR>/summer-gen/."),
40
50
  });
51
+ /**
52
+ * Save a generated image where the caller asked (a folder or a file path), or
53
+ * under TMPDIR/summer-gen. Returns the path, or null when the download failed.
54
+ */
55
+ export async function saveGeneratedImage(url, out, prefix = "img") {
56
+ try {
57
+ const target = out?.trim() ? resolve(out.trim()) : join(tmpdir(), "summer-gen");
58
+ const file = IMAGE_EXTENSIONS.has(extname(target).toLowerCase()) ? target : join(target, `${prefix}-${Date.now()}.png`);
59
+ await mkdir(dirname(file), { recursive: true });
60
+ const res = await fetch(url, { signal: AbortSignal.timeout(60_000) });
61
+ if (!res.ok)
62
+ return null;
63
+ await writeFile(file, Buffer.from(await res.arrayBuffer()));
64
+ return file;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
@@ -51,6 +51,10 @@ export declare function readGrants(body: Record<string, any>): Grant[];
51
51
  * Upload the given parts: signed in batches, PUT from disk, re-signed when
52
52
  * refused or about to expire. `sign` asks the store for URLs (build
53
53
  * publications bind each to its part SHA-256; store versions to its length).
54
+ * Each sign asks for every part not yet uploaded, including the parts other
55
+ * workers hold, so one sign usually covers them all. A part fails to sign only
56
+ * when the store was asked for it and did not sign it; the first fatal error
57
+ * stops the other workers and aborts their uploads.
54
58
  */
55
59
  export declare function uploadParts(sign: (partNumbers: number[]) => Promise<Grant[]>, file: string, sizeBytes: number, plan: {
56
60
  partSizeBytes: number;
@@ -8,7 +8,7 @@ import { appendStoreJsonLine } from "../store.js";
8
8
  import { readJsonResponse } from "../util/http.js";
9
9
  import { readLastExport } from "./export-game.js";
10
10
  import { publishDownload } from "./publish-download.js";
11
- import { BuildToolError, readSummerBundle } from "./summer-bundle.js";
11
+ import { BuildToolError, declarationMismatch, readSummerBundle } from "./summer-bundle.js";
12
12
  /**
13
13
  * summer_publish_build: upload a summer.games export to the creator's game
14
14
  * through the same creator store routes Studio's export upload uses
@@ -47,7 +47,7 @@ const INTAKE_FAILURES = {
47
47
  game_identity_mismatch: "This export belongs to another game. Upload it to its original game, or make a portable export for this game.",
48
48
  bundle_invalid: "Summer could not read this export. Export the game again and upload the new file.",
49
49
  declaration_mismatch: "The export's game or settings do not match this upload. Use the original game or export a new version with the intended settings.",
50
- template_set_unavailable: "Summer Games cannot run this Summer version yet. Update Summer Engine, export again and upload the new file.",
50
+ template_set_unavailable: "Summer Games cannot run this Summer version yet. If you exported with the newest Summer Engine, Summer Games may still be turning that version on: retry the same upload later, there is nothing to update. With an older engine, update Summer Engine and export again.",
51
51
  client_version_conflict: "This version number already belongs to a different export. Use a new clientVersion.",
52
52
  artifact_exists: "This exact export is already uploaded for this game. Export a new version and upload that.",
53
53
  approval_denied: "Summer Games did not approve this game, so it cannot take new builds.",
@@ -185,19 +185,28 @@ async function readPart(path, partNumber, partSize, sizeBytes) {
185
185
  * Upload the given parts: signed in batches, PUT from disk, re-signed when
186
186
  * refused or about to expire. `sign` asks the store for URLs (build
187
187
  * publications bind each to its part SHA-256; store versions to its length).
188
+ * Each sign asks for every part not yet uploaded, including the parts other
189
+ * workers hold, so one sign usually covers them all. A part fails to sign only
190
+ * when the store was asked for it and did not sign it; the first fatal error
191
+ * stops the other workers and aborts their uploads.
188
192
  */
189
193
  export async function uploadParts(sign, file, sizeBytes, plan, deps, parts = Array.from({ length: plan.partCount }, (_, index) => index + 1)) {
190
194
  const queue = [...parts];
195
+ const unfinished = new Set(parts);
191
196
  const grants = new Map();
197
+ const stop = new AbortController();
198
+ let failure;
192
199
  let signing = null;
193
200
  const fresh = (grant) => grant !== undefined && grant.expiresAt - deps.now() > 60_000;
194
201
  const grantFor = async (partNumber) => {
195
- for (let attempt = 0; attempt < 3; attempt++) {
196
- if (fresh(grants.get(partNumber)))
197
- return grants.get(partNumber);
202
+ for (;;) {
203
+ const grant = grants.get(partNumber);
204
+ if (fresh(grant))
205
+ return grant;
198
206
  if (!signing) {
199
- const batch = [...new Set([partNumber, ...queue.filter((n) => !fresh(grants.get(n)))])].slice(0, MAX_SIGNED_PARTS);
200
- signing = sign(batch)
207
+ const others = [...unfinished].filter((n) => n !== partNumber && !fresh(grants.get(n)));
208
+ const batch = [partNumber, ...others].slice(0, MAX_SIGNED_PARTS);
209
+ const done = sign(batch)
201
210
  .then((signed) => {
202
211
  for (const grant of signed)
203
212
  grants.set(grant.partNumber, grant);
@@ -205,43 +214,67 @@ export async function uploadParts(sign, file, sizeBytes, plan, deps, parts = Arr
205
214
  .finally(() => {
206
215
  signing = null;
207
216
  });
217
+ signing = { batch, done };
218
+ }
219
+ const { batch, done } = signing;
220
+ await done;
221
+ // The store was asked for this part and did not sign it (or signed it already expiring).
222
+ if (batch.includes(partNumber) && !fresh(grants.get(partNumber))) {
223
+ throw new BuildToolError("store_sign_failed", `The store did not sign part ${partNumber}.`, "Recovery: retry; a retry continues the same upload.");
208
224
  }
209
- await signing;
210
225
  }
211
- throw new BuildToolError("store_sign_failed", `The store did not sign part ${partNumber}.`, "Recovery: retry; a retry continues the same upload.");
212
226
  };
213
- const worker = async () => {
214
- for (let partNumber = queue.shift(); partNumber !== undefined; partNumber = queue.shift()) {
215
- const body = await readPart(file, partNumber, plan.partSizeBytes, sizeBytes);
216
- for (let attempt = 0;; attempt++) {
227
+ const upload = async (partNumber) => {
228
+ const body = await readPart(file, partNumber, plan.partSizeBytes, sizeBytes);
229
+ for (let attempt = 0;; attempt++) {
230
+ let status = null;
231
+ let error = null;
232
+ try {
217
233
  const grant = await grantFor(partNumber);
218
- let status = null;
219
- try {
220
- const response = await deps.fetch(grant.url, {
221
- method: "PUT",
222
- headers: grant.headers,
223
- body,
224
- redirect: "error",
225
- signal: AbortSignal.timeout(600_000),
226
- });
227
- if (response.ok)
228
- break;
229
- status = response.status;
230
- }
231
- catch {
232
- status = null;
233
- }
234
- // An expired or refused signature gets a new URL on the next try.
235
- if (status === 400 || status === 403)
236
- grants.delete(partNumber);
237
- if (attempt >= PART_RETRIES) {
238
- throw new BuildToolError("store_part_upload_failed", `Part ${partNumber} of ${plan.partCount} did not upload${status ? ` (${status})` : ""}.`, "Recovery: retry with the same file and clientVersion; the upload continues where it stopped.");
239
- }
240
- await deps.sleep(500 * 2 ** attempt);
234
+ const response = await deps.fetch(grant.url, {
235
+ method: "PUT",
236
+ headers: grant.headers,
237
+ body,
238
+ redirect: "error",
239
+ signal: AbortSignal.any([stop.signal, AbortSignal.timeout(600_000)]),
240
+ });
241
+ if (response.ok)
242
+ return;
243
+ status = response.status;
244
+ }
245
+ catch (caught) {
246
+ error = caught;
247
+ }
248
+ if (stop.signal.aborted)
249
+ return;
250
+ // An expired or refused signature gets a new URL on the next try.
251
+ if (status === 400 || status === 403)
252
+ grants.delete(partNumber);
253
+ if (attempt >= PART_RETRIES) {
254
+ if (error instanceof BuildToolError)
255
+ throw error;
256
+ throw new BuildToolError("store_part_upload_failed", `Part ${partNumber} of ${plan.partCount} did not upload${status ? ` (${status})` : ""}.`, "Recovery: retry with the same file and clientVersion; the upload continues where it stopped.");
257
+ }
258
+ await deps.sleep(500 * 2 ** attempt);
259
+ if (stop.signal.aborted)
260
+ return;
261
+ }
262
+ };
263
+ const worker = async () => {
264
+ for (let partNumber = queue.shift(); partNumber !== undefined && !stop.signal.aborted; partNumber = queue.shift()) {
265
+ try {
266
+ await upload(partNumber);
267
+ unfinished.delete(partNumber);
268
+ }
269
+ catch (error) {
270
+ failure ??= error;
271
+ stop.abort();
241
272
  }
242
273
  }
243
274
  };
244
275
  await Promise.all(Array.from({ length: Math.min(CONCURRENCY, queue.length) }, worker));
276
+ if (stop.signal.aborted)
277
+ throw failure;
245
278
  }
246
279
  export function normalizeClientVersion(value) {
247
280
  const raw = value?.trim() ?? "";
@@ -252,6 +285,10 @@ export function normalizeClientVersion(value) {
252
285
  return version;
253
286
  }
254
287
  function targetPlatforms(bundle) {
288
+ const mismatch = declarationMismatch(bundle);
289
+ if (mismatch) {
290
+ throw new BuildToolError("declaration_mismatch", `The store would refuse this upload (declaration_mismatch): ${mismatch}. Nothing was uploaded and no version was used.`, "Recovery: export again with summer_export_game alignDeclaration:true (it sets summer.build.json targetPlatforms to the targets you export), or with targets equal to what summer.build.json declares; then upload the new export.");
291
+ }
255
292
  if (bundle.hosted)
256
293
  return bundle.targetPlatforms;
257
294
  const other = bundle.targetPlatforms.filter((platform) => !STANDALONE_TARGETS.includes(platform));
@@ -421,7 +458,17 @@ export async function publishBuild(input, overrides = {}) {
421
458
  break;
422
459
  if (publication.state === "failed" || publication.state === "cancelled") {
423
460
  const code = typeof publication.errorCode === "string" ? publication.errorCode : "publication_failed";
424
- throw new BuildToolError(code, (code === "declaration_mismatch" && publication.errorMessage) || INTAKE_FAILURES[code] || publication.errorMessage || "Summer could not make a build from this upload.", "Recovery: export again, use a new clientVersion, and retry.", undefined, { publicationId });
461
+ throw new BuildToolError(code, ((code === "declaration_mismatch" || code === "template_set_unavailable") && publication.errorMessage) ||
462
+ INTAKE_FAILURES[code] ||
463
+ publication.errorMessage ||
464
+ "Summer could not make a build from this upload.",
465
+ // The store checks the declaration and the bundle before it records a
466
+ // client pack, so those refusals leave the version free.
467
+ code === "declaration_mismatch" || code === "bundle_invalid"
468
+ ? "Recovery: fix what the message names, export again and retry; the same clientVersion still works, because a refused upload records no version."
469
+ : code === "template_set_unavailable"
470
+ ? `Recovery: ${INTAKE_FAILURES.template_set_unavailable.replace(/^Summer Games cannot run this Summer version yet\. /, "")} The same clientVersion works on the retry.`
471
+ : "Recovery: export again, use a new clientVersion, and retry.", undefined, { publicationId });
425
472
  }
426
473
  if (deps.now() >= deadline) {
427
474
  const result = {
@@ -186,8 +186,8 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
186
186
  value?: string | number | boolean | undefined;
187
187
  instance?: string | undefined;
188
188
  name?: string | undefined;
189
- cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
190
189
  space?: "local" | "global" | "both" | undefined;
190
+ cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
191
191
  blend?: number | undefined;
192
192
  position?: number | undefined;
193
193
  speed?: number | undefined;
@@ -205,8 +205,8 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
205
205
  value?: string | number | boolean | undefined;
206
206
  instance?: string | undefined;
207
207
  name?: string | undefined;
208
- cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
209
208
  space?: "local" | "global" | "both" | undefined;
209
+ cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
210
210
  blend?: number | undefined;
211
211
  position?: number | undefined;
212
212
  speed?: number | undefined;
@@ -6,6 +6,15 @@
6
6
  */
7
7
  export declare const BUNDLE_SCHEMA = "summer.bundle.v1";
8
8
  export declare const BUNDLE_MANIFEST = "summer-bundle.json";
9
+ /** The same platforms, in any order. */
10
+ export declare function samePlatforms(a: readonly string[], b: readonly string[]): boolean;
11
+ /**
12
+ * Why the store will refuse this hosted bundle's Build declaration, or null.
13
+ * The store requires summer.build.json's targetPlatforms to equal the
14
+ * platforms the client pack was exported for (declaration_mismatch), and the
15
+ * refusal spends the clientVersion, so this is checked before any upload.
16
+ */
17
+ export declare function declarationMismatch(bundle: Pick<SummerBundle, "hostedBuild" | "targetPlatforms">): string | null;
9
18
  /** A failure with a stable code, a plain message and the next step. */
10
19
  export declare class BuildToolError extends Error {
11
20
  readonly code: string;
@@ -12,6 +12,25 @@ export const BUNDLE_MANIFEST = "summer-bundle.json";
12
12
  const BUNDLE_CLIENT = "client.pck";
13
13
  const BUNDLE_SERVER = "server.pck";
14
14
  const BUNDLE_BUILD = "config/summer.build.json";
15
+ /** The same platforms, in any order. */
16
+ export function samePlatforms(a, b) {
17
+ return a.length === b.length && [...a].sort().join(",") === [...b].sort().join(",");
18
+ }
19
+ /**
20
+ * Why the store will refuse this hosted bundle's Build declaration, or null.
21
+ * The store requires summer.build.json's targetPlatforms to equal the
22
+ * platforms the client pack was exported for (declaration_mismatch), and the
23
+ * refusal spends the clientVersion, so this is checked before any upload.
24
+ */
25
+ export function declarationMismatch(bundle) {
26
+ const declared = bundle.hostedBuild?.targetPlatforms;
27
+ if (!Array.isArray(declared))
28
+ return null;
29
+ const platforms = declared.map(String);
30
+ return samePlatforms(platforms, bundle.targetPlatforms)
31
+ ? null
32
+ : `the bundle targets ${[...bundle.targetPlatforms].sort().join(", ")} but its summer.build.json declares ${[...platforms].sort().join(", ")}`;
33
+ }
15
34
  const SHA256 = /^sha256:[0-9a-f]{64}$/;
16
35
  const AS_EXPORTED = "Recovery: export the game again with the summer.games preset and use the .zip exactly as Summer Engine wrote it.";
17
36
  /** A failure with a stable code, a plain message and the next step. */
@@ -28,7 +28,7 @@ import { executeSceneMutation, occurrenceCount, readTextPayload, safeProjectPath
28
28
  import { extractOpError, withOldEngineHint } from "./engine-receipt.js";
29
29
  import { lookupApiDocs } from "./api-docs.js";
30
30
  import { BOARD_ENDPOINT, boardArgsSchema, boardQuery } from "./board.js";
31
- import { imageGenerationArgsSchema } from "./image-generation.js";
31
+ import { IMAGE_GENERATION_TIMEOUT_MS, imageGenerationArgsSchema, saveGeneratedImage } from "./image-generation.js";
32
32
  import { buildMotionRequestBody, motionGenerationArgsSchema } from "./motion-generation.js";
33
33
  import { ImportHdriError, importHdriArgsSchema, importPolyHavenHdri } from "./hdri-import.js";
34
34
  import { FABRICATE_FALLBACK, buildFabricateMeshOp, fabricateArgsSchema } from "./fabricate-mesh.js";
@@ -618,6 +618,7 @@ export const TOOL_DISPATCH = [
618
618
  targets: Array.isArray(args.targets) ? args.targets.map(String) : undefined,
619
619
  format: optStr(args, "format"),
620
620
  debug: args.debug === true,
621
+ alignDeclaration: args.alignDeclaration === true,
621
622
  ...(typeof args.timeoutSeconds === "number" ? { timeoutMs: args.timeoutSeconds * 1000 } : {}),
622
623
  })),
623
624
  entry("summer_export_templates", "List or install Summer export templates from the CDN (web, macos, windows)", false, (args) => {
@@ -639,6 +640,8 @@ export const TOOL_DISPATCH = [
639
640
  waitSeconds: optNumberOrUndefined(args, "waitSeconds"),
640
641
  intervalSeconds: optNumberOrUndefined(args, "intervalSeconds"),
641
642
  out: optStr(args, "out"),
643
+ args: args.args,
644
+ steps: args.steps,
642
645
  })),
643
646
  entry("summer_publish_build", "Upload a summer.games export to your store game (confirm-gated)", false, (args) => publishBuild({
644
647
  gameId: optStr(args, "gameId"),
@@ -896,7 +899,15 @@ export const TOOL_DISPATCH = [
896
899
  const parsed = parseToolArgs(boardArgsSchema, args, "get-board");
897
900
  return gatewayGet(BOARD_ENDPOINT, boardQuery({ ...parsed, pictures: parsed.pictures ?? false }), 45_000);
898
901
  }),
899
- entry("summer_generate_image", "Generate or edit an image via Summer Studio", false, (args) => gatewayPost("/api/mcp/generate/image", parseToolArgs(imageGenerationArgsSchema, args, "generate-image"))),
902
+ entry("summer_generate_image", "Generate or edit an image via Summer Studio", false, async (args) => {
903
+ const { out, ...body } = parseToolArgs(imageGenerationArgsSchema, args, "generate-image");
904
+ const result = await gatewayPost("/api/mcp/generate/image", body, IMAGE_GENERATION_TIMEOUT_MS);
905
+ if (!out)
906
+ return result;
907
+ const asset = (result.asset ?? {});
908
+ const url = typeof asset.fileUrl === "string" ? asset.fileUrl : typeof asset.thumbnailUrl === "string" ? asset.thumbnailUrl : null;
909
+ return { ...result, localPath: url ? await saveGeneratedImage(url, out) : null };
910
+ }),
900
911
  entry("summer_slice_asset_sheet", "Detect and crop every asset from a generated sheet image", false, (args) => gatewayPost("/api/mcp/generate/slice-asset-sheet", { assetId: str(args, "assetId") }, 300_000)),
901
912
  entry("summer_generate_audio", "Generate speech, sound effects, music, or dialogue", false, (args) => {
902
913
  const body = { capability: str(args, "capability") };
@@ -10,6 +10,8 @@ export declare const FOOTER_SUFFIX = "If this entry is wrong, stale, or you devi
10
10
  export declare const LINKED_FILE_EXTENSIONS: ReadonlySet<string>;
11
11
  /** A linked file larger than this is refused rather than dumped. */
12
12
  export declare const LINKED_FILE_MAX_BYTES: number;
13
+ /** How long a hosted skill read waits for the hosted Summer Engine MCP. */
14
+ export declare const HOSTED_READ_TIMEOUT_MS = 8000;
13
15
  export declare const readLibraryInputShape: {
14
16
  id: z.ZodString;
15
17
  part: z.ZodOptional<z.ZodEnum<["skill", "resource", "all"]>>;
@@ -29,6 +31,8 @@ export interface LibraryReadDeps extends LibrarySearchDeps {
29
31
  /** Root holding library/ (default: the installed package). */
30
32
  packageRoot?: string;
31
33
  templates?: readonly TemplateEntry[];
34
+ /** Reads a hosted skill's text by resource URI; throws when it cannot. */
35
+ readHostedResource?: (uri: string) => Promise<string>;
32
36
  }
33
37
  export interface LibraryReadOk {
34
38
  ok: true;
@@ -57,6 +61,9 @@ export interface LibraryReadOk {
57
61
  target: string;
58
62
  id: string | null;
59
63
  }>;
64
+ /** Hosted skills: the resource the body comes from, and why it did not load. */
65
+ hosted_resource?: string;
66
+ hosted_error?: string;
60
67
  /** Tool records: how to reach it. */
61
68
  mcp_tool_name?: string;
62
69
  cli_command?: string;
@@ -31,11 +31,18 @@
31
31
  * ships it; and `reference/<slug>` that is no entry falls back to the one
32
32
  * `references/<slug>.md` an entry ships. A body render ends with its links and
33
33
  * the id that loads each. A linked file's footer names its entry.
34
+ *
35
+ * Hosted skills: a skill whose resource.yaml names a `hosted_resource`
36
+ * (summer://skills/<slug>) keeps its current text on the hosted Summer Engine
37
+ * MCP, the text the Studio agent reads. The body is read from there with the
38
+ * store sign-in; without it the local SKILL.md renders with the reason and how
39
+ * to load the full text. The entry also resolves by that resource URI.
34
40
  */
35
41
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
36
42
  import { join, posix, sep } from "node:path";
37
43
  import { z } from "zod";
38
44
  import { LIBRARY_KIND_DIRS, loadLibraryIndex, searchLibrary, } from "./library-search.js";
45
+ import { withHostedMcp } from "./hosted-mcp.js";
39
46
  import { PACKAGE_ROOT } from "./package-root.js";
40
47
  import { getTemplateRegistry } from "./templates.js";
41
48
  export const READ_PARTS = ["skill", "resource", "all"];
@@ -60,6 +67,8 @@ export const LINKED_FILE_EXTENSIONS = new Set([
60
67
  ]);
61
68
  /** A linked file larger than this is refused rather than dumped. */
62
69
  export const LINKED_FILE_MAX_BYTES = 256 * 1024;
70
+ /** How long a hosted skill read waits for the hosted Summer Engine MCP. */
71
+ export const HOSTED_READ_TIMEOUT_MS = 8_000;
63
72
  export const readLibraryInputShape = {
64
73
  id: z
65
74
  .string()
@@ -81,13 +90,13 @@ export function entryIdWithHash(id, contentHash) {
81
90
  return hash ? `${id}@${hash}` : id;
82
91
  }
83
92
  // ── Resolution ─────────────────────────────────────────────────────────────
84
- /** Exact id, an id with an @hash suffix (as the footer prints it), or a bare
85
- * slug that names exactly one entry. */
93
+ /** Exact id, an id with an @hash suffix (as the footer prints it), a hosted
94
+ * skill's resource URI, or a bare slug that names exactly one entry. */
86
95
  function resolveEntry(requested, entries) {
87
96
  const bare = requested.trim().replace(/@[a-f0-9]+$/i, "");
88
97
  if (!bare)
89
98
  return null;
90
- const exact = entries.find((entry) => entry.id === bare);
99
+ const exact = entries.find((entry) => entry.id === bare || entry.hosted_resource === bare);
91
100
  if (exact)
92
101
  return exact;
93
102
  if (!bare.includes("/")) {
@@ -324,7 +333,7 @@ function relatedMap(entry) {
324
333
  }
325
334
  return out;
326
335
  }
327
- function header(entry, slug, related) {
336
+ function header(entry, slug, related, hostedLoaded) {
328
337
  const lines = [];
329
338
  lines.push(`${entry.id} — ${entry.kind} v${entry.version ?? "?"} (${entry.status ?? "stable"})`);
330
339
  if (entry.summary)
@@ -338,7 +347,10 @@ function header(entry, slug, related) {
338
347
  const relatedIds = Object.values(related).flat();
339
348
  if (relatedIds.length > 0)
340
349
  lines.push(`related: ${relatedIds.join(", ")}`);
341
- if (entry.kind === "skill") {
350
+ if (hostedLoaded) {
351
+ lines.push(`The body is the current text from the hosted Summer Engine MCP (also the \`${slug}\` MCP prompt and the resource ${entry.hosted_resource}). Follow it, do not paraphrase it.`);
352
+ }
353
+ else if (entry.kind === "skill") {
342
354
  lines.push(`Invoke: the \`${slug}\` skill in your host (Claude Code: /${slug}); installed under its bare slug by \`summer setup\`. The body follows — follow it, do not paraphrase it.`);
343
355
  }
344
356
  return lines.join("\n");
@@ -451,7 +463,21 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
451
463
  let bodyTitle;
452
464
  let body;
453
465
  let links = [];
454
- if (kind === "tool") {
466
+ let hostedText = null;
467
+ let hostedError;
468
+ if (kind === "skill" && entry.hosted_resource && part !== "resource") {
469
+ try {
470
+ hostedText = (await (deps.readHostedResource ?? readHostedSkill)(entry.hosted_resource)).replace(/\s+$/, "");
471
+ }
472
+ catch (error) {
473
+ hostedError = error instanceof Error ? error.message : String(error);
474
+ }
475
+ }
476
+ if (hostedText !== null) {
477
+ bodyTitle = `${entry.hosted_resource} (hosted Summer Engine MCP)`;
478
+ body = hostedText;
479
+ }
480
+ else if (kind === "tool") {
455
481
  bodyTitle = "how to call";
456
482
  body = toolBody(entry, slug);
457
483
  }
@@ -474,11 +500,14 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
474
500
  body = `(no body file shipped for this ${kind}; the descriptor below is all there is)`;
475
501
  }
476
502
  }
477
- const sections = [header(entry, slug, related)];
503
+ const sections = [header(entry, slug, related, hostedText !== null)];
478
504
  if (part === "skill" || part === "all") {
479
505
  sections.push(`--- ${bodyTitle} ---\n${body}`);
480
506
  if (links.length > 0)
481
507
  sections.push(linksSection(links));
508
+ if (hostedError !== undefined) {
509
+ sections.push(`--- hosted text not loaded ---\nThe full current text of this skill is the resource ${entry.hosted_resource} on the hosted Summer Engine MCP, and it could not be read: ${hostedError}\nThe body above is only the local summary: load the full text before you act (here after the fix, or as the \`${slug}\` MCP prompt or resource in a host connected to the hosted Summer Engine MCP).`);
510
+ }
482
511
  }
483
512
  if (part === "resource" || part === "all")
484
513
  sections.push(`--- ${relPath}/resource.yaml ---\n${resourceYaml}`);
@@ -486,6 +515,10 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
486
515
  const result = { ...base, text: sections.join("\n\n") };
487
516
  if (bodyFile)
488
517
  result.body_file = bodyFile;
518
+ if (entry.hosted_resource)
519
+ result.hosted_resource = entry.hosted_resource;
520
+ if (hostedError !== undefined)
521
+ result.hosted_error = hostedError;
489
522
  if (links.length > 0 && part !== "resource")
490
523
  result.links = links;
491
524
  if (kind === "tool") {
@@ -499,6 +532,25 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
499
532
  }
500
533
  return result;
501
534
  }
535
+ /** A hosted skill's text, read from the hosted Summer Engine MCP with the store sign-in. */
536
+ async function readHostedSkill(uri) {
537
+ let timer;
538
+ const timeout = new Promise((_, reject) => {
539
+ timer = setTimeout(() => reject(new Error(`the hosted Summer Engine MCP did not answer within ${HOSTED_READ_TIMEOUT_MS / 1000} s.`)), HOSTED_READ_TIMEOUT_MS);
540
+ timer.unref?.();
541
+ });
542
+ try {
543
+ const result = await Promise.race([withHostedMcp((client) => client.readResource({ uri })), timeout]);
544
+ const contents = Array.isArray(result.contents) ? result.contents : [];
545
+ const text = contents.map((content) => (typeof content.text === "string" ? content.text : "")).join("\n").trim();
546
+ if (!text)
547
+ throw new Error("the hosted Summer Engine MCP returned no text for it.");
548
+ return text;
549
+ }
550
+ finally {
551
+ clearTimeout(timer);
552
+ }
553
+ }
502
554
  function safeTemplates() {
503
555
  try {
504
556
  return getTemplateRegistry();
@@ -49,6 +49,8 @@ export interface LibraryIndexEntry {
49
49
  cli_command?: string;
50
50
  authority?: Record<string, boolean>;
51
51
  recommended?: boolean;
52
+ /** The hosted Summer Engine MCP resource holding the skill's current text. */
53
+ hosted_resource?: string;
52
54
  }
53
55
  export type MatchedBy = "lexical" | "semantic";
54
56
  export interface LibrarySearchHit {
@@ -45,6 +45,31 @@ export declare function resolveAgentSelection(agentArg: string | undefined, agen
45
45
  /** `--scope` with a user-facing error; absent means "user". */
46
46
  export declare function resolveConfigScope(scopeOpt: string | undefined): ConfigScope;
47
47
  export declare function configureAgentMcp(options: AgentConfigOptions): Promise<AgentConfigResult>;
48
+ /** The Summer Games player MCP: friends, parties, messages and the player's
49
+ * inbox, as the signed-in player. A remote server; the agent signs in to it
50
+ * with OAuth in the browser, so setup writes no credential. */
51
+ export declare const SUMMER_GAMES_MCP_SERVER_NAME = "summer-games";
52
+ export declare const SUMMER_GAMES_MCP_URL = "https://mcp.summer.games/mcp";
53
+ export interface RemoteMcpResult {
54
+ /** The entry's name: summer-games, or the name the person already gave it. */
55
+ serverName: string;
56
+ url: string;
57
+ path: string;
58
+ changed: boolean;
59
+ wrote: boolean;
60
+ /** Set when the server was already listed under another name (e.g. a
61
+ * hand-added "summer"); setup then leaves the file alone. */
62
+ existingName?: string;
63
+ /** What the person does once to sign in (the agent opens summer.games). */
64
+ signIn: string;
65
+ }
66
+ /**
67
+ * Adds the Summer Games MCP next to summer-engine in the same config file, in
68
+ * the agent's own entry shape (remoteMcp in agent-table.ts). An entry that
69
+ * already points at the same URL, under any name, counts as added and is left
70
+ * as it is. Returns null for agents without remoteMcp.
71
+ */
72
+ export declare function configureSummerGamesMcp(options: Pick<AgentConfigOptions, "agent" | "scope" | "dryRun" | "cwd" | "env">): Promise<RemoteMcpResult | null>;
48
73
  export declare const DEFAULT_CHANNEL = "latest";
49
74
  /** npm dist-tags are lowercase words like `latest`, `next`, `beta`, `v3-preview`;
50
75
  * anything that could be read as a version or a path is refused (a bare
@@ -60,4 +85,4 @@ export declare function mcpEntry(format: McpFormat, server: StdioMcpServerConfig
60
85
  export declare function resolvePackageRoot(): string;
61
86
  /** Directory name Gemini expects the extension under; the manifest `name` must match it. */
62
87
  export declare const GEMINI_EXTENSION_DIR_NAME = "summer-engine";
63
- export declare function upsertTomlArrayBlock(content: string, block: string): string;
88
+ export declare function upsertTomlArrayBlock(content: string, block: string, name?: string): string;