summer-engine 2.6.5 → 2.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 (57) 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 -2
  4. package/.cursor-plugin/plugin.json +4 -1
  5. package/.opencode/plugins/summer.js +1 -1
  6. package/AGENTS.md +8 -4
  7. package/CHANGELOG.md +41 -0
  8. package/GEMINI.md +5 -4
  9. package/README.md +25 -20
  10. package/_persona/summer/SKILL.md +1 -0
  11. package/commands/summer.md +13 -0
  12. package/dist/bin/summer.js +4 -0
  13. package/dist/commands/debug.d.ts +2 -0
  14. package/dist/commands/debug.js +44 -0
  15. package/dist/commands/doctor.js +112 -8
  16. package/dist/commands/skills.js +1 -1
  17. package/dist/lib/api-client.d.ts +22 -6
  18. package/dist/lib/api-client.js +161 -36
  19. package/dist/lib/async-op-lifecycle.d.ts +7 -2
  20. package/dist/lib/async-op-lifecycle.js +32 -9
  21. package/dist/lib/cloud/engine-bridge.js +9 -1
  22. package/dist/lib/debug-report.d.ts +50 -0
  23. package/dist/lib/debug-report.js +308 -0
  24. package/dist/lib/game-task-plan.js +4 -4
  25. package/dist/lib/mcp-boot-notice.d.ts +3 -0
  26. package/dist/lib/mcp-boot-notice.js +7 -0
  27. package/dist/lib/mcp-log.d.ts +3 -0
  28. package/dist/lib/mcp-log.js +72 -0
  29. package/dist/lib/skills-registry.d.ts +25 -1
  30. package/dist/lib/skills-registry.js +46 -0
  31. package/dist/mcp/server.d.ts +2 -2
  32. package/dist/mcp/server.js +79 -7
  33. package/dist/mcp/tools/asset-tools.js +33 -12
  34. package/dist/mcp/tools/cloud-tools.js +15 -15
  35. package/dist/mcp/tools/debug-tools.js +44 -1
  36. package/dist/mcp/tools/file-tools.d.ts +2 -0
  37. package/dist/mcp/tools/file-tools.js +154 -0
  38. package/dist/mcp/tools/generate-tools.js +143 -5
  39. package/dist/mcp/tools/project-tools.js +25 -20
  40. package/dist/mcp/tools/scene-tools.js +101 -24
  41. package/dist/mcp/tools/with-engine.js +4 -4
  42. package/hooks/pre-commit-doctor.sh +0 -0
  43. package/hooks/run-hook.cmd +0 -0
  44. package/hooks/session-start.sh +0 -0
  45. package/package.json +6 -2
  46. package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
  47. package/skills/debugging/debug/SKILL.md +23 -1
  48. package/skills/deployment/remote-deploy/SKILL.md +107 -0
  49. package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
  50. package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
  51. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
  52. package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
  53. package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
  54. package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
  55. package/skills/workflow/mcpupdate/SKILL.md +6 -6
  56. package/skills/workflow/using-summer/SKILL.md +20 -1
  57. package/skills/workflow/writing-skills/render-graphs.js +0 -0
@@ -13,6 +13,8 @@ const TOOL_BY_ENDPOINT = {
13
13
  "/api/mcp/generate/3d": "summer_generate_3d",
14
14
  "/api/mcp/generate/video": "summer_generate_video",
15
15
  "/api/mcp/generate/motion": "summer_generate_motion",
16
+ "/api/mcp/generate/slice-asset-sheet": "summer_slice_asset_sheet",
17
+ "/api/mcp/workflows": "summer_get_studio_workflow",
16
18
  };
17
19
  // ---------------------------------------------------------------------------
18
20
  // Shared helpers
@@ -129,6 +131,37 @@ async function mcpGenerate(endpoint, body, timeoutMs = 120_000) {
129
131
  }
130
132
  return { data, status: res.status };
131
133
  }
134
+ async function mcpGet(endpoint, searchParams, timeoutMs = 30_000) {
135
+ const token = await getAuthToken();
136
+ const suffix = searchParams?.size ? `?${searchParams.toString()}` : "";
137
+ let res;
138
+ try {
139
+ res = await fetch(`${GATEWAY_URL}${endpoint}${suffix}`, {
140
+ headers: {
141
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
142
+ "X-Summer-Client": "summer-cli",
143
+ "X-Summer-Client-Version": CLI_VERSION,
144
+ "X-Summer-Client-Surface": "mcp",
145
+ "X-Summer-MCP-Endpoint": endpoint,
146
+ "X-Summer-MCP-Tool": TOOL_BY_ENDPOINT[endpoint] ?? "unknown",
147
+ },
148
+ signal: AbortSignal.timeout(timeoutMs),
149
+ });
150
+ }
151
+ catch (err) {
152
+ const msg = err instanceof Error ? err.message : String(err);
153
+ return { error: `Request failed: ${msg}`, status: 0 };
154
+ }
155
+ const data = await readJsonResponse(res);
156
+ if (!res.ok) {
157
+ return {
158
+ error: formattedApiErrorMessage(res.status, data),
159
+ data: { ...data, status: res.status },
160
+ status: res.status,
161
+ };
162
+ }
163
+ return { data, status: res.status };
164
+ }
132
165
  /**
133
166
  * Download an image URL to a temp file so the AI can Read it and show the user.
134
167
  */
