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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +2 -2
- package/.factory-plugin/plugin.json +2 -2
- package/CHANGELOG.md +16 -2
- package/README.md +0 -4
- package/dist/cli/commands/login.d.ts +0 -10
- package/dist/cli/commands/login.js +2 -82
- package/dist/cli/index.js +0 -4
- package/dist/core/auth.d.ts +0 -8
- package/dist/core/auth.js +5 -35
- package/dist/core/capabilities/seeing/args.d.ts +2 -2
- package/dist/core/capabilities/seeing/seeing.d.ts +1 -1
- package/dist/core/capabilities/sfx-choices.d.ts +78 -0
- package/dist/core/capabilities/sfx-choices.js +253 -0
- package/dist/core/capabilities/tool-dispatch.js +35 -52
- package/dist/core/config.d.ts +1 -7
- package/dist/core/config.js +5 -75
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools/creator-tools.js +0 -71
- package/dist/mcp/tools/generate-tools.js +69 -3
- package/gemini-extension.json +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +6 -7
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/skills/README.md +2 -2
- package/library/skills/ambient-bed/SKILL.md +3 -1
- package/library/skills/ambient-bed/resource.yaml +1 -1
- package/library/skills/audio-direction/SKILL.md +5 -5
- package/library/skills/audio-direction/resource.yaml +1 -1
- package/library/skills/export-and-ship/SKILL.md +2 -0
- package/library/skills/multiplayer-publish/SKILL.md +8 -4
- package/library/skills/publish-your-game/SKILL.md +2 -0
- package/library/skills/release-readiness/SKILL.md +141 -2
- package/library/skills/release-readiness/resource.yaml +4 -1
- package/library/skills/sound-effect/SKILL.md +83 -158
- package/library/skills/sound-effect/resource.yaml +8 -2
- package/library/skills/ui-basics/SKILL.md +1 -0
- package/library/tools/generate-audio/resource.yaml +6 -2
- package/library/tools/generate-sfx-choices/resource.yaml +69 -0
- package/library/tools/get-choices/resource.yaml +50 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +2 -3
- package/registry/generated/counts.json +2 -2
- package/registry/generated/gemini-extension.json +1 -1
- package/registry/generated/index.json +105 -121
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +2 -2
- package/registry/generated/plugin.codex.json +3 -3
- package/registry/generated/plugin.cursor.json +2 -2
- package/registry/generated/plugin.factory.json +2 -2
- package/registry/generated/skills-index.md +2 -2
- package/registry/generated/skills-registry.json +2 -2
- package/dist/cli/commands/publish.d.ts +0 -2
- package/dist/cli/commands/publish.js +0 -21
- package/dist/cli/commands/releases.d.ts +0 -2
- package/dist/cli/commands/releases.js +0 -20
- package/dist/core/capabilities/creator.d.ts +0 -65
- package/dist/core/capabilities/creator.js +0 -374
- package/library/tools/creator-config/resource.yaml +0 -63
- package/library/tools/creator-publish/resource.yaml +0 -74
- 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 {
|
|
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 ?? {}),
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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"
|
|
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>;
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
48
|
-
"An all-black screenshot means
|
|
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_*;
|
|
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
|
}
|