summer-engine 4.3.0 → 4.6.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 (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/AGENTS.md +2 -2
  7. package/CHANGELOG.md +100 -0
  8. package/README.md +67 -27
  9. package/commands/start.md +19 -15
  10. package/commands/summer.md +1 -0
  11. package/dist/cli/command-usage.d.ts +3 -1
  12. package/dist/cli/command-usage.js +10 -4
  13. package/dist/cli/commands/create.d.ts +1 -25
  14. package/dist/cli/commands/create.js +10 -138
  15. package/dist/cli/commands/dev.d.ts +66 -0
  16. package/dist/cli/commands/dev.js +234 -0
  17. package/dist/cli/commands/install.d.ts +15 -58
  18. package/dist/cli/commands/install.js +31 -481
  19. package/dist/cli/commands/login.js +1 -1
  20. package/dist/cli/commands/ops.d.ts +17 -1
  21. package/dist/cli/commands/ops.js +97 -9
  22. package/dist/cli/commands/preview.d.ts +58 -0
  23. package/dist/cli/commands/preview.js +181 -0
  24. package/dist/cli/commands/run.d.ts +1 -17
  25. package/dist/cli/commands/run.js +17 -206
  26. package/dist/cli/commands/setup.js +175 -32
  27. package/dist/cli/index.js +5 -0
  28. package/dist/core/agent-host.d.ts +3 -0
  29. package/dist/core/agent-host.js +1 -1
  30. package/dist/core/api-client.d.ts +10 -2
  31. package/dist/core/api-client.js +13 -8
  32. package/dist/core/capabilities/agent-playbook.js +1 -0
  33. package/dist/core/capabilities/apps.d.ts +9 -9
  34. package/dist/core/capabilities/asset-import.d.ts +4 -1
  35. package/dist/core/capabilities/asset-import.js +15 -6
  36. package/dist/core/capabilities/capture-gameplay.js +1 -1
  37. package/dist/core/capabilities/connect-signal.d.ts +2 -2
  38. package/dist/core/capabilities/content-make/index.d.ts +81 -0
  39. package/dist/core/capabilities/content-make/index.js +706 -0
  40. package/dist/core/capabilities/content-make/media.d.ts +57 -0
  41. package/dist/core/capabilities/content-make/media.js +172 -0
  42. package/dist/core/capabilities/content-make/plan.d.ts +130 -0
  43. package/dist/core/capabilities/content-make/plan.js +433 -0
  44. package/dist/core/capabilities/content-make/render.d.ts +64 -0
  45. package/dist/core/capabilities/content-make/render.js +370 -0
  46. package/dist/core/capabilities/content-make/study.d.ts +13 -0
  47. package/dist/core/capabilities/content-make/study.js +165 -0
  48. package/dist/core/capabilities/content-make/types.d.ts +173 -0
  49. package/dist/core/capabilities/content-make/types.js +6 -0
  50. package/dist/core/capabilities/content-make/voice.d.ts +7 -0
  51. package/dist/core/capabilities/content-make/voice.js +27 -0
  52. package/dist/core/capabilities/events.d.ts +2 -2
  53. package/dist/core/capabilities/export-game.d.ts +4 -1
  54. package/dist/core/capabilities/export-game.js +4 -3
  55. package/dist/core/capabilities/export-presets.d.ts +2 -1
  56. package/dist/core/capabilities/export-presets.js +2 -1
  57. package/dist/core/capabilities/go-live.d.ts +14 -0
  58. package/dist/core/capabilities/go-live.js +408 -0
  59. package/dist/core/capabilities/image-generation.d.ts +6 -6
  60. package/dist/core/capabilities/item-manifest.d.ts +92 -0
  61. package/dist/core/capabilities/item-manifest.js +247 -0
  62. package/dist/core/capabilities/placement.d.ts +7 -7
  63. package/dist/core/capabilities/projects-local.d.ts +68 -0
  64. package/dist/core/capabilities/projects-local.js +172 -0
  65. package/dist/core/capabilities/publish-build.d.ts +13 -2
  66. package/dist/core/capabilities/publish-build.js +87 -21
  67. package/dist/core/capabilities/publish-download.js +1 -1
  68. package/dist/core/capabilities/runtime-control.d.ts +7 -2
  69. package/dist/core/capabilities/runtime-control.js +27 -1
  70. package/dist/core/capabilities/seeing/args.d.ts +2 -2
  71. package/dist/core/capabilities/store-checks.d.ts +45 -0
  72. package/dist/core/capabilities/store-checks.js +85 -0
  73. package/dist/core/capabilities/store-details.d.ts +37 -0
  74. package/dist/core/capabilities/store-details.js +94 -0
  75. package/dist/core/capabilities/summer-bundle.d.ts +21 -2
  76. package/dist/core/capabilities/summer-bundle.js +54 -7
  77. package/dist/core/capabilities/sync-items.d.ts +89 -0
  78. package/dist/core/capabilities/sync-items.js +352 -0
  79. package/dist/core/capabilities/tool-dispatch.js +66 -6
  80. package/dist/core/capabilities/video-ogv.d.ts +49 -0
  81. package/dist/core/capabilities/video-ogv.js +260 -0
  82. package/dist/core/capability-skew.js +2 -2
  83. package/dist/core/cli-usage-beacon.js +26 -2
  84. package/dist/core/cli-usage.d.ts +5 -2
  85. package/dist/core/cli-usage.js +8 -2
  86. package/dist/core/engine-install.d.ts +53 -0
  87. package/dist/core/engine-install.js +93 -0
  88. package/dist/core/engine-installer.d.ts +134 -0
  89. package/dist/core/engine-installer.js +760 -0
  90. package/dist/core/engine-launch.d.ts +126 -0
  91. package/dist/core/engine-launch.js +398 -0
  92. package/dist/core/engine-self-start.d.ts +63 -0
  93. package/dist/core/engine-self-start.js +282 -0
  94. package/dist/core/engine.d.ts +7 -1
  95. package/dist/core/engine.js +9 -2
  96. package/dist/core/error-codes.d.ts +4 -1
  97. package/dist/core/error-codes.js +7 -3
  98. package/dist/core/library-read.d.ts +2 -2
  99. package/dist/core/library-search.d.ts +2 -2
  100. package/dist/core/local-project.d.ts +50 -0
  101. package/dist/core/local-project.js +234 -0
  102. package/dist/core/ops/areas.js +9 -3
  103. package/dist/core/ops/catalog.d.ts +9 -0
  104. package/dist/core/ops/catalog.js +56 -4
  105. package/dist/core/ops/engine-answer.d.ts +4 -1
  106. package/dist/core/ops/engine-answer.js +16 -3
  107. package/dist/core/ops/engine.js +4 -4
  108. package/dist/core/ops/save-files.d.ts +6 -1
  109. package/dist/core/ops/save-files.js +67 -50
  110. package/dist/core/ops/search.js +9 -2
  111. package/dist/core/preview/input.d.ts +35 -0
  112. package/dist/core/preview/input.js +105 -0
  113. package/dist/core/preview/launch-config.d.ts +52 -0
  114. package/dist/core/preview/launch-config.js +137 -0
  115. package/dist/core/preview/page.d.ts +22 -0
  116. package/dist/core/preview/page.js +232 -0
  117. package/dist/core/preview/preview-source.d.ts +179 -0
  118. package/dist/core/preview/preview-source.js +350 -0
  119. package/dist/core/preview/server.d.ts +26 -0
  120. package/dist/core/preview/server.js +218 -0
  121. package/dist/core/telemetry.d.ts +20 -16
  122. package/dist/core/telemetry.js +92 -16
  123. package/dist/core/templates.d.ts +20 -0
  124. package/dist/core/templates.js +300 -3
  125. package/dist/core/tool-profiles.d.ts +2 -2
  126. package/dist/core/tool-profiles.js +1 -0
  127. package/dist/core/usage-fields.d.ts +41 -0
  128. package/dist/core/usage-fields.js +150 -0
  129. package/dist/core/util/sleep.d.ts +3 -0
  130. package/dist/core/util/sleep.js +12 -0
  131. package/dist/installer/agent-config.d.ts +29 -0
  132. package/dist/installer/agent-config.js +75 -11
  133. package/dist/installer/agent-detect.d.ts +44 -0
  134. package/dist/installer/agent-detect.js +166 -0
  135. package/dist/installer/agent-table.d.ts +19 -0
  136. package/dist/installer/agent-table.js +130 -0
  137. package/dist/installer/setup-error.d.ts +19 -0
  138. package/dist/installer/setup-error.js +34 -0
  139. package/dist/installer/version-check.d.ts +5 -0
  140. package/dist/installer/version-check.js +21 -0
  141. package/dist/mcp/ops-surface.d.ts +7 -0
  142. package/dist/mcp/ops-surface.js +40 -3
  143. package/dist/mcp/server.d.ts +14 -4
  144. package/dist/mcp/server.js +42 -8
  145. package/dist/mcp/tool-surface.d.ts +8 -0
  146. package/dist/mcp/tool-surface.js +19 -2
  147. package/dist/mcp/tools/creator-tools.js +92 -14
  148. package/dist/mcp/tools/debug-tools.js +2 -0
  149. package/dist/mcp/tools/local-project-tools.d.ts +6 -0
  150. package/dist/mcp/tools/local-project-tools.js +33 -0
  151. package/dist/mcp/tools/navigation-tools.js +2 -1
  152. package/dist/mcp/tools/project-tools.js +5 -8
  153. package/dist/mcp/tools/seeing-tools.js +1 -1
  154. package/dist/mcp/tools/visual-tools.js +1 -1
  155. package/dist/mcp/tools/with-engine.js +1 -1
  156. package/dist/project-memory/agent-instructions.js +11 -3
  157. package/gemini-extension.json +3 -1
  158. package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
  159. package/library/skills/README.md +12 -8
  160. package/library/skills/animated-loop/SKILL.md +10 -16
  161. package/library/skills/cinematic-cutscene/SKILL.md +7 -18
  162. package/library/skills/headless-scripting/SKILL.md +7 -4
  163. package/library/skills/in-game-purchases/SKILL.md +25 -8
  164. package/library/skills/in-game-purchases/resource.yaml +2 -1
  165. package/library/skills/launch-ready/SKILL.md +351 -0
  166. package/library/skills/launch-ready/resource.yaml +45 -0
  167. package/library/skills/make-content/SKILL.md +202 -0
  168. package/library/skills/make-content/resource.yaml +32 -0
  169. package/library/skills/make-game/SKILL.md +6 -4
  170. package/library/skills/make-game/resource.yaml +2 -1
  171. package/library/skills/navigate-summer/SKILL.md +4 -2
  172. package/library/skills/new-project/SKILL.md +15 -22
  173. package/library/skills/new-project/resource.yaml +2 -1
  174. package/library/skills/painted-25d-world/SKILL.md +366 -0
  175. package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
  176. package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
  177. package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
  178. package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
  179. package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
  180. package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
  181. package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
  182. package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
  183. package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
  184. package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
  185. package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
  186. package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
  187. package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
  188. package/library/skills/painted-25d-world/resource.yaml +39 -0
  189. package/library/skills/publish-your-game/SKILL.md +13 -14
  190. package/library/skills/publish-your-game/resource.yaml +4 -3
  191. package/library/skills/release-checklist/SKILL.md +188 -0
  192. package/library/skills/release-checklist/resource.yaml +40 -0
  193. package/library/skills/release-readiness/SKILL.md +1 -1
  194. package/library/skills/release-readiness/resource.yaml +1 -1
  195. package/library/skills/store-art/SKILL.md +1 -1
  196. package/library/skills/store-art/resource.yaml +1 -1
  197. package/library/skills/store-listing/SKILL.md +1 -1
  198. package/library/skills/store-listing/resource.yaml +1 -1
  199. package/library/skills/summer-cli/SKILL.md +2 -0
  200. package/library/skills/summer-store/SKILL.md +295 -39
  201. package/library/skills/summer-store/resource.yaml +9 -3
  202. package/library/skills/trailer-shot/SKILL.md +3 -3
  203. package/library/templates/README.md +5 -0
  204. package/library/tools/content-make/resource.yaml +132 -0
  205. package/library/tools/frame-shot/GUIDE.md +1 -1
  206. package/library/tools/go-live/resource.yaml +67 -0
  207. package/library/tools/import-from-url/GUIDE.md +1 -0
  208. package/library/tools/import-from-url-batch/GUIDE.md +1 -1
  209. package/library/tools/play/GUIDE.md +2 -0
  210. package/library/tools/projects-create-local/resource.yaml +55 -0
  211. package/library/tools/projects-open-local/resource.yaml +51 -0
  212. package/library/tools/publish-build/resource.yaml +3 -3
  213. package/library/tools/screenshot/GUIDE.md +1 -1
  214. package/library/tools/store-set-details/resource.yaml +114 -0
  215. package/library/tools/sync-items/resource.yaml +72 -0
  216. package/package.json +1 -1
  217. package/registry/generated/aliases.json +6 -0
  218. package/registry/generated/counts.json +3 -3
  219. package/registry/generated/gemini-extension.json +3 -1
  220. package/registry/generated/index.json +463 -37
  221. package/registry/generated/marketplace.claude.json +1 -1
  222. package/registry/generated/plugin.claude-directory.json +4 -2
  223. package/registry/generated/plugin.claude.json +3 -1
  224. package/registry/generated/plugin.codex.json +5 -3
  225. package/registry/generated/plugin.cursor.json +4 -2
  226. package/registry/generated/plugin.factory.json +4 -2
  227. package/registry/generated/server.json +2 -2
  228. package/registry/generated/skills-index.md +12 -8
  229. package/registry/generated/skills-registry.json +62 -5
  230. package/registry/operations/legacy-names.json +40 -0
  231. package/registry/operations/public-catalog.json +10144 -387
@@ -0,0 +1,247 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
3
+ import { BuildToolError } from "./summer-bundle.js";
4
+ /**
5
+ * summer.items.json: the Game's items, keyed by creator slug, next to
6
+ * summer.build.json in the project root and committed with the game.
7
+ *
8
+ * It names no Game, environment or platform id: the same file syncs to the
9
+ * staging Game and to the production Game, and the slug (the creator key the
10
+ * game's code uses, e.g. "emote_ponder") is what identifies an item in both.
11
+ * The gameId comes with each sync call, as with summer_publish_build.
12
+ *
13
+ * {
14
+ * "schema": "summer.items/v1",
15
+ * "items": [
16
+ * {
17
+ * "slug": "emote_ponder",
18
+ * "name": "Ponder",
19
+ * "description": "Strike a thoughtful pose.",
20
+ * "class": "COSMETIC",
21
+ * "cosmetic": { "familyId": "emote", "variantKey": "ponder" },
22
+ * "tags": ["emote"],
23
+ * "ownership": "OWNED",
24
+ * "priceSparks": 150,
25
+ * "media": ["art/items/emote_ponder.png"]
26
+ * }
27
+ * ]
28
+ * }
29
+ *
30
+ * The checks here are the platform's own create rules (management API
31
+ * item-definitions), so a file that passes is not refused for its shape.
32
+ */
33
+ export const ITEMS_MANIFEST_FILE = "summer.items.json";
34
+ export const ITEMS_MANIFEST_SCHEMA = "summer.items/v1";
35
+ /** The platform's slug rule: 1-64 characters, lowercase, words joined by one ".", "_" or "-". */
36
+ export const ITEM_SLUG = /^[a-z][a-z0-9]*([._-][a-z0-9]+)*$/;
37
+ const TAG = /^[a-z0-9][a-z0-9._-]{0,47}$/;
38
+ /** Cosmetic familyId and variantKey: the platform's compatibility value rule, at most 128 characters. */
39
+ const COSMETIC_KEY = /^[a-z][a-z0-9]*([._-][a-z0-9]+)*$/;
40
+ const BACKGROUND = /^#[0-9A-Fa-f]{6}$/;
41
+ const MAX_ITEMS = 500;
42
+ const MAX_MEDIA = 20;
43
+ const MAX_NAME = 120;
44
+ const MAX_DESCRIPTION = 5000;
45
+ const MAX_TAGS = 20;
46
+ export const ITEM_CLASSES = ["STANDARD", "COSMETIC"];
47
+ export const OWNERSHIP_MODES = ["INSTANCE", "QUANTITY"];
48
+ export const OWNERSHIP_POLICIES = ["SOULBOUND", "OWNED", "MARKETABLE"];
49
+ export const MEDIA_TYPES = {
50
+ ".png": { kind: "IMAGE", contentType: "image/png", maxBytes: 32 << 20 },
51
+ ".jpg": { kind: "IMAGE", contentType: "image/jpeg", maxBytes: 32 << 20 },
52
+ ".jpeg": { kind: "IMAGE", contentType: "image/jpeg", maxBytes: 32 << 20 },
53
+ ".webp": { kind: "IMAGE", contentType: "image/webp", maxBytes: 32 << 20 },
54
+ ".glb": { kind: "MODEL_3D", contentType: "model/gltf-binary", maxBytes: 64 << 20 },
55
+ };
56
+ /** The platform behavior a manifest entry declares (item-definitions create body). */
57
+ export function behaviorOf(item) {
58
+ const marketable = item.ownership === "MARKETABLE";
59
+ return {
60
+ class: item.class,
61
+ ownershipMode: item.ownershipMode,
62
+ ...(item.maxOwned !== undefined ? { maxOwned: item.maxOwned } : {}),
63
+ rights: {
64
+ policy: item.ownership,
65
+ consumable: item.consumable,
66
+ directTransferAllowed: marketable,
67
+ giftingAfterOwnershipAllowed: marketable,
68
+ publicResalePolicyEligible: marketable,
69
+ },
70
+ // Studio's defaults: the game's server may grant it, and Summer support may restore it.
71
+ issuance: { serverMintAllowed: true, supportGrantAllowed: true },
72
+ ...(item.cosmetic ? { cosmetic: item.cosmetic } : {}),
73
+ };
74
+ }
75
+ class ManifestProblems {
76
+ list = [];
77
+ add(where, problem) {
78
+ if (this.list.length < 50)
79
+ this.list.push(`${where}: ${problem}`);
80
+ }
81
+ }
82
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
83
+ const positiveInt = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
84
+ const ITEM_FIELDS = new Set([
85
+ "slug", "name", "description", "class", "cosmetic", "tags", "ownershipMode", "maxOwned",
86
+ "ownership", "consumable", "supply", "priceSparks", "backgroundColor", "media",
87
+ ]);
88
+ function mediaPath(project, value, where, problems) {
89
+ if (typeof value !== "string" || !value.trim()) {
90
+ problems.add(where, "a project-relative file path");
91
+ return null;
92
+ }
93
+ const clean = normalize(value.trim());
94
+ const full = resolve(project, clean);
95
+ const inside = relative(project, full);
96
+ if (isAbsolute(value) || inside.startsWith("..") || isAbsolute(inside)) {
97
+ problems.add(where, `"${value}" is outside the project; use a path inside it`);
98
+ return null;
99
+ }
100
+ if (!MEDIA_TYPES[extname(clean).toLowerCase()]) {
101
+ problems.add(where, `"${value}" is not a store image (.png, .jpg, .webp) or model (.glb)`);
102
+ return null;
103
+ }
104
+ return inside.split(sep).join("/");
105
+ }
106
+ function readItem(project, raw, index, problems) {
107
+ const at = `items[${index}]`;
108
+ if (!isObject(raw)) {
109
+ problems.add(at, "an object");
110
+ return null;
111
+ }
112
+ const where = typeof raw.slug === "string" ? `${at} (${raw.slug})` : at;
113
+ for (const key of Object.keys(raw))
114
+ if (!ITEM_FIELDS.has(key))
115
+ problems.add(where, `unknown field "${key}"`);
116
+ const before = problems.list.length;
117
+ const slug = raw.slug;
118
+ if (typeof slug !== "string" || slug.length > 64 || !ITEM_SLUG.test(slug)) {
119
+ problems.add(where, 'slug: 1-64 lowercase letters and digits, words joined by ".", "_" or "-", starting with a letter (e.g. "emote_ponder")');
120
+ }
121
+ const name = raw.name;
122
+ if (typeof name !== "string" || name !== name.trim() || name.length < 1 || [...name].length > MAX_NAME) {
123
+ problems.add(where, `name: 1-${MAX_NAME} characters, no surrounding spaces`);
124
+ }
125
+ const description = raw.description;
126
+ if (typeof description !== "string" || description !== description.trim() || description.length < 1 || [...description].length > MAX_DESCRIPTION) {
127
+ problems.add(where, `description: 1-${MAX_DESCRIPTION} characters, no surrounding spaces`);
128
+ }
129
+ const itemClass = raw.class ?? "STANDARD";
130
+ if (!ITEM_CLASSES.includes(itemClass))
131
+ problems.add(where, `class: one of ${ITEM_CLASSES.join(", ")}`);
132
+ const ownershipMode = raw.ownershipMode ?? "INSTANCE";
133
+ if (!OWNERSHIP_MODES.includes(ownershipMode))
134
+ problems.add(where, `ownershipMode: one of ${OWNERSHIP_MODES.join(", ")}`);
135
+ const ownership = raw.ownership ?? "OWNED";
136
+ if (!OWNERSHIP_POLICIES.includes(ownership))
137
+ problems.add(where, `ownership: one of ${OWNERSHIP_POLICIES.join(", ")}`);
138
+ if (raw.consumable !== undefined && typeof raw.consumable !== "boolean")
139
+ problems.add(where, "consumable: true or false");
140
+ if (raw.maxOwned !== undefined && !positiveInt(raw.maxOwned))
141
+ problems.add(where, "maxOwned: a whole number, 1 or more");
142
+ if (raw.supply !== undefined && !positiveInt(raw.supply))
143
+ problems.add(where, "supply: a whole number, 1 or more (omit it for unlimited)");
144
+ if (raw.priceSparks !== undefined && !positiveInt(raw.priceSparks))
145
+ problems.add(where, "priceSparks: a whole number of Sparks, 1 or more (omit it to set no price)");
146
+ if (raw.backgroundColor !== undefined && (typeof raw.backgroundColor !== "string" || !BACKGROUND.test(raw.backgroundColor))) {
147
+ problems.add(where, 'backgroundColor: "#RRGGBB"');
148
+ }
149
+ const tags = raw.tags ?? [];
150
+ if (!Array.isArray(tags) || tags.length > MAX_TAGS || tags.some((tag) => typeof tag !== "string" || !TAG.test(tag)) || new Set(tags).size !== tags.length) {
151
+ problems.add(where, `tags: up to ${MAX_TAGS} different lowercase tags (letters, digits, ".", "_", "-")`);
152
+ }
153
+ let cosmetic;
154
+ if (itemClass === "COSMETIC") {
155
+ const facet = raw.cosmetic;
156
+ if (!isObject(facet) || typeof facet.familyId !== "string" || typeof facet.variantKey !== "string" || facet.familyId.length > 128 || facet.variantKey.length > 128 || !COSMETIC_KEY.test(facet.familyId) || !COSMETIC_KEY.test(facet.variantKey)) {
157
+ problems.add(where, 'cosmetic: { "familyId": ..., "variantKey": ... }, each lowercase words joined by ".", "_" or "-", at most 128 characters');
158
+ }
159
+ else {
160
+ cosmetic = { familyId: facet.familyId, variantKey: facet.variantKey };
161
+ }
162
+ if (ownershipMode === "QUANTITY" && raw.maxOwned !== 1)
163
+ problems.add(where, "a COSMETIC with ownershipMode QUANTITY needs maxOwned 1");
164
+ }
165
+ else if (raw.cosmetic !== undefined) {
166
+ problems.add(where, "cosmetic: only for class COSMETIC");
167
+ }
168
+ const mediaRaw = raw.media ?? [];
169
+ const media = [];
170
+ if (!Array.isArray(mediaRaw) || mediaRaw.length > MAX_MEDIA) {
171
+ problems.add(where, `media: up to ${MAX_MEDIA} project-relative image or model paths`);
172
+ }
173
+ else {
174
+ mediaRaw.forEach((value, mediaIndex) => {
175
+ const path = mediaPath(project, value, `${where} media[${mediaIndex}]`, problems);
176
+ if (path && media.includes(path))
177
+ problems.add(`${where} media[${mediaIndex}]`, `"${value}" is listed twice`);
178
+ else if (path)
179
+ media.push(path);
180
+ });
181
+ }
182
+ if (problems.list.length !== before)
183
+ return null;
184
+ return {
185
+ slug: slug,
186
+ name: name,
187
+ description: description,
188
+ class: itemClass,
189
+ ...(cosmetic ? { cosmetic } : {}),
190
+ tags: tags,
191
+ ownershipMode: ownershipMode,
192
+ ...(raw.maxOwned !== undefined ? { maxOwned: raw.maxOwned } : {}),
193
+ ownership: ownership,
194
+ consumable: raw.consumable === true,
195
+ ...(raw.supply !== undefined ? { supply: raw.supply } : {}),
196
+ ...(raw.priceSparks !== undefined ? { priceSparks: raw.priceSparks } : {}),
197
+ ...(raw.backgroundColor !== undefined ? { backgroundColor: raw.backgroundColor } : {}),
198
+ media,
199
+ };
200
+ }
201
+ /** Parse and check a manifest document; every problem is reported at once. */
202
+ export function parseItemsManifest(project, document, path = join(project, ITEMS_MANIFEST_FILE)) {
203
+ const problems = new ManifestProblems();
204
+ if (!isObject(document))
205
+ problems.add(ITEMS_MANIFEST_FILE, "a JSON object");
206
+ else {
207
+ if (document.schema !== ITEMS_MANIFEST_SCHEMA)
208
+ problems.add("schema", `"${ITEMS_MANIFEST_SCHEMA}"`);
209
+ for (const key of Object.keys(document))
210
+ if (key !== "schema" && key !== "items")
211
+ problems.add(ITEMS_MANIFEST_FILE, `unknown field "${key}"`);
212
+ if (!Array.isArray(document.items) || document.items.length > MAX_ITEMS)
213
+ problems.add("items", `a list of up to ${MAX_ITEMS} items`);
214
+ }
215
+ const rawItems = isObject(document) && Array.isArray(document.items) ? document.items : [];
216
+ const items = rawItems.map((raw, index) => readItem(project, raw, index, problems));
217
+ const seen = new Set();
218
+ for (const item of items) {
219
+ if (!item)
220
+ continue;
221
+ if (seen.has(item.slug))
222
+ problems.add(`items (${item.slug})`, "the same slug is used twice; each item needs its own");
223
+ seen.add(item.slug);
224
+ }
225
+ if (problems.list.length) {
226
+ throw new BuildToolError("items_manifest_invalid", `${ITEMS_MANIFEST_FILE} has ${problems.list.length === 50 ? "50 or more" : problems.list.length} problem${problems.list.length === 1 ? "" : "s"}. Nothing was sent.`, "Recovery: fix each listed field and call again.", undefined, { problems: problems.list, path });
227
+ }
228
+ return { path, project, items: items };
229
+ }
230
+ export async function readItemsManifest(project, file) {
231
+ const path = file ? resolve(project, file) : join(project, ITEMS_MANIFEST_FILE);
232
+ let text;
233
+ try {
234
+ text = await readFile(path, "utf8");
235
+ }
236
+ catch {
237
+ throw new BuildToolError("items_manifest_missing", `There is no ${ITEMS_MANIFEST_FILE} at ${path}.`, `Recovery: write ${ITEMS_MANIFEST_FILE} in the project root ({"schema": "${ITEMS_MANIFEST_SCHEMA}", "items": [...]}), then call again.`);
238
+ }
239
+ let document;
240
+ try {
241
+ document = JSON.parse(text);
242
+ }
243
+ catch (error) {
244
+ throw new BuildToolError("items_manifest_invalid", `${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}.`, "Recovery: fix the JSON and call again.");
245
+ }
246
+ return parseItemsManifest(project, document, path);
247
+ }
@@ -159,8 +159,8 @@ export declare const measureArgsSchema: z.ZodObject<{
159
159
  scenePath: string;
160
160
  mode?: "pair" | "plane" | undefined;
161
161
  a?: string | undefined;
162
- space?: "local" | "world" | undefined;
163
162
  b?: string | undefined;
163
+ space?: "local" | "world" | undefined;
164
164
  nodes?: string[] | undefined;
165
165
  axis?: "y" | "z" | "x" | undefined;
166
166
  tolerance?: number | undefined;
@@ -178,8 +178,8 @@ export declare const raycastArgsSchema: z.ZodObject<{
178
178
  evidence: z.ZodDefault<z.ZodOptional<z.ZodEnum<["auto", "physics", "visual_aabb"]>>>;
179
179
  exclude: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, "many">>;
180
180
  }, "strip", z.ZodTypeAny, {
181
- scenePath: string;
182
181
  origin: [number, number, number];
182
+ scenePath: string;
183
183
  direction: [number, number, number];
184
184
  evidence: "auto" | "physics" | "visual_aabb";
185
185
  maxDistance: number;
@@ -187,8 +187,8 @@ export declare const raycastArgsSchema: z.ZodObject<{
187
187
  collideWithAreas: boolean;
188
188
  exclude?: string[] | undefined;
189
189
  }, {
190
- scenePath: string;
191
190
  origin: [number, number, number];
191
+ scenePath: string;
192
192
  direction: [number, number, number];
193
193
  evidence?: "auto" | "physics" | "visual_aabb" | undefined;
194
194
  maxDistance?: number | undefined;
@@ -222,9 +222,9 @@ export declare const placeAdjacentArgsSchema: z.ZodObject<{
222
222
  }>]>>>;
223
223
  space: z.ZodDefault<z.ZodOptional<z.ZodEnum<["world", "local"]>>>;
224
224
  }, "strip", z.ZodTypeAny, {
225
+ reference: string;
225
226
  scenePath: string;
226
227
  space: "local" | "world";
227
- reference: string;
228
228
  subject: string;
229
229
  axis: "y" | "z" | "x";
230
230
  side: "min" | "max";
@@ -235,8 +235,8 @@ export declare const placeAdjacentArgsSchema: z.ZodObject<{
235
235
  x?: "none" | "min" | "max" | "center" | undefined;
236
236
  };
237
237
  }, {
238
- scenePath: string;
239
238
  reference: string;
239
+ scenePath: string;
240
240
  subject: string;
241
241
  axis: "y" | "z" | "x";
242
242
  side: "min" | "max";
@@ -371,8 +371,8 @@ export declare const repeatAlongArgsSchema: z.ZodObject<{
371
371
  scale: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
372
372
  namePrefix: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
373
373
  }, "strip", z.ZodTypeAny, {
374
- scenePath: string;
375
374
  template: string;
375
+ scenePath: string;
376
376
  parent: string;
377
377
  start: [number, number, number];
378
378
  align: "end" | "start" | "center";
@@ -384,8 +384,8 @@ export declare const repeatAlongArgsSchema: z.ZodObject<{
384
384
  rotationDegrees?: [number, number, number] | undefined;
385
385
  namePrefix?: string | undefined;
386
386
  }, {
387
- scenePath: string;
388
387
  template: string;
388
+ scenePath: string;
389
389
  parent: string;
390
390
  start: [number, number, number];
391
391
  end?: [number, number, number] | undefined;
@@ -0,0 +1,68 @@
1
+ import type { EngineApiClient } from "../api-client.js";
2
+ import { type EngineSelection } from "../engine.js";
3
+ import { type SelfStartDeps } from "../engine-self-start.js";
4
+ /** The starter project when no template is named (the same as `summer dev`). */
5
+ export declare const LOCAL_DEFAULT_TEMPLATE = "3d-basic";
6
+ /** A failure the caller can fix: `code` is the answer's error code,
7
+ * `recovery` its one next step, `reason` the install's own code when an install failed. */
8
+ export declare class LocalProjectError extends Error {
9
+ readonly code: "invalid_input" | "not_found" | "unavailable" | "conflict" | "unsupported";
10
+ readonly recovery: string;
11
+ readonly reason?: string | undefined;
12
+ constructor(code: "invalid_input" | "not_found" | "unavailable" | "conflict" | "unsupported", message: string, recovery: string, reason?: string | undefined);
13
+ }
14
+ /** `{ok:false, code, error, recovery}`, the shape every face prints. */
15
+ export declare function localProjectFailure(error: LocalProjectError): Record<string, unknown>;
16
+ /**
17
+ * Where a new project goes when no folder is named. Next to the project the
18
+ * session is in (never inside another project); else the session's folder
19
+ * when it is a folder of the person's own inside home (not home itself, not
20
+ * an app's folder: chat apps start their server in an install or app-data
21
+ * folder, or in / or home); else ~/Documents/Summer.
22
+ */
23
+ export declare function defaultProjectParent(cwd: string, home?: string): string;
24
+ export interface CreateLocalInput {
25
+ template?: string;
26
+ name?: string;
27
+ dir?: string;
28
+ }
29
+ export interface CreatedLocalProject {
30
+ ok: true;
31
+ projectPath: string;
32
+ /** The template it was made from; null when a project was already there. */
33
+ template: string | null;
34
+ created: boolean;
35
+ next: {
36
+ op: "projects.open_local";
37
+ input: {
38
+ path: string;
39
+ };
40
+ };
41
+ }
42
+ /** Write a new project from a template; a folder that already holds one is left as it is. */
43
+ export declare function createLocalProject(input: CreateLocalInput, cwd?: string, waitMs?: number): Promise<CreatedLocalProject>;
44
+ export interface OpenLocalInput {
45
+ path?: string;
46
+ install?: boolean;
47
+ }
48
+ export interface OpenedLocalProject {
49
+ ok: true;
50
+ projectPath: string;
51
+ engineVersion: string | null;
52
+ /** `summer preview`'s address, and the .claude/launch.json that registers it for Claude Code Desktop (null: not written). */
53
+ preview: {
54
+ url: string;
55
+ launchJson: string | null;
56
+ };
57
+ next: string;
58
+ }
59
+ /**
60
+ * Open a project in Summer Engine and wait until it connects (core/engine-self-start.ts):
61
+ * an editor that already has it is used as it is. Throws LocalProjectError
62
+ * when there is no project, EngineNotRunningError with the next step otherwise.
63
+ */
64
+ export declare function openLocalProject(input: OpenLocalInput, selection: EngineSelection | undefined, deps?: SelfStartDeps,
65
+ /** The MCP binds its session to the opened project with the connected client. */
66
+ onOpen?: (projectRoot: string, client: EngineApiClient) => void,
67
+ /** The folder the agent session runs in: its .claude/launch.json gets summer-preview. */
68
+ sessionFolder?: string): Promise<OpenedLocalProject>;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * projects.create_local and projects.open_local: `summer dev` for agents
3
+ * with no shell (Claude Desktop, chat apps). create_local writes a project
4
+ * from a template to this computer; open_local opens a project in Summer
5
+ * Engine and waits until it connects, installing the engine first only when
6
+ * the person said yes (install: true). Both run in this package; the cloud
7
+ * projects.create makes a plan on Summer's server instead.
8
+ */
9
+ import { existsSync, statSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
12
+ import { findProjectRoot } from "../engine.js";
13
+ import { EngineInstallError } from "../engine-install.js";
14
+ import { openProjectInEditor, sessionProject } from "../engine-self-start.js";
15
+ import { createProject, isEmptyProjectFolder, projectRootAbove } from "../local-project.js";
16
+ import { PREVIEW_URL, registerPreview } from "../preview/launch-config.js";
17
+ import { getTemplateRegistry, resolveTemplate } from "../templates.js";
18
+ import { settlesWithin } from "../util/sleep.js";
19
+ /** The starter project when no template is named (the same as `summer dev`). */
20
+ export const LOCAL_DEFAULT_TEMPLATE = "3d-basic";
21
+ const DEFAULT_NAME = "my-game";
22
+ /** A failure the caller can fix: `code` is the answer's error code,
23
+ * `recovery` its one next step, `reason` the install's own code when an install failed. */
24
+ export class LocalProjectError extends Error {
25
+ code;
26
+ recovery;
27
+ reason;
28
+ constructor(code, message, recovery, reason) {
29
+ super(message);
30
+ this.code = code;
31
+ this.recovery = recovery;
32
+ this.reason = reason;
33
+ this.name = "LocalProjectError";
34
+ }
35
+ }
36
+ /** `{ok:false, code, error, recovery}`, the shape every face prints. */
37
+ export function localProjectFailure(error) {
38
+ return { ok: false, code: error.code, error: error.message, recovery: error.recovery, ...(error.reason ? { reason: error.reason } : {}) };
39
+ }
40
+ /** An approved install that failed, with its own reason (disk_space, download, ...). */
41
+ function installFailure(error) {
42
+ const code = { unsupported_platform: "unsupported", app_running: "conflict" }[error.reason] ?? "unavailable";
43
+ return new LocalProjectError(code, "Summer Engine could not be installed.", error.message, error.reason);
44
+ }
45
+ /** App folders a chat app may start its server in: never a place for a game. */
46
+ const APP_FOLDERS = /(^|[\\/])(AppData|Library|\.config|\.local|Program Files( \(x86\))?|WindowsApps)([\\/]|$)/i;
47
+ /**
48
+ * Where a new project goes when no folder is named. Next to the project the
49
+ * session is in (never inside another project); else the session's folder
50
+ * when it is a folder of the person's own inside home (not home itself, not
51
+ * an app's folder: chat apps start their server in an install or app-data
52
+ * folder, or in / or home); else ~/Documents/Summer.
53
+ */
54
+ export function defaultProjectParent(cwd, home = homedir()) {
55
+ const dir = resolve(cwd);
56
+ const enclosing = projectRootAbove(dir, home);
57
+ if (enclosing)
58
+ return dirname(enclosing);
59
+ const inHome = relative(resolve(home), dir);
60
+ if (inHome && !inHome.startsWith("..") && !isAbsolute(inHome) && !APP_FOLDERS.test(inHome))
61
+ return dir;
62
+ return join(resolve(home), "Documents", "Summer");
63
+ }
64
+ /** With no name, a new game never reuses a folder: my-game, then my-game-2, my-game-3, ...
65
+ * (a download this process is still running keeps its folder). */
66
+ function freeDefaultFolder(parent, base) {
67
+ for (let n = 1;; n++) {
68
+ const dir = join(parent, n === 1 ? base : `${base}-${n}`);
69
+ if (creating.has(dir) || !existsSync(dir) || (statSync(dir).isDirectory() && isEmptyProjectFolder(dir) && !existsSync(join(dir, "project.godot"))))
70
+ return dir;
71
+ }
72
+ }
73
+ /** Projects being written by this process, by folder: a slow template download outlives one tool call. */
74
+ const creating = new Map();
75
+ /** One call waits this long for a template download, then answers "still creating". */
76
+ const CREATE_WAIT_MS = 40_000;
77
+ /** The error for a folder that cannot be written, a full disk, or the template fetch. */
78
+ function createFailure(error, dir) {
79
+ const code = error.code;
80
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") {
81
+ return new LocalProjectError("invalid_input", `Cannot write to ${dir} (${code}).`, "Pass dir: a folder you can write to, e.g. ~/Documents/Summer/my-game.");
82
+ }
83
+ if (code === "ENOSPC")
84
+ return new LocalProjectError("unavailable", `The disk is full (${dir}).`, "Free up space, then call again.");
85
+ return new LocalProjectError("unavailable", `Could not create the project: ${error.message}`, "Check the network, then call again.");
86
+ }
87
+ /** Write a new project from a template; a folder that already holds one is left as it is. */
88
+ export async function createLocalProject(input, cwd = process.cwd(), waitMs = CREATE_WAIT_MS) {
89
+ const entries = getTemplateRegistry();
90
+ const asked = input.template?.trim() || LOCAL_DEFAULT_TEMPLATE;
91
+ const resolution = resolveTemplate(asked, entries);
92
+ if (resolution.kind !== "match") {
93
+ const which = resolution.kind === "ambiguous" ? resolution.candidates : entries.filter((entry) => entry.status !== "deprecated");
94
+ throw new LocalProjectError("invalid_input", `${resolution.kind === "ambiguous" ? `'${asked}' matches several templates` : `No template matches '${asked}'`}.`, `Pass one of: ${which.map((entry) => entry.slug).join(", ")}.`);
95
+ }
96
+ const entry = resolution.entry;
97
+ const name = input.name?.trim();
98
+ if (name && (/[\\/]/.test(name) || name === "." || name === "..")) {
99
+ throw new LocalProjectError("invalid_input", `name is the project's name, not a path: '${name}'.`, "Put the folder in dir.");
100
+ }
101
+ const askedDir = input.dir?.trim().replace(/^~(?=$|[\\/])/, homedir());
102
+ const dir = askedDir ? resolve(cwd, askedDir) : name ? join(defaultProjectParent(cwd), name) : freeDefaultFolder(defaultProjectParent(cwd), input.template ? entry.slug : DEFAULT_NAME);
103
+ const next = { op: "projects.open_local", input: { path: dir } };
104
+ // A call that timed out left the download running: wait for it again, never start a second one.
105
+ let job = creating.get(dir);
106
+ if (!job) {
107
+ if (existsSync(join(dir, "project.godot")))
108
+ return { ok: true, projectPath: dir, template: null, created: false, next };
109
+ const around = projectRootAbove(dirname(dir));
110
+ if (around) {
111
+ throw new LocalProjectError("invalid_input", `${dir} is inside the project ${around}.`, `Pass dir: a folder outside it, e.g. ${join(dirname(around), basename(dir))}.`);
112
+ }
113
+ if (existsSync(dir) && (!statSync(dir).isDirectory() || !isEmptyProjectFolder(dir))) {
114
+ throw new LocalProjectError("invalid_input", `${dir} has files but no project.godot.`, "Pass dir: a new folder, or an empty one.");
115
+ }
116
+ job = createProject(entry, dir, { name: name || basename(dir), log: () => { } }).then(() => ({ ok: true, projectPath: dir, template: entry.slug, created: true, next }), (error) => {
117
+ throw createFailure(error, dir);
118
+ });
119
+ job.catch(() => undefined);
120
+ creating.set(dir, job);
121
+ }
122
+ const done = await settlesWithin(job, waitMs);
123
+ if (!done) {
124
+ throw new LocalProjectError("unavailable", `Still creating ${dir} (downloading the template).`, "Call projects.create_local again in 30 seconds with the same input.");
125
+ }
126
+ creating.delete(dir);
127
+ return job;
128
+ }
129
+ /**
130
+ * Open a project in Summer Engine and wait until it connects (core/engine-self-start.ts):
131
+ * an editor that already has it is used as it is. Throws LocalProjectError
132
+ * when there is no project, EngineNotRunningError with the next step otherwise.
133
+ */
134
+ export async function openLocalProject(input, selection, deps = {},
135
+ /** The MCP binds its session to the opened project with the connected client. */
136
+ onOpen,
137
+ /** The folder the agent session runs in: its .claude/launch.json gets summer-preview. */
138
+ sessionFolder = process.cwd()) {
139
+ const asked = input.path?.trim();
140
+ const root = asked ? await findProjectRoot(resolve(selection?.cwd ?? process.cwd(), asked)) : await sessionProject(selection);
141
+ if (!root) {
142
+ throw new LocalProjectError("not_found", asked ? `Project not found: no project.godot at or above ${asked}.` : "Project not found: this session has no project.", "Make one with projects.create_local, then pass its projectPath as path.");
143
+ }
144
+ let client;
145
+ try {
146
+ client = await openProjectInEditor(root, { cwd: root }, { install: input.install === true }, deps);
147
+ }
148
+ catch (error) {
149
+ if (error instanceof EngineInstallError)
150
+ throw installFailure(error);
151
+ throw error;
152
+ }
153
+ onOpen?.(root, client);
154
+ // Claude Code Desktop's Browser pane shows the game from the session's launch.json (only adds; never in home).
155
+ let launchJson = null;
156
+ try {
157
+ const written = registerPreview(sessionFolder, root);
158
+ if (written && written.status !== "skipped")
159
+ launchJson = written.path;
160
+ }
161
+ catch {
162
+ // the game still opened; the preview is optional
163
+ }
164
+ return {
165
+ ok: true,
166
+ projectPath: root,
167
+ engineVersion: client.getEngineVersion() ?? null,
168
+ preview: { url: PREVIEW_URL, launchJson },
169
+ next: `Call summer_get_project_context, then build the scene. See the game at ${PREVIEW_URL}: ` +
170
+ "Claude Code Desktop's Browser pane starts it; elsewhere run: npx -y summer-engine@latest preview --open",
171
+ };
172
+ }
@@ -6,7 +6,7 @@ export interface PublishBuildInput {
6
6
  /** Exported .zip. Defaults to the last summer_export_game result. */
7
7
  file?: string;
8
8
  clientVersion?: string;
9
- /** Also approve the Build for players (needs a game that passed review). */
9
+ /** Also make the Build available to players (summer_go_live does this and the release in one step). */
10
10
  publish?: boolean;
11
11
  /**
12
12
  * A web build or native download (summer_export_game format "download"):
@@ -30,9 +30,10 @@ export interface Store {
30
30
  call<T = Record<string, unknown>>(method: string, path: string, options?: {
31
31
  body?: unknown;
32
32
  key?: string;
33
+ headers?: Record<string, string>;
33
34
  }): Promise<T>;
34
35
  }
35
- export declare function storeError(status: number, code: string, message: string | undefined, requestId?: string): BuildToolError;
36
+ export declare function storeError(status: number, code: string, message: string | undefined, requestId?: string, detail?: Record<string, unknown>): BuildToolError;
36
37
  export declare function createStore(baseUrl: string, token: string, deps: PublishBuildDependencies): Store;
37
38
  /** Whole-file and per-part SHA-256 ("sha256:<hex>") in one pass. */
38
39
  export declare function hashParts(path: string, partSize: number): Promise<{
@@ -61,6 +62,9 @@ export declare function uploadParts(sign: (partNumbers: number[]) => Promise<Gra
61
62
  partCount: number;
62
63
  }, deps: PublishBuildDependencies, parts?: number[]): Promise<void>;
63
64
  export declare function normalizeClientVersion(value?: string): string;
65
+ /** gameId is required: the error carries the person's games, or why the store did not list them. */
66
+ export declare function listGamesOrThrow(deps: PublishBuildDependencies): Promise<never>;
67
+ export declare function storeToken(deps: PublishBuildDependencies): Promise<string>;
64
68
  /**
65
69
  * Before the person confirms, prove the target is real: the game exists for
66
70
  * this account, and its store page lists the platforms the export targets.
@@ -74,6 +78,13 @@ export declare function checkStoreGame(gameId: string, exportTargets: string[],
74
78
  };
75
79
  warnings: string[];
76
80
  }>;
81
+ /**
82
+ * Why the platform did not take a Build to players when asked: the Game's
83
+ * approval state as the platform reports it. Staff review is becoming a
84
+ * step after launch, so a pending review is reported as Summer's hold, not
85
+ * as something the creator waits on or fixes.
86
+ */
87
+ export declare function platformHold(store: Store, gameId: string): Promise<Record<string, unknown>>;
77
88
  export type PublishBuildResult = Record<string, unknown> & {
78
89
  ok: true;
79
90
  status: string;