summer-engine 3.5.2 → 3.7.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 (63) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -3
  4. package/.cursor-plugin/plugin.json +2 -2
  5. package/.factory-plugin/plugin.json +2 -2
  6. package/CHANGELOG.md +16 -2
  7. package/README.md +0 -4
  8. package/dist/cli/commands/login.d.ts +0 -10
  9. package/dist/cli/commands/login.js +2 -82
  10. package/dist/cli/index.js +0 -4
  11. package/dist/core/auth.d.ts +0 -8
  12. package/dist/core/auth.js +5 -35
  13. package/dist/core/capabilities/seeing/args.d.ts +2 -2
  14. package/dist/core/capabilities/seeing/seeing.d.ts +1 -1
  15. package/dist/core/capabilities/sfx-choices.d.ts +78 -0
  16. package/dist/core/capabilities/sfx-choices.js +253 -0
  17. package/dist/core/capabilities/tool-dispatch.js +35 -52
  18. package/dist/core/config.d.ts +1 -7
  19. package/dist/core/config.js +5 -75
  20. package/dist/mcp/server.d.ts +2 -1
  21. package/dist/mcp/server.js +6 -5
  22. package/dist/mcp/tools/creator-tools.js +0 -71
  23. package/dist/mcp/tools/generate-tools.js +69 -3
  24. package/gemini-extension.json +1 -1
  25. package/library/references/mcp-tools-reference/mcp-tools-reference.md +6 -7
  26. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  27. package/library/skills/README.md +2 -2
  28. package/library/skills/ambient-bed/SKILL.md +3 -1
  29. package/library/skills/ambient-bed/resource.yaml +1 -1
  30. package/library/skills/audio-direction/SKILL.md +5 -5
  31. package/library/skills/audio-direction/resource.yaml +1 -1
  32. package/library/skills/export-and-ship/SKILL.md +2 -0
  33. package/library/skills/multiplayer-publish/SKILL.md +8 -4
  34. package/library/skills/publish-your-game/SKILL.md +2 -0
  35. package/library/skills/release-readiness/SKILL.md +141 -2
  36. package/library/skills/release-readiness/resource.yaml +4 -1
  37. package/library/skills/sound-effect/SKILL.md +83 -158
  38. package/library/skills/sound-effect/resource.yaml +8 -2
  39. package/library/skills/ui-basics/SKILL.md +1 -0
  40. package/library/tools/generate-audio/resource.yaml +6 -2
  41. package/library/tools/generate-sfx-choices/resource.yaml +69 -0
  42. package/library/tools/get-choices/resource.yaml +50 -0
  43. package/package.json +1 -1
  44. package/registry/generated/aliases.json +2 -3
  45. package/registry/generated/counts.json +2 -2
  46. package/registry/generated/gemini-extension.json +1 -1
  47. package/registry/generated/index.json +105 -121
  48. package/registry/generated/marketplace.claude.json +2 -2
  49. package/registry/generated/plugin.claude.json +2 -2
  50. package/registry/generated/plugin.codex.json +3 -3
  51. package/registry/generated/plugin.cursor.json +2 -2
  52. package/registry/generated/plugin.factory.json +2 -2
  53. package/registry/generated/skills-index.md +2 -2
  54. package/registry/generated/skills-registry.json +2 -2
  55. package/dist/cli/commands/publish.d.ts +0 -2
  56. package/dist/cli/commands/publish.js +0 -21
  57. package/dist/cli/commands/releases.d.ts +0 -2
  58. package/dist/cli/commands/releases.js +0 -20
  59. package/dist/core/capabilities/creator.d.ts +0 -65
  60. package/dist/core/capabilities/creator.js +0 -374
  61. package/library/tools/creator-config/resource.yaml +0 -63
  62. package/library/tools/creator-publish/resource.yaml +0 -74
  63. package/library/tools/creator-releases/resource.yaml +0 -53