@@ -153,7 +186,7 @@ async function downloadToTemp(url, prefix, ext = "png") {
153
186
  * Poll a job until completed, failed, or timeout.
154
187
  * Returns the final job status object.
155
188
  */
156
- async function pollJob(jobId, maxWaitMs = 300_000, intervalMs = 5_000) {
189
+ async function pollJob(jobId, maxWaitMs = 600_000, intervalMs = 5_000) {
157
190
  const token = await getAuthToken();
158
191
  if (!token)
159
192
  return { error: "Not signed in" };
@@ -210,6 +243,28 @@ function successResult(data) {
210
243
  // Registration
211
244
  // ---------------------------------------------------------------------------
212
245
  export function registerGenerateTools(server) {
246
+ // ========================================================================
247
+ // summer_get_studio_workflow
248
+ // ========================================================================
249
+ server.tool("summer_get_studio_workflow", `Discover the same Guided workflow recipes shown in Summer Engine Studio.
250
+
251
+ Call without workflowId to list every workflow and its support level. Call with
252
+ an id to get the exact starter prompt, ordered steps, required MCP tools, and
253
+ honest limitations. A "manual" support level means Studio still has a visual
254
+ handoff that MCP cannot perform automatically; do not pretend that stage ran.`, {
255
+ workflowId: z
256
+ .string()
257
+ .optional()
258
+ .describe("Guided workflow id, e.g. sprite-sheet, character-pack, asset-pack, or component-compiler"),
259
+ }, async ({ workflowId }) => {
260
+ const params = new URLSearchParams();
261
+ if (workflowId)
262
+ params.set("id", workflowId);
263
+ const result = await mcpGet("/api/mcp/workflows", params);
264
+ if (result.error)
265
+ return errorResult(result.error, result.data);
266
+ return successResult(result.data);
267
+ });
213
268
  // ========================================================================
214
269
  // summer_generate_image
215
270
  // ========================================================================
@@ -277,6 +332,35 @@ Requires authentication: run 'npx summer-engine login' first.`, {
277
332
  });
278
333
  });
279
334
  // ========================================================================
335
+ // summer_slice_asset_sheet
336
+ // ========================================================================
337
+ server.tool("summer_slice_asset_sheet", `Detect and crop every distinct game asset from an existing image asset sheet.
338
+
339
+ Use the assetId returned by summer_generate_image (or another Summer image asset).
340
+ The same guided workflow used in Studio removes the sheet background when needed,
341
+ detects named/category-aware bounding boxes, crops each item, and uploads the slices.
342
+
343
+ Returns source dimensions plus the generated slice URLs, names, categories, bounding
344
+ boxes, and widget metadata when the sheet contains UI controls.
345
+
346
+ Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
347
+ Requires authentication: run 'npx summer-engine login' first.`, {
348
+ assetId: z
349
+ .string()
350
+ .describe("Summer image asset ID to slice, usually returned by summer_generate_image"),
351
+ }, async ({ assetId }) => {
352
+ const result = await mcpGenerate("/api/mcp/generate/slice-asset-sheet", { assetId }, 300_000);
353
+ if (result.error) {
354
+ return errorResult(result.error, result.data);
355
+ }
356
+ return successResult({
357
+ ...result.data,
358
+ sliceCount: Array.isArray(result.data?.slices)
359
+ ? result.data.slices.length
360
+ : 0,
361
+ });
362
+ });
363
+ // ========================================================================
280
364
  // summer_generate_audio
281
365
  // ========================================================================
282
366
  server.tool("summer_generate_audio", `Generate audio using AI providers via Summer Engine Studio.
@@ -377,7 +461,16 @@ Example:
377
461
  })
378
462
  // Returns jobId; poll until result includes { assetId, rigAssetId }.
379
463
 
380
- By default, waits for completion (up to 5 min) and returns the result directly.
464
+ Single-image generation automatically assesses whether the source is suitable
465
+ for 3D. If needed, Summer prepares an isolated object view or rig-safe character
466
+ pose before conversion. Set referencePreparation="always" or "never" only when
467
+ you intentionally want to override that shared Studio behavior.
468
+
469
+ For a complete animated humanoid package, pass rig=true plus animationNames or
470
+ actionIds. This routes through the shared character pipeline instead of a
471
+ one-off mesh job.
472
+
473
+ By default, waits for completion (up to 10 min) and returns the result directly.
381
474
  Set wait=false to get the jobId immediately and poll manually with summer_check_job.
382
475
 
383
476
  Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
@@ -398,21 +491,66 @@ Requires authentication: run 'npx summer-engine login' first.`, {
398
491
  .string()
399
492
  .optional()
400
493
  .describe("Source image URL for image-to-3d or texture"),
494
+ imageUrls: z
495
+ .array(z.string())
496
+ .max(4)
497
+ .optional()
498
+ .describe("Up to four views for multi-image-to-3d"),
499
+ title: z.string().optional().describe("Asset or character title"),
500
+ idempotencyKey: z
501
+ .string()
502
+ .optional()
503
+ .describe("Stable retry key so the same logical request is not billed or queued twice"),
504
+ assetIntent: z
505
+ .enum(["character", "object"])
506
+ .optional()
507
+ .describe("Controls whether hidden reference preparation targets a rig-safe character pose or isolated object view"),
508
+ referencePreparation: z
509
+ .enum(["auto", "always", "never"])
510
+ .default("auto")
511
+ .describe("Shared Studio reference-preparation policy; auto is recommended"),
512
+ rig: z.boolean().default(false).describe("Auto-rig the generated model"),
513
+ animationNames: z
514
+ .array(z.string())
515
+ .optional()
516
+ .describe("Animation names for the shared animated-character pipeline, e.g. Idle, Walk, Run, Jump"),
517
+ actionIds: z
518
+ .array(z.number().int().min(0).max(696))
519
+ .optional()
520
+ .describe("Exact Meshy action ids for the shared animated-character pipeline"),
521
+ riggingHeightMeters: z
522
+ .number()
523
+ .positive()
524
+ .max(10)
525
+ .optional()
526
+ .describe("Character height used by auto-rigging"),
401
527
  wait: z
402
528
  .boolean()
403
529
  .default(true)
404
- .describe("Wait for completion (default true, up to 5 min). Set false to get jobId immediately."),
530
+ .describe("Wait for completion (default true, up to 10 min). Set false to get jobId immediately."),
405
531
  options: z
406
532
  .record(z.any())
407
533
  .optional()
408
534
  .describe("Provider-specific params (target_polycount, topology, art_style, etc.)"),
409
- }, async ({ prompt, kind, model, imageUrl, wait, options }) => {
535
+ }, async ({ prompt, kind, model, imageUrl, imageUrls, title, idempotencyKey, assetIntent, referencePreparation, rig, animationNames, actionIds, riggingHeightMeters, wait, options, }) => {
536
+ const mergedOptions = {
537
+ ...(options ?? {}),
538
+ ...(imageUrls ? { imageUrls } : {}),
539
+ ...(assetIntent ? { assetIntent } : {}),
540
+ referencePreparation,
541
+ ...(rig ? { rig: true } : {}),
542
+ ...(animationNames ? { animationNames } : {}),
543
+ ...(actionIds ? { actionIds } : {}),
544
+ ...(riggingHeightMeters !== undefined ? { riggingHeightMeters } : {}),
545
+ };
410
546
  const result = await mcpGenerate("/api/mcp/generate/3d", {
411
547
  prompt,
412
548
  kind,
413
549
  model,
414
550
  imageUrl,
415
- options,
551
+ title,
552
+ idempotencyKey,
553
+ options: mergedOptions,
416
554
  });
417
555
  if (result.error) {
418
556
  return errorResult(result.error, result.data);
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { ASSET_POLICIES, GAME_TASK_MODES, GAME_TASK_TARGETS, VERIFICATION_LEVELS, buildGameTaskPlan, } from "../../lib/game-task-plan.js";
3
+ import { getCachedBootDriftNotice } from "../../lib/mcp-boot-notice.js";
3
4
  import { getProjectMemorySummary } from "../../lib/project-memory.js";
4
5
  import { withEngine } from "./with-engine.js";
5
6
  function asRecord(value) {
@@ -134,18 +135,19 @@ anti-patterns, and recovery steps.`, {}, async () => ({
134
135
  {
135
136
  type: "text",
136
137
  text: JSON.stringify({
138
+ summerUpdateNotice: getCachedBootDriftNotice()?.text ?? null,
137
139
  startupChecklist: [
138
140
  "Understand the request and outline a brief plan before reaching for tools.",
139
- "Default medium is host file tools: write/edit .gd/.cs/.tscn/.tres/.json/docs/config directly as text.",
140
- "Use Summer MCP only when you need the LIVE engine: play/stop, diagnostics, screenshots/verification, navmesh or light bake, runtime inspect, or asset import.",
141
+ "Use summer_read_file, summer_write_file, and summer_replace_text for project text files so reads and guarded writes stay bound to the open engine project.",
142
+ "Use Summer MCP for project mutations and live-engine work: scenes, resources, play/stop, diagnostics, screenshots/verification, bake, runtime inspection, and asset import.",
141
143
  "Call summer_get_project_context first so you do not guess scene paths or the project language. It also BINDS this session to the currently-open project (see projectBinding below).",
142
144
  "Use projectMemory from summer_get_project_context to decide which .summer Markdown files to read before creative/audio/dialogue/level/character work.",
143
145
  "After writing code, VERIFY through the engine (see verificationLadder) instead of waiting for the user to navigate to it.",
144
146
  ],
145
147
  safeDefaults: [
146
148
  "Never guess scene filenames (main.tscn/Main.tscn) -- get them from summer_get_project_context.",
147
- "Edit files (including .tscn/.tres) with host file tools by default. Reach for MCP scene-ops only for live-engine needs (bake, play, runtime inspect) or when you want the editor to manage node ids / instancing.",
148
- "If you hand-edit a .tscn that is OPEN in the editor, reload it there afterward -- the editor's open tab can overwrite your file (clobber).",
149
+ "Use summer_replace_text for existing project text and summer_write_file with create_only:true for new files; overwrites require the sha256 from summer_read_file.",
150
+ "For live scene hierarchy and inspector changes, prefer scene tools. Guarded text writes support .tscn/.tres, and the engine schedules editor reloads after they land.",
149
151
  "Write GDScript by default; use C# only if the project already uses it.",
150
152
  "Never remove multiple top-level nodes unless the user explicitly requests destructive edits.",
151
153
  "Never change priority: locked .summer memory, voice IDs, canon, or provider bindings without explicit user confirmation.",
@@ -153,7 +155,7 @@ anti-patterns, and recovery steps.`, {}, async () => ({
153
155
  buildFlow: [
154
156
  "1. Understand what the user wants (one pass, no tool spelunking first).",
155
157
  "2. Outline the plan fast and briefly; proceed once it is clearly right.",
156
- "3. Execute in pure code -- edit .gd/.tscn/.tres as text (GDScript by default, C# only if the project already uses it).",
158
+ "3. Execute through identity-bound MCP mutations -- guarded text tools for files, scene tools for live hierarchy/inspector work (GDScript by default, C# only if the project already uses it).",
157
159
  "4. Verify through the engine using the verificationLadder below, fix, and repeat until it launches clean.",
158
160
  ],
159
161
  // The MCP-native verification ladder. Climb only as high as the
@@ -172,11 +174,10 @@ anti-patterns, and recovery steps.`, {}, async () => ({
172
174
  ],
173
175
  "3_runs": [
174
176
  "Compose the run-and-check yourself — there is no single 'verify' tool:",
175
- "summer_clear_console -> clean slate",
176
177
  "summer_play [scene] -> boot the game (or a specific scene)",
177
178
  "summer_get_debugger_errors -> runtime errors (null refs, missing nodes, physics)",
178
179
  "summer_screenshot target:'game' -> optional visual of the live frame",
179
- "summer_stop -> ALWAYS stop; scene mutations are refused while the game runs",
180
+ "summer_stop -> stop when runtime verification is finished; editor scene mutations are not categorically blocked by a running game, but an existing game instance may need a restart to observe them",
180
181
  ],
181
182
  "4_interactive": [
182
183
  "To prove input-driven behavior (does jump/move/shoot actually work), you have two routes when the engine build supports them (both go through summer_batch as raw ops — see rawOpsViaBatch):",
@@ -204,12 +205,12 @@ anti-patterns, and recovery steps.`, {}, async () => ({
204
205
  "To intentionally follow the switch, call summer_get_project_context again to rebind, then retry.",
205
206
  ],
206
207
  liveEngineFlow: [
207
- "Use this flow ONLY when you genuinely need live engine state (navmesh/light bake, instancing into an already-open scene, runtime inspect):",
208
+ "Use this flow when you genuinely need live engine state (navmesh/light bake, scene mutation, runtime inspect):",
208
209
  "summer_get_project_context",
209
- "summer_open_main_scene (if needed)",
210
- "summer_get_scene_tree",
211
- "summer_add_node / summer_set_prop / summer_set_resource_property",
212
- "summer_save_scene",
210
+ "Choose the exact res:// scenePath. OpenScene is a user-visible tab action, not a mutation prerequisite.",
211
+ "summer_get_scene_tree with scenePath when that exact scene is already loaded; omit it only for the visible tab.",
212
+ "summer_add_node / summer_set_prop / summer_set_resource_property with scenePath",
213
+ "Mutation tools append one final SaveScene; use summer_save_scene only for a standalone save/save-as.",
213
214
  "summer_get_diagnostics",
214
215
  ],
215
216
  // summer_batch forwards each {op, ...} verbatim to the engine, so
@@ -221,11 +222,12 @@ anti-patterns, and recovery steps.`, {}, async () => ({
221
222
  "These are runtime ops, not scene mutations — the batch undo group is a harmless no-op for them.",
222
223
  ],
223
224
  recovery: [
224
- "If you see 'no scene open': run summer_open_main_scene.",
225
+ "If a scene mutation reports missing_scene_path: pass the exact res:// scenePath and retry.",
226
+ "If a scene target cannot load: repair the named missing/invalid dependency, then retry the same scenePath.",
225
227
  "If open_scene fails: re-check mainScene from summer_get_project_context.",
226
- "If save fails: verify scene is open and game is not running (summer_is_running / summer_stop).",
228
+ "If save fails: use the returned scenePath/error to repair the exact cause. A running game alone is not a generic scene-mutation blocker.",
227
229
  "If a mutation is rejected with identity_mismatch: the engine switched projects — call summer_get_project_context to rebind (only if you meant to follow it), then retry.",
228
- "If a .tscn you wrote keeps reverting: the editor has that scene open -- reload or close the tab, then write again.",
230
+ "If a guarded file mutation is rejected with content mismatch: call summer_read_file again, review the new content, and retry with its new sha256 only if the edit is still valid.",
229
231
  "If a run op or SimulateInput returns 'unsupported' / an unknown-op error: this engine build predates it — fall back to summer_play + summer_get_debugger_errors, or ask the user to interact.",
230
232
  ],
231
233
  debugging: [
@@ -270,10 +272,11 @@ Use this first in every fresh chat to avoid guessing scene filenames or editing
270
272
  mainScene,
271
273
  boundProjectIdHash,
272
274
  projectMemory: getProjectMemorySummary(projectPath),
275
+ summerUpdateNotice: getCachedBootDriftNotice()?.text ?? null,
273
276
  guidance: mainScene
274
277
  ? "Use `summer_open_scene` with `mainScene` if no scene is open."
275
278
  : "Main scene not found in project state. Open a known scene path explicitly.",
276
- fileEditingGuidance: "Edit files (including .gd/.cs/.tscn/.tres/.json/docs) with host file tools by default. Use MCP only for live engine state: play/stop, diagnostics, screenshots/verification, navmesh or light bake, runtime inspect, and asset import. If you edit a .tscn that is open in the editor, reload it there afterward so the editor's tab does not overwrite your changes.",
279
+ fileEditingGuidance: "Use summer_read_file plus summer_replace_text or guarded summer_write_file for project files, including .gd/.cs/.tscn/.tres/.json/docs. New files require create_only:true; overwrites require the sha256 receipt from summer_read_file. Prefer scene tools for live hierarchy/inspector changes.",
277
280
  };
278
281
  }));
279
282
  server.tool("summer_open_main_scene", `Open the project's configured main scene from project settings.
@@ -315,10 +318,12 @@ Example: Bind jump to Space and W:
315
318
  ];
316
319
  return client.executeOps(ops);
317
320
  }));
318
- server.tool("summer_get_scene_tree", `Get the full scene tree structure of the currently open scene.
319
-
320
- Use this before structural edits (add/remove/replace).
321
- If you get "no edited scene", call summer_open_main_scene first.`, {}, async () => withEngine(async (client) => client.getSceneState()));
321
+ server.tool("summer_get_scene_tree", `Get a scene tree. Pass scenePath to read that exact in-memory/open scene;
322
+ omit it only when you intentionally want the currently visible editor scene.
323
+ Scene mutations load their explicit target, so a follow-up targeted read does
324
+ not require OpenScene.`, {
325
+ scenePath: z.string().optional().describe("Exact res:// scene path to inspect"),
326
+ }, async ({ scenePath }) => withEngine(async (client) => client.getSceneState(scenePath)));
322
327
  server.tool("summer_import_from_url", `Download a file from a URL and import it into the project. Triggers Godot's full import pipeline — generates .import files, extracts textures from .glb models, creates materials.
323
328
 
324
329
  Use this for:
@@ -2,6 +2,27 @@ import { z } from "zod";
2
2
  import { withEngine } from "./with-engine.js";
3
3
  import { readFile } from "fs/promises";
4
4
  import { join } from "path";
5
+ function sceneMutationOps(ops) {
6
+ const saveIndexes = ops
7
+ .map((op, index) => op.op === "SaveScene" ? index : -1)
8
+ .filter((index) => index >= 0);
9
+ if (saveIndexes.length > 1) {
10
+ throw new Error("A scene mutation batch may contain only one SaveScene");
11
+ }
12
+ if (saveIndexes.length === 1) {
13
+ if (saveIndexes[0] !== ops.length - 1) {
14
+ throw new Error("SaveScene must be the final operation in a scene mutation batch");
15
+ }
16
+ return ops;
17
+ }
18
+ return [...ops, { op: "SaveScene" }];
19
+ }
20
+ function executeSceneMutation(client, scenePath, ops, options) {
21
+ return client.executeIdentityBoundOps(sceneMutationOps(ops), {
22
+ ...(options ?? {}),
23
+ scenePath,
24
+ });
25
+ }
5
26
  async function readMainSceneFromProject(projectPath) {
6
27
  if (!projectPath)
7
28
  return null;
@@ -14,6 +35,20 @@ async function readMainSceneFromProject(projectPath) {
14
35
  return null;
15
36
  }
16
37
  }
38
+ function requireSuccessfulOps(result, context) {
39
+ const receipt = (result ?? {});
40
+ const results = Array.isArray(receipt.results)
41
+ ? receipt.results
42
+ : [];
43
+ const failed = results.find((entry) => entry?.ok === false);
44
+ if (receipt.status === "error" || failed) {
45
+ const error = (typeof failed?.error === "string" && failed.error) ||
46
+ (typeof receipt.error === "string" && receipt.error) ||
47
+ `${context} failed`;
48
+ throw new Error(error);
49
+ }
50
+ return receipt;
51
+ }
17
52
  export function registerSceneTools(server) {
18
53
  server.tool("summer_create_scene", `Create a new empty scene file safely.
19
54
 
@@ -45,19 +80,22 @@ Recommended workflow:
45
80
  throw new Error("No scene open and could not resolve main scene. Call summer_get_project_context first, then open a known scene.");
46
81
  }
47
82
  await client.executeOps([{ op: "OpenScene", path: templateScene }]);
48
- const tree = (await client.getSceneState());
83
+ const tree = (await client.getSceneState(templateScene));
49
84
  const children = tree.data?.children ?? tree.children ?? [];
50
85
  const removeOps = children
51
86
  .map((c) => c.path)
52
87
  .filter((p) => typeof p === "string" && p.length > 0)
53
88
  .map((p) => ({ op: "RemoveNode", path: `./${p}` }));
54
- if (removeOps.length > 0) {
55
- await client.executeOps(removeOps, { groupUndo: true });
56
- }
57
- await client.executeOps([{ op: "SetProp", path: ".", key: "name", value: rootName }]);
58
- await client.executeOps([{ op: "SaveScene", path }]);
89
+ const createOps = [
90
+ ...removeOps,
91
+ { op: "SetProp", path: ".", key: "name", value: rootName },
92
+ { op: "SaveScene", path },
93
+ ];
94
+ const createReceipt = requireSuccessfulOps(await executeSceneMutation(client, templateScene, createOps, {
95
+ groupUndo: true,
96
+ }), `Creating ${path}`);
59
97
  if (currentScene && currentScene !== path) {
60
- await client.executeOps([{ op: "OpenScene", path: currentScene }]);
98
+ requireSuccessfulOps(await client.executeOps([{ op: "OpenScene", path: currentScene }]), `Restoring ${currentScene}`);
61
99
  }
62
100
  return {
63
101
  ok: true,
@@ -65,14 +103,13 @@ Recommended workflow:
65
103
  rootName,
66
104
  templateScene,
67
105
  restoredScene: currentScene,
106
+ receipt: createReceipt,
68
107
  };
69
108
  }));
70
109
  server.tool("summer_add_node", `Add a new node to the scene tree.
71
110
 
72
- Preflight (fresh chat):
73
- 1) summer_get_project_context
74
- 2) summer_open_main_scene (if no scene open)
75
- 3) summer_get_scene_tree
111
+ Pass the exact res:// scenePath to mutate. The scene does not need to be the
112
+ active editor tab.
76
113
 
77
114
  Common node types:
78
115
  - 3D: Node3D, MeshInstance3D, CharacterBody3D, RigidBody3D, StaticBody3D, Camera3D, DirectionalLight3D, OmniLight3D, SpotLight3D, WorldEnvironment, CollisionShape3D, Area3D
@@ -81,10 +118,11 @@ Common node types:
81
118
  - Audio: AudioStreamPlayer, AudioStreamPlayer3D
82
119
 
83
120
  The parent path uses "./" prefix for relative paths from scene root. E.g., "./World" means the "World" child of the root node.`, {
121
+ scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
84
122
  parent: z.string().describe("Parent node path, e.g. './World' or './World/Enemies'"),
85
123
  type: z.string().describe("Godot node type, e.g. 'MeshInstance3D', 'CharacterBody3D'"),
86
124
  name: z.string().describe("Name for the new node, e.g. 'Player', 'MainCamera'"),
87
- }, async ({ parent, type, name }) => withEngine(async (client) => client.executeOps([{ op: "AddNode", parent, type, name }])));
125
+ }, async ({ scenePath, parent, type, name }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "AddNode", parent, type, name }])));
88
126
  server.tool("summer_set_prop", `Set a property on a node. This is the primary way to configure nodes after adding them.
89
127
 
90
128
  VALUE FORMAT — Godot string syntax for complex types:
@@ -106,29 +144,37 @@ COMMON PROPERTIES:
106
144
  - shadow_enabled: true — for lights
107
145
  - light_energy: 1.5 — light intensity
108
146
  - fov: 75.0 — camera field of view`, {
147
+ scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
109
148
  path: z.string().describe("Node path, e.g. './World/Player'"),
110
149
  key: z.string().describe("Property name, e.g. 'position', 'mesh', 'visible'"),
111
150
  value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format for complex types, native JSON for primitives"),
112
- }, async ({ path, key, value }) => withEngine(async (client) => client.executeOps([{ op: "SetProp", path, key, value }])));
151
+ }, async ({ scenePath, path, key, value }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "SetProp", path, key, value }])));
113
152
  server.tool("summer_set_resource_property", `Set a nested property on a resource attached to a node.
114
153
 
115
154
  Use when you need to modify a sub-property of a resource, like:
116
155
  - CollisionShape3D shape size: nodePath="./Player/CollisionShape3D", resourceProperty="shape", subProperty="size", value="Vector3(1, 2, 1)"
117
156
  - Material albedo color: nodePath="./Floor", resourceProperty="material_override", subProperty="albedo_color", value="Color(0.2, 0.5, 0.2, 1)"
118
157
  - Mesh size: nodePath="./Box", resourceProperty="mesh", subProperty="size", value="Vector3(2, 2, 2)"`, {
158
+ scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
119
159
  nodePath: z.string().describe("Node path, e.g. './Player/CollisionShape3D'"),
120
160
  resourceProperty: z.string().describe("Resource property on the node, e.g. 'shape', 'mesh', 'material_override'"),
121
161
  subProperty: z.string().describe("Property on the resource, e.g. 'size', 'radius', 'albedo_color'"),
122
162
  value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format"),
123
- }, async ({ nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => client.executeOps([
163
+ }, async ({ scenePath, nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [
124
164
  { op: "SetResourceProperty", nodePath, resourceProperty, subProperty, value },
125
165
  ])));
126
- server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.", { path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'") }, async ({ path }) => withEngine(async (client) => client.executeOps([{ op: "RemoveNode", path }])));
127
- server.tool("summer_save_scene", "Save the current scene to disk. Call this after making scene changes to persist them. Without saving, changes only exist in the editor's memory.", { path: z.string().optional().describe("Save-as path for creating a new scene file, e.g. 'res://levels/level2.tscn'") }, async ({ path }) => withEngine(async (client) => {
166
+ server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.", {
167
+ scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
168
+ path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'"),
169
+ }, async ({ scenePath, path }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "RemoveNode", path }])));
170
+ server.tool("summer_save_scene", "Save an explicit scene to disk. Mutation tools already append one save; use this for a standalone save or save-as.", {
171
+ scenePath: z.string().describe("Scene to save, e.g. 'res://main.tscn'"),
172
+ path: z.string().optional().describe("Optional save-as path, e.g. 'res://levels/level2.tscn'"),
173
+ }, async ({ scenePath, path }) => withEngine(async (client) => {
128
174
  const op = { op: "SaveScene" };
129
175
  if (path)
130
176
  op.path = path;
131
- return client.executeOps([op]);
177
+ return executeSceneMutation(client, scenePath, [op]);
132
178
  }));
133
179
  server.tool("summer_open_scene", `Open a scene file in the editor. Use this to switch between scenes.
134
180
 
@@ -142,14 +188,15 @@ Do not guess paths. Prefer:
142
188
  - Compose scenes from smaller scenes (e.g., add a "Player" scene into a "Level" scene)
143
189
 
144
190
  The scene must already exist in the project. Use summer_import_from_url first if importing from external sources.`, {
191
+ scenePath: z.string().describe("Target scene to receive the instance, e.g. 'res://main.tscn'"),
145
192
  parent: z.string().describe("Parent node path, e.g. './World'"),
146
193
  scene: z.string().describe("Scene/model path, e.g. 'res://player.tscn' or 'res://models/tree.glb'"),
147
194
  name: z.string().optional().describe("Override the instance name"),
148
- }, async ({ parent, scene, name }) => withEngine(async (client) => {
195
+ }, async ({ scenePath, parent, scene, name }) => withEngine(async (client) => {
149
196
  const op = { op: "InstantiateScene", parent, scene };
150
197
  if (name)
151
198
  op.name = name;
152
- return client.executeOps([op]);
199
+ return executeSceneMutation(client, scenePath, [op]);
153
200
  }));
154
201
  server.tool("summer_connect_signal", `Connect a signal between two nodes. Signals are Godot's event system — they notify when something happens.
155
202
 
@@ -161,11 +208,12 @@ Common signals:
161
208
  - "input_event" — CollisionObject received input
162
209
 
163
210
  The receiver node must have a script with the specified method.`, {
211
+ scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
164
212
  emitter: z.string().describe("Node that fires the signal, e.g. './Player/HitArea'"),
165
213
  signal: z.string().describe("Signal name, e.g. 'body_entered'"),
166
214
  receiver: z.string().describe("Node with the handler script, e.g. './Player'"),
167
215
  method: z.string().describe("Method name in the receiver's script, e.g. '_on_hit_area_body_entered'"),
168
- }, async ({ emitter, signal, receiver, method }) => withEngine(async (client) => client.executeOps([
216
+ }, async ({ scenePath, emitter, signal, receiver, method }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [
169
217
  { op: "ConnectSignal", emitter, signal, receiver, method },
170
218
  ])));
171
219
  server.tool("summer_select_node", "Select a node in the editor's scene tree and show it in the inspector panel. Useful for focusing the editor on a specific node.", {
@@ -178,16 +226,17 @@ The receiver node must have a script with the specified method.`, {
178
226
  return client.executeOps([op]);
179
227
  }));
180
228
  server.tool("summer_replace_node", "Replace a node with a different type or scene, preserving its position in the tree and its children. Useful for changing a StaticBody3D to a RigidBody3D, or swapping a placeholder with a proper prefab.", {
229
+ scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
181
230
  path: z.string().describe("Node path to replace"),
182
231
  type: z.string().optional().describe("New node type, e.g. 'RigidBody3D'"),
183
232
  scene: z.string().optional().describe("Scene to replace with, e.g. 'res://enemies/boss.tscn'"),
184
- }, async ({ path, type, scene }) => withEngine(async (client) => {
233
+ }, async ({ scenePath, path, type, scene }) => withEngine(async (client) => {
185
234
  const op = { op: "ReplaceNode", path };
186
235
  if (type)
187
236
  op.type = type;
188
237
  if (scene)
189
238
  op.scene = scene;
190
- return client.executeOps([op]);
239
+ return executeSceneMutation(client, scenePath, [op]);
191
240
  }));
192
241
  server.tool("summer_inspect_node", `Get all editable properties of a node with their current values, types, and resource info.
193
242
 
@@ -213,7 +262,35 @@ Each op in the array uses the same format as the individual tools:
213
262
 
214
263
  RAW RUNTIME OPS (interactive verification — engine-build dependent; structured failure_reason incl "unsupported" passes through verbatim):
215
264
  - SimulateInput — drive the RUNNING game (summer_play first): {"op": "SimulateInput", "type": "action", "action": "jump", "pressed": true}. type is "action" | "key" | "mouse_click" | "axis".
216
- - RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "extends SummerProbeBase\\nfunc _ready(): await super._ready(); report('ok', true); finish()", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report()/save_frame()/press()/key()/finish().`, {
265
+ - RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "extends SummerProbeBase\\nfunc _ready(): await super._ready(); report('ok', true); finish()", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report()/save_frame()/press()/key()/finish().
266
+
267
+ Do not mix OpenScene with scene mutations in one batch. OpenScene is a UI action;
268
+ send it separately. scenePath selects every mutation target. The tool appends one
269
+ final SaveScene when the batch mutates a scene; if supplied explicitly, SaveScene
270
+ must appear exactly once and be the final operation.`, {
271
+ scenePath: z.string().optional().describe("Required when ops contains scene mutations; exact res:// target scene path"),
217
272
  ops: z.array(z.record(z.unknown())).describe("Array of operation objects, each with 'op' plus its parameters"),
218
- }, async ({ ops }) => withEngine(async (client) => client.executeOps(ops, { groupUndo: true })));
273
+ }, async ({ scenePath, ops }) => withEngine(async (client) => {
274
+ const rawFileMutation = ops.find((op) => {
275
+ const kind = String(op.op ?? "");
276
+ return kind === "WriteFile" || kind === "ReplaceText";
277
+ });
278
+ if (rawFileMutation) {
279
+ throw new Error(`summer_batch does not accept raw ${String(rawFileMutation.op)} operations. ` +
280
+ "Use summer_write_file or summer_replace_text so project identity, content guards, and same-file ordering are enforced.");
281
+ }
282
+ const sceneMutations = new Set([
283
+ "AddNode", "RemoveNode", "MoveNode", "ReparentNode", "ReplaceNode",
284
+ "SetProp", "SetResourceProperty", "ConnectSignal", "DisconnectSignal",
285
+ "InstantiateScene", "SaveScene", "Undo",
286
+ ]);
287
+ const needsScenePath = ops.some((op) => sceneMutations.has(String(op.op ?? "")));
288
+ if (needsScenePath && !scenePath) {
289
+ throw new Error("summer_batch requires scenePath when ops contains scene mutations");
290
+ }
291
+ const options = { groupUndo: true, ...(scenePath ? { scenePath } : {}) };
292
+ return needsScenePath
293
+ ? executeSceneMutation(client, scenePath, ops, options)
294
+ : client.executeOps(ops, options);
295
+ }));
219
296
  }
@@ -19,7 +19,10 @@ const SUCCESS_TERMINAL_STATES = new Set(["applied", "no_op"]);
19
19
  // `error` string (queue-full / lease-reject / identity-mismatch / no-progress
20
20
  // timeout frequently arrive with terminalState set and results[] absent).
21
21
  const TERMINAL_STATE_MESSAGES = {
22
- timed_out: "Engine operation timed out (terminalState: timed_out). Nothing was applied.",
22
+ timed_out: "Engine operation timed out. Its final state is unknown; inspect the target before retrying.",
23
+ still_queued: "Summer Engine accepted the operation, but it was still queued when the client stopped waiting. It may still run; do not retry blindly.",
24
+ still_running: "Summer Engine accepted and started the operation, but no final receipt arrived. It may still be running or may already have applied; inspect the target before retrying.",
25
+ uncertain: "Summer Engine did not provide a final operation receipt. The current state is uncertain; inspect the target before retrying.",
23
26
  not_connected: "Summer Engine is not connected (terminalState: not_connected). Nothing was applied.",
24
27
  identity_mismatch: "Operation rejected — wrong project/instance (terminalState: identity_mismatch). Nothing was mutated.",
25
28
  content_mismatch: "Operation rejected — content changed since last read (terminalState: content_mismatch). Nothing was applied.",
@@ -117,9 +120,6 @@ function buildActionHint(message) {
117
120
  if (normalized.includes("failed to open scene")) {
118
121
  return "Scene path could not be opened. Call `summer_get_project_context` to get `mainScene`, then open that exact path. Avoid guessing scene filenames.";
119
122
  }
120
- if (normalized.includes("writefile cannot edit .tscn/.scn")) {
121
- return "Write .gd/.cs/.json/docs/simple config files with normal file-edit tools. Use Summer MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, and visual verification.";
122
- }
123
123
  return null;
124
124
  }
125
125
  export async function withEngine(fn, opts) {
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "summer-engine",
3
- "version": "2.6.5",
4
- "description": "Summer CLI and MCP server for Summer Engine, the AI game engine. Install the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games. Skills, hooks, and plugins included.",
3
+ "version": "2.7.0",
4
+ "description": "Local Summer CLI and MCP server for Summer Engine. Install and run the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games with bundled skills, hooks, and plugins.",
5
5
  "keywords": [
6
6
  "summer-engine",
7
7
  "summer-cli",
@@ -27,6 +27,10 @@
27
27
  "type": "git",
28
28
  "url": "git+https://github.com/SummerEngine/summer-engine-agent.git"
29
29
  },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org"
33
+ },
30
34
  "license": "MIT",
31
35
  "author": "Summer Engine <founders@summerengine.com>",
32
36
  "type": "module",