summer-engine 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) 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 +23 -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.js +17 -3
  18. package/dist/core/capabilities/summer-bundle.d.ts +9 -0
  19. package/dist/core/capabilities/summer-bundle.js +19 -0
  20. package/dist/core/capabilities/tool-dispatch.js +13 -2
  21. package/dist/core/library-read.d.ts +7 -0
  22. package/dist/core/library-read.js +59 -7
  23. package/dist/core/library-search.d.ts +2 -0
  24. package/dist/installer/agent-config.d.ts +25 -0
  25. package/dist/installer/agent-config.js +114 -3
  26. package/dist/installer/agent-table.d.ts +12 -0
  27. package/dist/installer/agent-table.js +9 -0
  28. package/dist/mcp/tools/creator-tools.js +32 -3
  29. package/dist/mcp/tools/generate-tools.js +11 -30
  30. package/dist/mcp/tools/library-tools.d.ts +2 -1
  31. package/dist/mcp/tools/library-tools.js +1 -0
  32. package/gemini-extension.json +5 -1
  33. package/library/references/mcp-tools-reference/mcp-tools-reference.md +1 -1
  34. package/library/skills/README.md +11 -1
  35. package/library/skills/export-and-ship/SKILL.md +1 -1
  36. package/library/skills/export-and-ship/resource.yaml +2 -1
  37. package/library/skills/grow-analytics/SKILL.md +21 -0
  38. package/library/skills/grow-analytics/resource.yaml +27 -0
  39. package/library/skills/publish-your-game/SKILL.md +21 -0
  40. package/library/skills/publish-your-game/resource.yaml +29 -0
  41. package/library/skills/store-art/SKILL.md +23 -0
  42. package/library/skills/store-art/resource.yaml +30 -0
  43. package/library/skills/store-listing/SKILL.md +21 -0
  44. package/library/skills/store-listing/resource.yaml +25 -0
  45. package/library/skills/summer-analytics/resource.yaml +1 -0
  46. package/library/tools/capture-gameplay/resource.yaml +66 -1
  47. package/library/tools/export-game/resource.yaml +3 -0
  48. package/library/tools/generate-image/resource.yaml +4 -2
  49. package/package.json +1 -1
  50. package/registry/generated/counts.json +2 -2
  51. package/registry/generated/gemini-extension.json +5 -1
  52. package/registry/generated/index.json +153 -10
  53. package/registry/generated/marketplace.claude.json +1 -1
  54. package/registry/generated/plugin.claude.json +5 -1
  55. package/registry/generated/plugin.codex.json +5 -1
  56. package/registry/generated/plugin.cursor.json +5 -1
  57. package/registry/generated/plugin.factory.json +5 -1
  58. package/registry/generated/skills-index.md +11 -1
  59. package/registry/generated/skills-registry.json +52 -0
  60. 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
+ }
@@ -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.",
@@ -252,6 +252,10 @@ export function normalizeClientVersion(value) {
252
252
  return version;
253
253
  }
254
254
  function targetPlatforms(bundle) {
255
+ const mismatch = declarationMismatch(bundle);
256
+ if (mismatch) {
257
+ 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.");
258
+ }
255
259
  if (bundle.hosted)
256
260
  return bundle.targetPlatforms;
257
261
  const other = bundle.targetPlatforms.filter((platform) => !STANDALONE_TARGETS.includes(platform));
@@ -421,7 +425,17 @@ export async function publishBuild(input, overrides = {}) {
421
425
  break;
422
426
  if (publication.state === "failed" || publication.state === "cancelled") {
423
427
  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 });
428
+ throw new BuildToolError(code, ((code === "declaration_mismatch" || code === "template_set_unavailable") && publication.errorMessage) ||
429
+ INTAKE_FAILURES[code] ||
430
+ publication.errorMessage ||
431
+ "Summer could not make a build from this upload.",
432
+ // The store checks the declaration and the bundle before it records a
433
+ // client pack, so those refusals leave the version free.
434
+ code === "declaration_mismatch" || code === "bundle_invalid"
435
+ ? "Recovery: fix what the message names, export again and retry; the same clientVersion still works, because a refused upload records no version."
436
+ : code === "template_set_unavailable"
437
+ ? `Recovery: ${INTAKE_FAILURES.template_set_unavailable.replace(/^Summer Games cannot run this Summer version yet\. /, "")} The same clientVersion works on the retry.`
438
+ : "Recovery: export again, use a new clientVersion, and retry.", undefined, { publicationId });
425
439
  }