@@ -0,0 +1,253 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Sound-effect choices (summer_generate_sfx_choices + summer_get_choices).
4
+ *
5
+ * Sound-effect models differ a lot from one sound to the next, so the agent
6
+ * can make one sound with several models and let the person pick by ear:
7
+ * POST /api/mcp/generate/audio/choices makes the versions and a choice set,
8
+ * the person listens and approves one or more on the Studio page
9
+ * /studio/choices/<id>, and GET /api/mcp/choices/<id> reads the decision.
10
+ * The server queues the page for the person's open Studio tab itself
11
+ * (studioTab.queued); only when no tab took it does this open a browser.
12
+ * An approval makes that model the person's default for later
13
+ * summer_generate_audio sound_effects calls without a modelId.
14
+ *
15
+ * One implementation for both faces: the MCP tools (generate-tools.ts) and
16
+ * `summer tool generate-sfx-choices` / `get-choices` (tool-dispatch.ts) pass
17
+ * their own gateway request functions and share everything else here.
18
+ */
19
+ export const SFX_MODEL_IDS = ["eleven_text_to_sound_v2", "sonilo-sfx-1.1", "stable-audio-3-sfx"];
20
+ export const SFX_CHOICES_ENDPOINT = "/api/mcp/generate/audio/choices";
21
+ export const CHOICES_ENDPOINT_PREFIX = "/api/mcp/choices/";
22
+ /** Three models run in parallel on the server; the slowest decides. */
23
+ export const SFX_CHOICES_TIMEOUT_MS = 240_000;
24
+ export const MAX_CHOICES_WAIT_SECONDS = 50;
25
+ export const sfxChoicesArgsSchema = z.object({
26
+ text: z
27
+ .string()
28
+ .min(1)
29
+ .max(1000)
30
+ .describe("The sound to make, written as a Foley description (what hits what, the attack and the tail, close-mic, dry)"),
31
+ durationSeconds: z
32
+ .number()
33
+ .positive()
34
+ .max(30)
35
+ .optional()
36
+ .describe("Exact length in seconds. One-shots 0.5-2; animation-synced sounds match the animation. stable-audio-3-sfx needs >= 1, sonilo-sfx-1.1 >= 0.5"),
37
+ loop: z
38
+ .boolean()
39
+ .optional()
40
+ .describe("Ask for a seamless loop (eleven_text_to_sound_v2 only; say 'seamless loop' in text too)"),
41
+ models: z
42
+ .array(z.enum(SFX_MODEL_IDS))
43
+ .min(2)
44
+ .max(4)
45
+ .optional()
46
+ .describe("2-4 model ids to compare. Default: eleven_text_to_sound_v2, sonilo-sfx-1.1, stable-audio-3-sfx"),
47
+ open: z
48
+ .boolean()
49
+ .default(true)
50
+ .describe("Show the choices page in the browser when no open Studio tab took it. false: no browser, only return the URL"),
51
+ });
52
+ export const getChoicesArgsSchema = z.object({
53
+ choiceSetId: z.string().min(1).max(200).describe("The choiceSetId from summer_generate_sfx_choices"),
54
+ waitSeconds: z
55
+ .number()
56
+ .int()
57
+ .min(0)
58
+ .max(MAX_CHOICES_WAIT_SECONDS)
59
+ .optional()
60
+ .describe("Wait up to this many seconds (max 50) for the person to decide before answering. 0 or omitted: answer now"),
61
+ });
62
+ // ---------------------------------------------------------------------------
63
+ // Showing the page
64
+ // ---------------------------------------------------------------------------
65
+ function hostOf(url) {
66
+ try {
67
+ return new URL(url).hostname.toLowerCase();
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ function isSummerHost(host) {
74
+ return host === "summerengine.com" || (host?.endsWith(".summerengine.com") ?? false);
75
+ }
76
+ function isLoopback(host) {
77
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
78
+ }
79
+ /** Same rule as summer_open: a browser only ever opens summerengine.com, its subdomains, or loopback. */
80
+ export function canOpenReviewUrl(url) {
81
+ const host = hostOf(url);
82
+ return isSummerHost(host) || isLoopback(host);
83
+ }
84
+ /** False where a browser launch cannot reach the person: CI, or Linux with no display. */
85
+ export function defaultCanOpenBrowser(env = process.env, platform = process.platform) {
86
+ if (env.CI)
87
+ return false;
88
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
89
+ return false;
90
+ return true;
91
+ }
92
+ /** The server's studioTab answer: queued for the person's open tab, or why not. Missing on older servers. */
93
+ function studioTabAnswer(value) {
94
+ if (!value || typeof value !== "object")
95
+ return null;
96
+ const record = value;
97
+ return {
98
+ queued: record.queued === true,
99
+ ...(typeof record.reason === "string" ? { reason: record.reason } : {}),
100
+ };
101
+ }
102
+ async function showChoicesPage(reviewUrl, studioTab, deps) {
103
+ const notQueued = studioTab ? (studioTab.reason ?? "not_queued") : undefined;
104
+ if (!canOpenReviewUrl(reviewUrl))
105
+ return { shown: "not_opened", studioTab: notQueued, browser: "not_a_summer_url" };
106
+ if (!deps.canOpenBrowser())
107
+ return { shown: "not_opened", studioTab: notQueued, browser: "no_browser_here" };
108
+ try {
109
+ await deps.openUrl(reviewUrl);
110
+ return { shown: "browser", studioTab: notQueued };
111
+ }
112
+ catch (error) {
113
+ return { shown: "not_opened", studioTab: notQueued, browser: error instanceof Error ? error.message : String(error) };
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // summer_generate_sfx_choices
118
+ // ---------------------------------------------------------------------------
119
+ export function sfxChoicesRequestBody(args) {
120
+ const body = { capability: "sound_effects", text: args.text };
121
+ if (args.durationSeconds !== undefined)
122
+ body.durationSeconds = args.durationSeconds;
123
+ if (args.loop !== undefined)
124
+ body.loop = args.loop;
125
+ if (args.models)
126
+ body.models = args.models;
127
+ return body;
128
+ }
129
+ function clipSummary(clip) {
130
+ return {
131
+ clipId: clip.clipId,
132
+ modelId: clip.modelId,
133
+ modelName: clip.modelName,
134
+ audioUrl: clip.audioUrl,
135
+ assetId: clip.assetId,
136
+ durationSeconds: clip.durationSeconds,
137
+ };
138
+ }
139
+ export async function generateSfxChoices(args, deps) {
140
+ const reply = await deps.post(SFX_CHOICES_ENDPOINT, sfxChoicesRequestBody(args), SFX_CHOICES_TIMEOUT_MS);
141
+ if (reply.error) {
142
+ return { ok: false, error: reply.error, ...(reply.data ? { details: reply.data } : {}) };
143
+ }
144
+ const data = reply.data ?? {};
145
+ const clips = Array.isArray(data.clips) ? data.clips : [];
146
+ const failed = Array.isArray(data.failed) ? data.failed : [];
147
+ const choiceSetId = typeof data.choiceSetId === "string" ? data.choiceSetId : null;
148
+ const reviewUrl = typeof data.reviewUrl === "string" ? data.reviewUrl : null;
149
+ const studioPath = typeof data.studioPath === "string" ? data.studioPath : undefined;
150
+ if (!choiceSetId || !reviewUrl || clips.length === 0) {
151
+ return {
152
+ ok: false,
153
+ error: clips.length === 0 ? "No model made a version of this sound." : "The server did not return a choice set.",
154
+ failed,
155
+ hint: "Read failed[] for each model's error. Fix the request (a longer durationSeconds, a plainer description) or call summer_generate_audio with one modelId.",
156
+ };
157
+ }
158
+ // The server queued the Studio-tab open already; that happens even with
159
+ // open:false, so report it rather than hide it.
160
+ const studioTab = studioTabAnswer(data.studioTab);
161
+ const shown = studioTab?.queued
162
+ ? { shown: "studio_tab" }
163
+ : args.open
164
+ ? await showChoicesPage(reviewUrl, studioTab, deps)
165
+ : { shown: "not_opened", browser: "open_false" };
166
+ const count = clips.length;
167
+ const where = shown.shown === "studio_tab"
168
+ ? "I opened them in your Summer Studio tab"
169
+ : shown.shown === "browser"
170
+ ? "I opened them in your browser"
171
+ : `Open ${reviewUrl} to hear them`;
172
+ const say = `I made ${count} versions of this sound with ${count} different models. ${where}. Listen, approve the ones you like (one or more), then tell me. The model you approve becomes your default for sound effects.`;
173
+ return {
174
+ ok: true,
175
+ choiceSetId,
176
+ prompt: typeof data.prompt === "string" ? data.prompt : args.text,
177
+ reviewUrl,
178
+ ...(studioPath ? { studioPath } : {}),
179
+ shown: shown.shown,
180
+ ...(shown.studioTab || shown.browser
181
+ ? { notShownBecause: { ...(shown.studioTab ? { studioTab: shown.studioTab } : {}), ...(shown.browser ? { browser: shown.browser } : {}) } }
182
+ : {}),
183
+ clips: clips.map(clipSummary),
184
+ failed,
185
+ say,
186
+ next: `Tell the person the "say" text (include reviewUrl when shown is "not_opened"). Do not pick for them. Then call summer_get_choices with choiceSetId "${choiceSetId}" and waitSeconds ${MAX_CHOICES_WAIT_SECONDS}; call it again while status is "pending".`,
187
+ };
188
+ }
189
+ // ---------------------------------------------------------------------------
190
+ // summer_get_choices
191
+ // ---------------------------------------------------------------------------
192
+ function fileExtension(url) {
193
+ if (!url)
194
+ return "wav";
195
+ try {
196
+ const match = /\.(wav|mp3|ogg)$/i.exec(new URL(url).pathname);
197
+ return match ? match[1].toLowerCase() : "wav";
198
+ }
199
+ catch {
200
+ return "wav";
201
+ }
202
+ }
203
+ export function choicesEndpoint(choiceSetId) {
204
+ return `${CHOICES_ENDPOINT_PREFIX}${encodeURIComponent(choiceSetId)}`;
205
+ }
206
+ export function choicesTimeoutMs(waitSeconds) {
207
+ return ((waitSeconds ?? 0) + 20) * 1000;
208
+ }
209
+ export async function getChoices(args, deps) {
210
+ const params = new URLSearchParams();
211
+ if (args.waitSeconds)
212
+ params.set("waitSeconds", String(args.waitSeconds));
213
+ const reply = await deps.get(choicesEndpoint(args.choiceSetId), params, choicesTimeoutMs(args.waitSeconds));
214
+ if (reply.error) {
215
+ return { ok: false, error: reply.error, ...(reply.data ? { details: reply.data } : {}) };
216
+ }
217
+ const data = reply.data ?? {};
218
+ const clips = Array.isArray(data.clips) ? data.clips : [];
219
+ const status = data.status === "decided" ? "decided" : "pending";
220
+ const approved = clips
221
+ .filter((clip) => clip.approved === true)
222
+ .map((clip) => ({
223
+ ...clipSummary(clip),
224
+ ...(clip.note ? { note: clip.note } : {}),
225
+ fileExtension: fileExtension(clip.audioUrl),
226
+ }));
227
+ let next;
228
+ if (status === "pending") {
229
+ next = `The person has not decided yet. Call summer_get_choices again with waitSeconds ${MAX_CHOICES_WAIT_SECONDS}, or wait until they say they picked. Do not pick for them.`;
230
+ }
231
+ else if (approved.length === 0) {
232
+ next = "They approved none. Ask what to change (the description, the length, the models) and run summer_generate_sfx_choices again.";
233
+ }
234
+ else {
235
+ next =
236
+ "Import each approved clip into the project: summer_import_from_url({ url: audioUrl, path: \"res://audio/sfx/<name>.<fileExtension>\" }) " +
237
+ "or summer_import_asset_by_id({ assetId, path }). Read each clip's note. Then wire it (sound-effect skill, step Wire). " +
238
+ (typeof data.preferredModelId === "string"
239
+ ? `Their default sound-effect model is now ${data.preferredModelId}: later summer_generate_audio sound_effects calls without modelId use it.`
240
+ : "");
241
+ }
242
+ return {
243
+ ok: true,
244
+ choiceSetId: typeof data.choiceSetId === "string" ? data.choiceSetId : args.choiceSetId,
245
+ kind: data.kind,
246
+ prompt: data.prompt,
247
+ status,
248
+ preferredModelId: data.preferredModelId ?? null,
249
+ approved,
250
+ clips: clips.map((clip) => ({ ...clipSummary(clip), approved: clip.approved === true, ...(clip.note ? { note: clip.note } : {}) })),
251
+ next: next.trim(),
252
+ };
253
+ }
@@ -30,6 +30,7 @@ import { lookupApiDocs } from "./api-docs.js";
30
30
  import { BOARD_ENDPOINT, boardArgsSchema, boardQuery } from "./board.js";
31
31
  import { IMAGE_GENERATION_TIMEOUT_MS, imageGenerationArgsSchema, saveGeneratedImage } from "./image-generation.js";
32
32
  import { buildMotionRequestBody, motionGenerationArgsSchema } from "./motion-generation.js";
33
+ import { defaultCanOpenBrowser, generateSfxChoices, getChoices, getChoicesArgsSchema, sfxChoicesArgsSchema, } from "./sfx-choices.js";
33
34
  import { ImportHdriError, importHdriArgsSchema, importPolyHavenHdri } from "./hdri-import.js";
34
35
  import { FABRICATE_FALLBACK, buildFabricateMeshOp, fabricateArgsSchema } from "./fabricate-mesh.js";
35
36
  import { buildUiActionsOp, buildUiActivateOp, buildUiScreenshotOp, buildUiTreeOp, executeUiOp, uiActionsArgsSchema, uiActivateArgsSchema, uiScreenshotArgsSchema, uiScreenshotCaption, uiScreenshotImage, uiTreeArgsSchema, withUiFailureDetails, } from "./ui-control.js";
@@ -46,12 +47,11 @@ import { getAuthToken } from "../auth.js";
46
47
  import open from "open";
47
48
  import { openArgsSchema, runOpen } from "./navigation/open.js";
48
49
  import { shapeEngineLogResponse } from "../log-filters.js";
49
- import { listCreatorReleases, publishCreator, } from "./creator.js";
50
50
  import { exportGame } from "./export-game.js";
51
51
  import { exportTemplates } from "./export-templates.js";
52
52
  import { captureGameplay } from "./capture-gameplay.js";
53
53
  import { publishBuild } from "./publish-build.js";
54
- import { CONFIG_KEYS, getConfigValue, isConfigKey, readSummerConfig, resolveGatewayUrl, setConfigValue, unsetConfigValue, } from "../config.js";
54
+ import { resolveGatewayUrl } from "../config.js";
55
55
  import { createDebugReportArtifact } from "./debug-report.js";
56
56
  import { buildGameTaskPlan, gameTaskPlanInputSchema } from "./game-task-plan.js";
57
57
  import { buildProjectContext, projectContextInputSchema, projectSettingValue, } from "./project-context.js";
@@ -393,6 +393,25 @@ function optStr(args, key) {
393
393
  }
394
394
  /** Validate CLI args against the SAME zod schema the MCP face uses, so the
395
395
  * two surfaces reject the same inputs with a readable message. */
396
+ /** The gateway helpers as the shared sfx-choices module expects them: a reply, never a throw. */
397
+ async function gatewayReply(call) {
398
+ try {
399
+ return { data: await call(), status: 200 };
400
+ }
401
+ catch (error) {
402
+ if (!(error instanceof ToolDispatchError))
403
+ throw error;
404
+ return { error: error.message, status: 0 };
405
+ }
406
+ }
407
+ function sfxChoicesDeps() {
408
+ return {
409
+ post: (endpoint, body, timeoutMs) => gatewayReply(() => gatewayPost(endpoint, body, timeoutMs)),
410
+ get: (endpoint, params, timeoutMs) => gatewayReply(() => gatewayGet(endpoint, params, timeoutMs)),
411
+ openUrl: (url) => open(url),
412
+ canOpenBrowser: () => defaultCanOpenBrowser(),
413
+ };
414
+ }
396
415
  function parseToolArgs(schema, args, tool) {
397
416
  const parsed = schema.safeParse(args);
398
417
  if (parsed.success)
@@ -653,56 +672,6 @@ export const TOOL_DISPATCH = [
653
672
  ...(typeof args.waitSeconds === "number" ? { waitSeconds: args.waitSeconds } : {}),
654
673
  face: "cli",
655
674
  })),
656
- entry("summer_creator_publish", "Deprecated: publish a .pck to the legacy creator API; use publish-build", false, (args) => publishCreator({
657
- project: optStr(args, "project"),
658
- artifact: optStr(args, "artifact"),
659
- version: optStr(args, "version"),
660
- manifest: optStr(args, "manifest"),
661
- projectId: optStr(args, "projectId"),
662
- channel: optStr(args, "channel"),
663
- notes: optStr(args, "notes"),
664
- confirm: args.confirm === true,
665
- face: "cli",
666
- })),
667
- entry("summer_creator_releases", "Deprecated: list releases on the legacy creator API; use summer_store_releases", false, (args) => listCreatorReleases({
668
- projectId: optStr(args, "projectId"),
669
- limit: typeof args.limit === "number" ? args.limit : 20,
670
- cursor: optStr(args, "cursor"),
671
- face: "cli",
672
- })),
673
- entry("summer_creator_config", "Read or update the shared non-secret Summer configuration", false, async (args) => {
674
- const action = str(args, "action");
675
- if (action === "list") {
676
- const config = await readSummerConfig();
677
- return {
678
- ok: true,
679
- values: Object.fromEntries(CONFIG_KEYS.map((name) => [name, getConfigValue(config, name) ?? null])),
680
- };
681
- }
682
- const key = optStr(args, "key");
683
- if (!key || !isConfigKey(key)) {
684
- throw new ToolDispatchError(`A valid key is required. Use one of ${CONFIG_KEYS.join(", ")}.`);
685
- }
686
- if (action === "get") {
687
- return { ok: true, key, value: getConfigValue(await readSummerConfig(), key) ?? null };
688
- }
689
- if (args.confirm !== true) {
690
- throw new ToolDispatchError(`Changing ${key} requires confirmation. Retry with "confirm": true after the user approves the exact change.`);
691
- }
692
- if (action === "set") {
693
- const value = optStr(args, "value");
694
- if (value === undefined) {
695
- throw new ToolDispatchError(`A value is required for ${key}.`);
696
- }
697
- const config = await setConfigValue(key, value);
698
- return { ok: true, key, value: getConfigValue(config, key) };
699
- }
700
- if (action === "unset") {
701
- const config = await unsetConfigValue(key);
702
- return { ok: true, key, value: getConfigValue(config, key) ?? null };
703
- }
704
- throw new ToolDispatchError(`Unknown action "${action}". Use list, get, set, or unset.`);
705
- }),
706
675
  // --- debug ---
707
676
  entry("summer_get_diagnostics", "Overview of editor console and runtime debugger errors/warnings", true, async (args, ctx) => requireEngineSuccess(await (await ctx.engine()).getDiagnostics())),
708
677
  entry("summer_get_console", "Read recent editor Output panel messages (deduplicated)", true, async (args, ctx) => {
@@ -917,12 +886,26 @@ export const TOOL_DISPATCH = [
917
886
  }
918
887
  if (typeof args.durationSeconds === "number")
919
888
  body.durationSeconds = args.durationSeconds;
889
+ if (typeof args.loop === "boolean")
890
+ body.loop = args.loop;
920
891
  if (Array.isArray(args.inputs))
921
892
  body.inputs = args.inputs;
922
893
  if (args.options)
923
894
  body.options = args.options;
924
895
  return gatewayPost("/api/mcp/generate/audio", body);
925
896
  }),
897
+ entry("summer_generate_sfx_choices", "Make one sound effect with several models and show them for the person to pick", false, async (args) => {
898
+ const result = await generateSfxChoices(parseToolArgs(sfxChoicesArgsSchema, args, "generate-sfx-choices"), sfxChoicesDeps());
899
+ if (result.ok !== true)
900
+ throw new ToolResultError(result, String(result.error));
901
+ return result;
902
+ }),
903
+ entry("summer_get_choices", "Read which sound-effect versions the person approved", false, async (args) => {
904
+ const result = await getChoices(parseToolArgs(getChoicesArgsSchema, args, "get-choices"), sfxChoicesDeps());
905
+ if (result.ok !== true)
906
+ throw new ToolResultError(result, String(result.error));
907
+ return result;
908
+ }),
926
909
  entry("summer_generate_3d", "Generate a 3D model (optional auto-rig + animations)", false, async (args) => {
927
910
  const options = {
928
911
  ...(args.options ?? {}),
@@ -3,13 +3,8 @@ export interface SummerConfig {
3
3
  gateway?: {
4
4
  url?: string;
5
5
  };
6
- creator?: {
7
- apiUrl?: string;
8
- projectId?: string;
9
- channel?: string;
10
- };
11
6
  }
12
- export declare const CONFIG_KEYS: readonly ["gateway.url", "creator.apiUrl", "creator.projectId", "creator.channel"];
7
+ export declare const CONFIG_KEYS: readonly ["gateway.url"];
13
8
  export type ConfigKey = (typeof CONFIG_KEYS)[number];
14
9
  export declare function readSummerConfig(): Promise<SummerConfig>;
15
10
  export declare function getConfigValue(config: SummerConfig, key: ConfigKey): string | undefined;
@@ -27,4 +22,3 @@ export declare function isConfigKey(value: string): value is ConfigKey;
27
22
  * Returns an origin-like base without a trailing slash.
28
23
  */
29
24
  export declare function resolveGatewayUrl(): Promise<string>;
30
- export declare function getCreatorApiUrl(): Promise<string>;
@@ -1,13 +1,7 @@
1
1
  import { readStoreJson, writeStoreJson } from "./store.js";
2
2
  const CONFIG_FILE = "config.json";
3
3
  const DEFAULT_GATEWAY_URL = "https://www.summerengine.com";
4
- const DEFAULT_CREATOR_API_URL = "https://summercraft.ai";
5
- export const CONFIG_KEYS = [
6
- "gateway.url",
7
- "creator.apiUrl",
8
- "creator.projectId",
9
- "creator.channel",
10
- ];
4
+ export const CONFIG_KEYS = ["gateway.url"];
11
5
  function emptyConfig() {
12
6
  return { schemaVersion: 1 };
13
7
  }
@@ -39,90 +33,30 @@ function validateGatewayUrl(value) {
39
33
  }
40
34
  return parsed.toString().replace(/\/+$/, "");
41
35
  }
42
- function validateCreatorApiUrl(value) {
43
- let parsed;
44
- try {
45
- parsed = new URL(value);
46
- }
47
- catch {
48
- throw new Error('creator.apiUrl must be a complete URL. Recovery: use "summer config set creator.apiUrl https://summercraft.ai".');
49
- }
50
- const local = parsed.hostname === "localhost" ||
51
- parsed.hostname === "127.0.0.1" ||
52
- parsed.hostname === "[::1]";
53
- if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
54
- throw new Error("creator.apiUrl must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
55
- }
56
- if (parsed.username ||
57
- parsed.password ||
58
- parsed.pathname !== "/" ||
59
- parsed.search ||
60
- parsed.hash) {
61
- throw new Error("creator.apiUrl must be an origin without credentials, a path, query parameters, or fragments.");
62
- }
63
- return parsed.origin;
64
- }
65
36
  function validateValue(key, value) {
66
37
  const clean = value.trim();
67
38
  if (!clean) {
68
39
  throw new Error(`Cannot set ${key} to an empty value. Recovery: provide a value, or run "summer config unset ${key}".`);
69
40
  }
70
- if (key === "gateway.url")
71
- return validateGatewayUrl(clean);
72
- if (key === "creator.apiUrl")
73
- return validateCreatorApiUrl(clean);
74
- if (key === "creator.channel" && !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(clean)) {
75
- throw new Error("creator.channel may contain letters, numbers, dots, underscores, and hyphens. Recovery: choose a channel such as production or preview.");
76
- }
77
- if (key === "creator.projectId" && clean.length > 128) {
78
- throw new Error("creator.projectId is too long. Recovery: copy the project ID from the Summer creator dashboard and retry.");
79
- }
80
- return clean;
41
+ return validateGatewayUrl(clean);
81
42
  }
82
43
  export function getConfigValue(config, key) {
83
- if (key === "gateway.url")
84
- return config.gateway?.url;
85
- if (key === "creator.apiUrl")
86
- return config.creator?.apiUrl;
87
- if (key === "creator.projectId")
88
- return config.creator?.projectId;
89
- return config.creator?.channel;
44
+ return config.gateway?.url;
90
45
  }
91
46
  export async function setConfigValue(key, rawValue) {
92
47
  const config = await readSummerConfig();
93
48
  const value = validateValue(key, rawValue);
94
- if (key === "gateway.url") {
95
- config.gateway = { ...config.gateway, url: value };
96
- }
97
- else if (key === "creator.apiUrl") {
98
- config.creator = { ...config.creator, apiUrl: value };
99
- }
100
- else if (key === "creator.projectId") {
101
- config.creator = { ...config.creator, projectId: value };
102
- }
103
- else {
104
- config.creator = { ...config.creator, channel: value };
105
- }
49
+ config.gateway = { ...config.gateway, url: value };
106
50
  await writeStoreJson(CONFIG_FILE, config);
107
51
  return config;
108
52
  }
109
53
  export async function unsetConfigValue(key) {
110
54
  const config = await readSummerConfig();
111
- if (key === "gateway.url" && config.gateway)
55
+ if (config.gateway)
112
56
  delete config.gateway.url;
113
- if (key === "creator.apiUrl" && config.creator)
114
- delete config.creator.apiUrl;
115
- if (key === "creator.projectId" && config.creator) {
116
- delete config.creator.projectId;
117
- }
118
- if (key === "creator.channel" && config.creator)
119
- delete config.creator.channel;
120
57
  if (config.gateway && Object.keys(config.gateway).length === 0) {
121
58
  delete config.gateway;
122
59
  }
123
- if (config.creator && Object.keys(config.creator).length === 0) {
124
- delete config.creator;
125
- }
126
60
  await writeStoreJson(CONFIG_FILE, config);
127
61
  return config;
128
62
  }
@@ -147,7 +81,3 @@ export async function resolveGatewayUrl() {
147
81
  const configured = config.gateway?.url?.trim();
148
82
  return configured ? validateGatewayUrl(configured) : DEFAULT_GATEWAY_URL;
149
83
  }
150
- export async function getCreatorApiUrl() {
151
- const config = await readSummerConfig();
152
- return config.creator?.apiUrl ?? DEFAULT_CREATOR_API_URL;
153
- }
@@ -11,7 +11,8 @@ export declare const getCachedBootDriftNotice: typeof getCachedNotice;
11
11
  * turn and would otherwise learn the hard way. Kept under 600 characters so
12
12
  * it costs a few hundred tokens once; the full operating guide stays in the
13
13
  * summer_agent_playbook prompt / summer_get_agent_playbook tool. The last
14
- * sentence names the publishing path and the owner-approval rule.
14
+ * sentence names the publishing path, the owner-approval rule and where
15
+ * review feedback is read.
15
16
  */
16
17
  export declare const SUMMER_MCP_INSTRUCTIONS: string;
17
18
  export declare function configureMcpEngineSelection(selection?: EngineSelection): void;
@@ -40,14 +40,15 @@ export const getCachedBootDriftNotice = getCachedNotice;
40
40
  * turn and would otherwise learn the hard way. Kept under 600 characters so
41
41
  * it costs a few hundred tokens once; the full operating guide stays in the
42
42
  * summer_agent_playbook prompt / summer_get_agent_playbook tool. The last
43
- * sentence names the publishing path and the owner-approval rule.
43
+ * sentence names the publishing path, the owner-approval rule and where
44
+ * review feedback is read.
44
45
  */
45
- export const SUMMER_MCP_INSTRUCTIONS = "Summer Engine MCP. Call summer_get_project_context first: it binds the open project and returns mainScene, projectMemory and any capabilitySkewWarning. " +
46
+ export const SUMMER_MCP_INSTRUCTIONS = "Summer Engine MCP. Call summer_get_project_context first: it binds the open project, returns mainScene, projectMemory, any capabilitySkewWarning. " +
46
47
  "Before building or fixing, summer_search_library then summer_read_library the hit. " +
47
- "After a playthrough read summer_get_diagnostics, not summer_get_console alone. " +
48
- "An all-black screenshot means the viewport had not redrawn: recapture. " +
48
+ "After play, read summer_get_diagnostics, not summer_get_console alone. " +
49
+ "An all-black screenshot means no redraw yet: recapture. " +
49
50
  "Report verified entries via summer_library_feedback. " +
50
- "Publishing: summer_export_game, summer_publish_build, summer_store_*; an agent never publishes alone: summer_store_submit returns the owner's approval link.";
51
+ "Publishing: summer_export_game, summer_publish_build, summer_store_*; agents never publish alone: summer_store_submit returns the owner's approval link; summer_store_review, feedback to fix.";
51
52
  /**
52
53
  * Fire-and-forget probe of the npm registry on MCP boot. Caches the result for
53
54
  * the session. We never await this in startup so a slow / offline registry
@@ -1,27 +1,16 @@
1
1
  import { z } from "zod";
2
- import { CreatorOperationError, listCreatorReleases, publishCreator, } from "../../core/capabilities/creator.js";
3
2
  import { captureGameplay } from "../../core/capabilities/capture-gameplay.js";
4
3
  import { exportGame } from "../../core/capabilities/export-game.js";
5
4
  import { STORE_TARGETS } from "../../core/capabilities/export-presets.js";
6
5
  import { exportTemplates, TEMPLATE_PLATFORMS } from "../../core/capabilities/export-templates.js";
7
6
  import { publishBuild } from "../../core/capabilities/publish-build.js";
8
7
  import { BuildToolError } from "../../core/capabilities/summer-bundle.js";
9
- import { CONFIG_KEYS, getConfigValue, isConfigKey, readSummerConfig, setConfigValue, unsetConfigValue, } from "../../core/config.js";
10
8
  import { textJson } from "./text-json.js";
11
9
  async function creatorResult(operation) {
12
10
  try {
13
11
  return textJson(await operation());
14
12
  }
15
13
  catch (error) {
16
- if (error instanceof CreatorOperationError) {
17
- return textJson({
18
- ok: false,
19
- code: error.code,
20
- operation: error.operation,
21
- message: error.message,
22
- recovery: error.recovery,
23
- }, true);
24
- }
25
14
  if (error instanceof BuildToolError) {
26
15
  return textJson({
27
16
  ok: false,
@@ -116,64 +105,4 @@ export function registerCreatorTools(server) {
116
105
  confirm: z.boolean().default(false).describe("Set true only after the user approves the exact upload."),
117
106
  waitSeconds: z.number().int().min(0).max(1800).optional().describe("How long to wait for Summer to make the Build (default 600). On timeout, call again to keep waiting."),
118
107
  }, async (args) => creatorResult(() => publishBuild({ ...args, face: "mcp" })));
119
- server.tool("summer_creator_publish", "Deprecated: use summer_export_game, then summer_publish_build, which upload the summer.games .zip to the game's store listing. This old path publishes a .pck to the legacy Summercraft creator API with a separate sc_ token and will be removed. First call with confirm=false and present the returned project, version, digest, size, artifact path, channel, and notes; set confirm only after the user approves that exact target. The server independently verifies token scope, ownership, bytes, and review state.", {
120
- project: z.string().optional().describe("Project root. Defaults to the current working directory."),
121
- artifact: z.string().describe("Exact path to the exported Summer .pck artifact."),
122
- version: z.string().describe("Immutable release version."),
123
- manifest: z.string().optional().describe("Optional path to a JSON release manifest."),
124
- projectId: z.string().optional().describe("Creator project ID. Defaults to ~/.summer/config.json."),
125
- channel: z.string().optional().describe("Release channel. Defaults to the configured channel or production."),
126
- notes: z.string().optional().describe("Release notes shown during confirmation and recorded in the local audit."),
127
- confirm: z.boolean().default(false).describe("Set true only after the user approves the exact release target."),
128
- }, async (args) => creatorResult(() => publishCreator({
129
- ...args,
130
- face: "mcp",
131
- })));
132
- server.tool("summer_creator_releases", "Deprecated: lists releases on the legacy Summercraft creator API (separate sc_ token), not summer.games. For what players get on summer.games, and which upload can be released, use summer_store_releases.", {
133
- projectId: z.string().optional().describe("Creator project ID. Defaults to ~/.summer/config.json."),
134
- limit: z.number().int().min(1).max(100).default(20),
135
- cursor: z.string().optional().describe("Opaque nextCursor from the previous page."),
136
- }, async (args) => creatorResult(() => listCreatorReleases({
137
- ...args,
138
- face: "mcp",
139
- })));
140
- server.tool("summer_creator_config", "Read or update the shared non-secret ~/.summer configuration used by the CLI and this MCP. Mutations require confirm=true. Tokens are never returned or accepted by this tool.", {
141
- action: z.enum(["list", "get", "set", "unset"]),
142
- key: z.enum(CONFIG_KEYS).optional(),
143
- value: z.string().optional(),
144
- confirm: z.boolean().default(false),
145
- }, async ({ action, key, value, confirm }) => creatorResult(async () => {
146
- if (action === "list") {
147
- const config = await readSummerConfig();
148
- return {
149
- ok: true,
150
- values: Object.fromEntries(CONFIG_KEYS.map((name) => [
151
- name,
152
- getConfigValue(config, name) ?? null,
153
- ])),
154
- };
155
- }
156
- if (!key || !isConfigKey(key)) {
157
- throw new Error(`A valid key is required. Recovery: use one of ${CONFIG_KEYS.join(", ")}.`);
158
- }
159
- if (action === "get") {
160
- return {
161
- ok: true,
162
- key,
163
- value: getConfigValue(await readSummerConfig(), key) ?? null,
164
- };
165
- }
166
- if (!confirm) {
167
- throw new Error(`Changing ${key} requires confirmation. Recovery: show the exact change to the user, then retry with confirm=true.`);
168
- }
169
- if (action === "set") {
170
- if (value === undefined) {
171
- throw new Error(`A value is required for ${key}. Recovery: provide value and retry with confirm=true.`);
172
- }
173
- const config = await setConfigValue(key, value);
174
- return { ok: true, key, value: getConfigValue(config, key) };
175
- }
176
- const config = await unsetConfigValue(key);
177
- return { ok: true, key, value: getConfigValue(config, key) ?? null };
178
- }));
179
108
  }