426
440
  if (deps.now() >= deadline) {
427
441
  const result = {
@@ -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.
68
+ * An entry that already points at the same URL, under any name, counts as
69
+ * added and is left as it is. Returns null for agents without a remote-server
70
+ * entry in agent-table.ts.
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
@@ -73,6 +73,116 @@ export async function configureAgentMcp(options) {
73
73
  nextSteps: [`Updated ${target.path}.`, spec.restart],
74
74
  };
75
75
  }
76
+ /** The Summer Games player MCP: friends, parties, messages and the player's
77
+ * inbox, as the signed-in player. A remote server; the agent signs in to it
78
+ * with OAuth in the browser, so setup writes no credential. */
79
+ export const SUMMER_GAMES_MCP_SERVER_NAME = "summer-games";
80
+ export const SUMMER_GAMES_MCP_URL = "https://mcp.summer.games/mcp";
81
+ /**
82
+ * Adds the Summer Games MCP next to summer-engine in the same config file.
83
+ * An entry that already points at the same URL, under any name, counts as
84
+ * added and is left as it is. Returns null for agents without a remote-server
85
+ * entry in agent-table.ts.
86
+ */
87
+ export async function configureSummerGamesMcp(options) {
88
+ const spec = agentSpec(options.agent);
89
+ if (!spec.remoteMcp)
90
+ return null;
91
+ const env = options.env ?? process.env;
92
+ const target = resolveConfigTarget(spec, options.scope, resolve(options.cwd ?? process.cwd()), env);
93
+ const write = !options.dryRun;
94
+ const url = SUMMER_GAMES_MCP_URL;
95
+ let existing;
96
+ let changed = false;
97
+ if (spec.remoteMcp.format === "toml-url") {
98
+ const current = await readTextFileIfExists(target.path);
99
+ existing = findTomlServerByUrl(current, url);
100
+ if (!existing) {
101
+ const name = SUMMER_GAMES_MCP_SERVER_NAME;
102
+ const next = upsertTomlTable(current, `[mcp_servers.${name}]\nurl = ${tomlString(url)}\n`, name);
103
+ changed = current !== next;
104
+ if (write && changed)
105
+ await writeTextFile(target.path, next);
106
+ }
107
+ }
108
+ else {
109
+ const current = await readJsonConfig(target.path);
110
+ existing = findJsonServerByUrl(current, url);
111
+ if (!existing) {
112
+ const next = copyJsonObject(current);
113
+ next.mcpServers = mergeNamedObject(next.mcpServers, SUMMER_GAMES_MCP_SERVER_NAME, { type: "http", url }, "mcpServers");
114
+ const nextRendered = renderJsonFile(next);
115
+ changed = renderJsonFile(current) !== nextRendered;
116
+ if (write && changed)
117
+ await writeTextFile(target.path, nextRendered);
118
+ }
119
+ }
120
+ const serverName = existing ?? SUMMER_GAMES_MCP_SERVER_NAME;
121
+ return {
122
+ serverName,
123
+ url,
124
+ path: target.path,
125
+ changed,
126
+ wrote: write && changed,
127
+ ...(existing && existing !== SUMMER_GAMES_MCP_SERVER_NAME ? { existingName: existing } : {}),
128
+ signIn: spec.remoteMcp.signIn(serverName),
129
+ };
130
+ }
131
+ function sameMcpUrl(a, b) {
132
+ if (typeof a !== "string")
133
+ return false;
134
+ const normalize = (value) => {
135
+ try {
136
+ const parsed = new URL(value.trim());
137
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`.toLowerCase();
138
+ }
139
+ catch {
140
+ return value.trim().toLowerCase();
141
+ }
142
+ };
143
+ return normalize(a) === normalize(b);
144
+ }
145
+ /** The name of a server with this URL in mcpServers. Claude Code's
146
+ * ~/.claude.json also keeps per-project servers (`claude mcp add` without
147
+ * --scope user) under projects.<dir>.mcpServers; those count too. */
148
+ function findJsonServerByUrl(config, url) {
149
+ const maps = [config.mcpServers];
150
+ if (isJsonObject(config.projects)) {
151
+ for (const project of Object.values(config.projects)) {
152
+ if (isJsonObject(project))
153
+ maps.push(project.mcpServers);
154
+ }
155
+ }
156
+ for (const map of maps) {
157
+ if (!isJsonObject(map))
158
+ continue;
159
+ for (const [name, entry] of Object.entries(map)) {
160
+ if (isJsonObject(entry) && sameMcpUrl(entry.url, url))
161
+ return name;
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+ /** The name of a [mcp_servers.<name>] table whose url is this URL. */
167
+ function findTomlServerByUrl(content, url) {
168
+ let table;
169
+ for (const line of content.split(/\r?\n/)) {
170
+ const header = /^\s*\[([^\[\]]+)\]\s*(?:#.*)?$/.exec(line);
171
+ if (header) {
172
+ const server = /^mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))$/.exec(header[1].trim());
173
+ table = server ? (server[1] ?? server[2]) : undefined;
174
+ continue;
175
+ }
176
+ if (/^\s*\[/.test(line)) {
177
+ table = undefined;
178
+ continue;
179
+ }
180
+ const value = /^\s*url\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(line);
181
+ if (table && value && sameMcpUrl(value[1] ?? value[2], url))
182
+ return table;
183
+ }
184
+ return undefined;
185
+ }
76
186
  async function upsertConfig(format, path, server, write) {
77
187
  switch (format) {
78
188
  case "toml":
@@ -470,10 +580,11 @@ async function readTextFileIfExists(path) {
470
580
  throw new Error(`Could not read ${path}: ${formatError(error)}`);
471
581
  }
472
582
  }
473
- function upsertTomlTable(content, table) {
583
+ function upsertTomlTable(content, table, name = SUMMER_MCP_SERVER_NAME) {
474
584
  const lines = content.split(/\r?\n/);
475
- const serverHeader = /^\s*\[mcp_servers\.(?:"summer-engine"|summer-engine)(?:\.|\])/;
476
- const start = lines.findIndex((line) => /^\s*\[mcp_servers\.(?:"summer-engine"|summer-engine)\]\s*(?:#.*)?$/.test(line));
585
+ const key = name.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
586
+ const serverHeader = new RegExp(`^\\s*\\[mcp_servers\\.(?:"${key}"|${key})(?:\\.|\\])`);
587
+ const start = lines.findIndex((line) => new RegExp(`^\\s*\\[mcp_servers\\.(?:"${key}"|${key})\\]\\s*(?:#.*)?$`).test(line));
477
588
  if (start === -1) {
478
589
  const trimmed = content.endsWith("\n") || content === "" ? content : `${content}\n`;
479
590
  return `${trimmed}${trimmed === "" ? "" : "\n"}${table}`;
@@ -22,6 +22,7 @@ export type ConfigScope = "user" | "project";
22
22
  * json-gemini the Gemini extension manifest (legacy)
23
23
  */
24
24
  export type McpFormat = "json" | "json-stdio" | "json-vscode" | "json-copilot" | "json-opencode" | "json-servers" | "json-zed" | "json-amp" | "json-crush" | "json-transport" | "toml" | "toml-array" | "yaml-goose" | "yaml-hermes" | "json-gemini";
25
+ export type RemoteMcpFormat = "json-http" | "toml-url";
25
26
  export type SkillLocationKind = "skill-dir" | "cursor-rule-dir" | "windsurf-rule-file" | "cline-rule-dir" | "opencode-skill-dir";
26
27
  export interface PathContext {
27
28
  home: string;
@@ -70,6 +71,17 @@ export interface AgentSpec {
70
71
  /** Env var that overrides the MCP config path (tests, unusual installs). */
71
72
  envOverride: string;
72
73
  format: McpFormat;
74
+ /**
75
+ * How this agent lists a remote MCP server that signs in with OAuth on its
76
+ * own (the Summer Games player MCP). Absent: setup links the engine only.
77
+ * json-http { mcpServers: { <name>: { type: "http", url } } } (Claude Code)
78
+ * toml-url [mcp_servers.<name>] url (Codex)
79
+ * signIn tells the person how to sign in once, for the entry's name.
80
+ */
81
+ remoteMcp?: {
82
+ format: RemoteMcpFormat;
83
+ signIn: (name: string) => string;
84
+ };
73
85
  mcp: {
74
86
  /** null: user-level servers are managed in the app's UI; a user request writes the project file with a warning. */
75
87
  user: ((ctx: PathContext) => string) | null;
@@ -108,6 +108,7 @@ const SPECS = [
108
108
  status: "active",
109
109
  envOverride: "SUMMER_CLAUDE_CONFIG_FILE",
110
110
  format: "json-stdio",
111
+ remoteMcp: { format: "json-http", signIn: (name) => `In Claude Code, run /mcp, choose ${name} and sign in.` },
111
112
  mcp: { user: home(".claude.json"), project: project(".mcp.json") },
112
113
  restart: "Restart Claude Code or run /mcp in a new session.",
113
114
  skills: skillDirs(home(".claude", "skills"), project(".claude", "skills")),
@@ -134,6 +135,10 @@ const SPECS = [
134
135
  status: "active",
135
136
  envOverride: "SUMMER_CODEX_CONFIG_FILE",
136
137
  format: "toml",
138
+ remoteMcp: {
139
+ format: "toml-url",
140
+ signIn: (name) => `In Codex, run /mcp login ${name} (or \`codex mcp login ${name}\` in a terminal) and sign in.`,
141
+ },
137
142
  mcp: {
138
143
  user: home(".codex", "config.toml"),
139
144
  project: project(".codex", "config.toml"),
@@ -149,6 +154,10 @@ const SPECS = [
149
154
  status: "active",
150
155
  envOverride: "SUMMER_CURSOR_MCP_CONFIG_FILE",
151
156
  format: "json-stdio",
157
+ // No remoteMcp (Summer Games) yet: Cursor's fallback OAuth flow registers
158
+ // cursor://anysphere.cursor-mcp/oauth/callback, which api.summer.games
159
+ // refuses (summer-platform#1681 allows it). Add { format: "json-url" }
160
+ // ({ mcpServers: { <name>: { url } } }) once that is live.
152
161
  mcp: { user: home(".cursor", "mcp.json"), project: project(".cursor", "mcp.json") },
153
162
  restart: "Restart Cursor and enable the summer-engine MCP server if prompted.",
154
163
  skills: sharedSkills({