summer-engine 2.8.0 → 2.8.1
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/CHANGELOG.md +19 -1
- package/README.md +1 -3
- package/dist/commands/login.js +14 -2
- package/dist/lib/api-client.d.ts +23 -6
- package/dist/lib/api-client.js +37 -10
- package/dist/mcp/tools/debug-tools.d.ts +12 -0
- package/dist/mcp/tools/debug-tools.js +100 -1
- package/dist/mcp/tools/project-tools.js +125 -15
- package/dist/mcp/tools/scene-tools.js +162 -62
- package/dist/mcp/tools/visual-tools.js +29 -6
- package/dist/mcp/tools/with-engine.js +51 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to summer-engine will be documented here. Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [2.8.1] — 2026-08-18 — "Scene mutations work again on engine 0.5.60+"
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `summer_get_scene_tree` accepts optional `depth` and `limit` params (engine defaults: depth 2, limit 200 — a 102-node scene silently truncated to 61 nodes at the defaults). The engine only honors them on scene-targeted reads, so the tool resolves the current scene path first when needed and says so when it can't. `summer_get_project_context` accepts an optional `settingsPrefix` and, without one, trims `project.data.entries` to a curated prefix set (application/, display/, input/, physics/, rendering/) instead of returning the full ~188KB settings dump — the payload declares the trim via `settingsTruncated`/`totalSettings`/`settingsHint`.
|
|
9
|
+
- `summer_screenshot` scene captures accept `nodePath` (frame a specific node, honest `node_not_found` failure) and new framing directions `back`/`left`/`right`; captions report the resolved framing and any render retries. Requires engine 0.5.62+ to take effect — older engines silently ignore the new fields.
|
|
10
|
+
- `summer_get_diagnostics` returns a prioritized bounded view (errors first, then warnings, capped info tail) with honest suppression counters, plus `includeAll: true` for the untrimmed payload. Severity + recency + caps only — no noise-pattern matching.
|
|
11
|
+
- `scripts/compat-smoke.sh`: a latest-MCP × candidate-engine release gate that drives the real built MCP server against a live engine and fails loudly on batch-contract incompatibilities (the class of bug that broke 2.7.0–2.8.0 × engine 0.5.60+). Run it before every engine release and every npm publish.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- `summer_create_scene` no longer uses the destructive temporary-template strategy (open current scene → delete its children → save-as → restore). It now writes a minimal `.tscn` through the identity-bound engine `WriteFile` with a create-only guard, never touches the open scene, verifies by reading the file back, and gained a `rootType` param (Node3D/Node2D/Control). `allow_temporary_scene_mutation` remains accepted as a deprecated no-op.
|
|
15
|
+
- **Scene mutations were completely broken against engine 0.5.60/0.5.61.** The engine now requires `SaveScene`, `InstantiateScene`, `ReplaceNode`, `SimulateInput`, and the `Run*`/`Import*`/`Git*` ops to travel as their own single-op request, and rejects any multi-op batch containing one of them wholesale (`failure_reason: "unsupported_transport"`). Since 2.7.0 appended `SaveScene` to every mutation batch, every `summer_add_node`/`summer_set_prop`/`summer_batch`/`summer_create_scene` call was rejected before anything executed. Mutation batches are now automatically split into sequential requests around single-only ops; all receipts are preserved and merged, and a mutation that applied followed by a save that failed is reported honestly (including which ops already applied and which were not sent).
|
|
16
|
+
- Engine failures no longer hide the precise rejection. `extractOpError` previously returned a generic `"Engine operation failed (terminalState: failed)"` without inspecting `results[]`; it now surfaces the failed op's own error and `failure_reason` (envelope or per-op, either spelling), rendered as JSON when a classifier is present so agents can key on `failure_reason` reliably.
|
|
17
|
+
- `save_frame` is documented with its required `name` argument everywhere (`save_frame()` with no args is a probe script error), plus the deferred scene-mount pattern that avoids black frames.
|
|
18
|
+
- SimulateInput guidance corrected: it IS reachable over MCP/CLI as a single op against the running game (`failure_reason: "not_running"`/`"unsupported"` are the real failure modes); `"unsupported_transport"` only means it was batched with other ops. The previous claim that it needs the in-editor bridge on every build was stale.
|
|
19
|
+
- The scene-preview synthetic-camera note no longer claims the scene has no camera — the engine always synthesizes the preview camera; `sceneHasCamera` is the authoritative signal and keeps its own warning.
|
|
20
|
+
- `summer login` waits 15 minutes on one session id (was 2) with periodic reminders, covering first-time account creation + email confirmation. The gateway never expires a pending session, so the single id stays valid the whole window.
|
|
21
|
+
- Removed the stale "Engine mirror only" banner from the README (it shipped to npm and the public repo, and its claims were wrong).
|
|
22
|
+
|
|
23
|
+
## [2.8.0] — 2026-08-17 — "Multi-editor MCP routing"
|
|
6
24
|
|
|
7
25
|
### Added
|
|
8
26
|
- MCP discovers every live Summer editor through `~/.summer/instances/` and automatically binds local tools to the editor whose project contains the agent's current working directory.
|
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Summer Engine: the AI game engine
|
|
2
2
|
|
|
3
|
-
> **Engine mirror only.** This directory is intentionally marked `private` and must never be published to npm. Review and reconcile changes into [SummerEngine/summer-engine-agent](https://github.com/SummerEngine/summer-engine-agent), which is the only npm release source.
|
|
4
|
-
|
|
5
3
|
Build real 2D and 3D Summer games with Summer Engine, the Summer SDK, and
|
|
6
4
|
GDScript. The agent layer can help author code and scenes, but creators remain
|
|
7
5
|
in control of changes and release decisions. Local export support depends on
|
|
@@ -324,7 +322,7 @@ summer_batch ops:[{op:"RunVerification", probe_source:"…", max_seconds:20}]
|
|
|
324
322
|
→ {ok, results, frames, out_dir}
|
|
325
323
|
```
|
|
326
324
|
|
|
327
|
-
Unlike `--headless`, that instance has a real renderer, so `save_frame()` writes an actual image. Probe API: `report()` / `save_frame()` / `dump_tree()` / `press()` / `key()` / `finish()`. **`press()` and `key()` are coroutines — `await` them.** Every project scaffolded by `summer create` ships a working example at `tests/autopilot/`.
|
|
325
|
+
Unlike `--headless`, that instance has a real renderer, so `save_frame("name")` writes an actual image (the name argument is required). Probe API: `report(name, value)` / `save_frame(name)` / `dump_tree()` / `press(action)` / `key(keycode)` / `finish()`. **`press()` and `key()` are coroutines — `await` them.** Every project scaffolded by `summer create` ships a working example at `tests/autopilot/`.
|
|
328
326
|
|
|
329
327
|
`SimulateInput` is **not** reachable from MCP or the CLI, on any build. It needs the in-editor chat bridge's async reply channel; every queued caller is answered `{ok:false, failure_reason:"unsupported_transport"}`. Use the probe.
|
|
330
328
|
|
package/dist/commands/login.js
CHANGED
|
@@ -4,7 +4,13 @@ import open from "open";
|
|
|
4
4
|
import { getAuthToken, getCreatorToken, saveCreatorToken, saveLoginSession, } from "../lib/auth.js";
|
|
5
5
|
import { getCreatorApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
6
6
|
const POLL_INTERVAL_MS = 2000;
|
|
7
|
-
|
|
7
|
+
// One generous window on ONE session id. First-time users may need to create an
|
|
8
|
+
// account and confirm an email before they can approve the CLI. The gateway
|
|
9
|
+
// never expires a pending session (the completed payload waits under this id
|
|
10
|
+
// for pickup), so the worst mistake would be rotating ids mid-wait — the user
|
|
11
|
+
// would approve the original browser tab while we poll a different session.
|
|
12
|
+
const POLL_TIMEOUT_MS = 900000;
|
|
13
|
+
const HEARTBEAT_MS = 30000;
|
|
8
14
|
export const loginCommand = new Command("login")
|
|
9
15
|
.description("Sign in to Summer Engine via your browser")
|
|
10
16
|
.option("--creator", "Connect a separately scoped Summercraft creator token for publishing")
|
|
@@ -121,9 +127,15 @@ export async function runLogin(overrides = {}) {
|
|
|
121
127
|
deps.log("Waiting for authentication...");
|
|
122
128
|
const pollUrl = `${gatewayUrl}/api/auth/cli-login?session=${encodeURIComponent(sessionId)}`;
|
|
123
129
|
const startTime = deps.now();
|
|
130
|
+
let lastHeartbeat = startTime;
|
|
124
131
|
let lastError = null;
|
|
125
132
|
while (deps.now() - startTime < POLL_TIMEOUT_MS) {
|
|
126
133
|
await deps.sleep(POLL_INTERVAL_MS);
|
|
134
|
+
if (deps.now() - lastHeartbeat >= HEARTBEAT_MS) {
|
|
135
|
+
lastHeartbeat = deps.now();
|
|
136
|
+
deps.log('Still waiting — finish signing in (or creating your account) in the browser, then click "Yes, Sign In". Same link: ' +
|
|
137
|
+
loginUrl);
|
|
138
|
+
}
|
|
127
139
|
try {
|
|
128
140
|
const res = await deps.fetch(pollUrl, {
|
|
129
141
|
signal: AbortSignal.timeout(5000),
|
|
@@ -161,5 +173,5 @@ export async function runLogin(overrides = {}) {
|
|
|
161
173
|
lastError = err instanceof Error ? err.message : "Network error";
|
|
162
174
|
}
|
|
163
175
|
}
|
|
164
|
-
throw new Error(`Login timed out after
|
|
176
|
+
throw new Error(`Login timed out after ${Math.round(POLL_TIMEOUT_MS / 60000)} minutes.${lastError ? ` Last error: ${lastError}` : ""} Recovery: run "summer login" again and complete "Yes, Sign In" in the browser tab it opens.`);
|
|
165
177
|
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -29,6 +29,15 @@ export type EngineSnapshot = {
|
|
|
29
29
|
sceneHasCamera?: boolean;
|
|
30
30
|
sceneHadLight?: boolean;
|
|
31
31
|
usedSyntheticCamera?: boolean;
|
|
32
|
+
/** Scene-preview only: the framing the engine actually rendered with
|
|
33
|
+
* ("auto" resolves to "iso"). */
|
|
34
|
+
framing?: string;
|
|
35
|
+
/** Scene-preview only: path (relative to the scene root) of the node the
|
|
36
|
+
* camera framed when nodePath was given. */
|
|
37
|
+
framedNode?: string;
|
|
38
|
+
/** Scene-preview only: how many blank-readback retries the engine needed
|
|
39
|
+
* before the capture produced a non-blank frame (0 = clean first read). */
|
|
40
|
+
renderRetries?: number;
|
|
32
41
|
/** Set true when the bound projectIdHash no longer matches the engine's live
|
|
33
42
|
* health hash at capture time — the frame may be from the WRONG project
|
|
34
43
|
* (item 4, client-side drift check). */
|
|
@@ -86,8 +95,11 @@ export declare class EngineApiClient {
|
|
|
86
95
|
*/
|
|
87
96
|
executeIdentityBoundOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
|
|
88
97
|
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
89
|
-
getSceneState(scenePath?: string
|
|
90
|
-
|
|
98
|
+
getSceneState(scenePath?: string, options?: {
|
|
99
|
+
depth?: number;
|
|
100
|
+
limit?: number;
|
|
101
|
+
}): Promise<unknown>;
|
|
102
|
+
getProjectState(prefix?: string): Promise<unknown>;
|
|
91
103
|
getDiagnostics(): Promise<unknown>;
|
|
92
104
|
inspectNode(path: string): Promise<unknown>;
|
|
93
105
|
inspectResource(path: string): Promise<unknown>;
|
|
@@ -117,13 +129,18 @@ export declare class EngineApiClient {
|
|
|
117
129
|
* Offscreen scene render via the `ScenePreview` op over /api/ops (no game
|
|
118
130
|
* boot; physics/animations are static). Mirrors the web previewScene op input
|
|
119
131
|
* (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
132
|
+
* node? } — scene_path optional, engine defaults to the open scene. `node` is
|
|
133
|
+
* the canonical focus-node arg (the engine also accepts the legacy web wire
|
|
134
|
+
* name `node_path`); an unresolvable node fails with failure_reason
|
|
135
|
+
* "node_not_found". framing "auto" is an alias of "iso"; the engine also
|
|
136
|
+
* accepts "top"/"front"/"back"/"left"/"right" and echoes the resolved framing.
|
|
137
|
+
* The result carries image_base64 + mime (+ width/height) plus the P4.3
|
|
138
|
+
* confession fields (scene_has_camera / scene_had_light /
|
|
139
|
+
* used_synthetic_camera) and framing / framed_node / render_retries.
|
|
123
140
|
*/
|
|
124
141
|
scenePreview(input?: {
|
|
125
142
|
scenePath?: string;
|
|
126
|
-
framing?: "auto" | "top" | "front" | "
|
|
143
|
+
framing?: "auto" | "iso" | "top" | "front" | "back" | "left" | "right";
|
|
127
144
|
size?: [number, number];
|
|
128
145
|
nodePath?: string;
|
|
129
146
|
}): Promise<EngineSnapshot>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -330,14 +330,33 @@ export class EngineApiClient {
|
|
|
330
330
|
});
|
|
331
331
|
return this.request("GET", `/api/state/read-file?${query.toString()}`);
|
|
332
332
|
}
|
|
333
|
-
async getSceneState(scenePath) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
async getSceneState(scenePath, options) {
|
|
334
|
+
// depth/limit only take effect on targeted (scene=) reads: the engine
|
|
335
|
+
// routes those through the live command queue and forwards every query
|
|
336
|
+
// param into StateProvider::scene_state (tool_net_thread.cpp
|
|
337
|
+
// _parse_state_args). An UNtargeted read is answered from a pre-published
|
|
338
|
+
// snapshot built with the defaults (depth 2, limit 200) and its query
|
|
339
|
+
// params are ignored — callers who need depth/limit must pass scenePath
|
|
340
|
+
// (summer_get_scene_tree resolves the current scene path for this).
|
|
341
|
+
const parts = [];
|
|
342
|
+
if (scenePath)
|
|
343
|
+
parts.push(`scene=${encodeURIComponent(scenePath)}`);
|
|
344
|
+
if (options?.depth !== undefined)
|
|
345
|
+
parts.push(`depth=${options.depth}`);
|
|
346
|
+
if (options?.limit !== undefined)
|
|
347
|
+
parts.push(`limit=${options.limit}`);
|
|
348
|
+
const query = parts.length ? `?${parts.join("&")}` : "";
|
|
337
349
|
return this.request("GET", `/api/state/scene${query}`);
|
|
338
350
|
}
|
|
339
|
-
async getProjectState() {
|
|
340
|
-
|
|
351
|
+
async getProjectState(prefix) {
|
|
352
|
+
// ?prefix= is sent for forward-compatibility, but current engine builds
|
|
353
|
+
// IGNORE it: /api/state/project is always served from the snapshot, which
|
|
354
|
+
// is published with empty args (local_api_server.cpp
|
|
355
|
+
// _maybe_publish_snapshot), so StateProvider::project_state never sees the
|
|
356
|
+
// query. Callers that need a prefix subset must also filter client-side
|
|
357
|
+
// (summer_get_project_context does).
|
|
358
|
+
const query = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
|
|
359
|
+
return this.request("GET", `/api/state/project${query}`);
|
|
341
360
|
}
|
|
342
361
|
async getDiagnostics() {
|
|
343
362
|
return this.request("GET", "/api/state/diagnostics");
|
|
@@ -475,6 +494,9 @@ export class EngineApiClient {
|
|
|
475
494
|
sceneHasCamera: boolFrom(payload.scene_has_camera),
|
|
476
495
|
sceneHadLight: boolFrom(payload.scene_had_light),
|
|
477
496
|
usedSyntheticCamera: boolFrom(payload.used_synthetic_camera),
|
|
497
|
+
framing: stringFrom(payload.framing),
|
|
498
|
+
framedNode: stringFrom(payload.framed_node),
|
|
499
|
+
renderRetries: numberFrom(payload.render_retries),
|
|
478
500
|
projectMismatch,
|
|
479
501
|
metadata: withoutImageData(payload),
|
|
480
502
|
};
|
|
@@ -525,9 +547,14 @@ export class EngineApiClient {
|
|
|
525
547
|
* Offscreen scene render via the `ScenePreview` op over /api/ops (no game
|
|
526
548
|
* boot; physics/animations are static). Mirrors the web previewScene op input
|
|
527
549
|
* (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
550
|
+
* node? } — scene_path optional, engine defaults to the open scene. `node` is
|
|
551
|
+
* the canonical focus-node arg (the engine also accepts the legacy web wire
|
|
552
|
+
* name `node_path`); an unresolvable node fails with failure_reason
|
|
553
|
+
* "node_not_found". framing "auto" is an alias of "iso"; the engine also
|
|
554
|
+
* accepts "top"/"front"/"back"/"left"/"right" and echoes the resolved framing.
|
|
555
|
+
* The result carries image_base64 + mime (+ width/height) plus the P4.3
|
|
556
|
+
* confession fields (scene_has_camera / scene_had_light /
|
|
557
|
+
* used_synthetic_camera) and framing / framed_node / render_retries.
|
|
531
558
|
*/
|
|
532
559
|
async scenePreview(input) {
|
|
533
560
|
const opInput = { op: "ScenePreview" };
|
|
@@ -539,7 +566,7 @@ export class EngineApiClient {
|
|
|
539
566
|
if (input?.size)
|
|
540
567
|
opInput.size = input.size;
|
|
541
568
|
if (input?.nodePath)
|
|
542
|
-
opInput.
|
|
569
|
+
opInput.node = input.nodePath;
|
|
543
570
|
let response;
|
|
544
571
|
try {
|
|
545
572
|
// executeOps stamps the bound identity so a drifted project is rejected
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Prioritized, bounded view of the engine diagnostics payload:
|
|
4
|
+
* - console.messages reordered: errors first, then warnings, then a capped tail
|
|
5
|
+
* of info/std/editor noise (engine order is newest-first; that order is
|
|
6
|
+
* preserved within each severity bucket, so the kept tail is the most recent).
|
|
7
|
+
* - debugger.errors_data untouched; debugger.warnings_data capped to the most
|
|
8
|
+
* recent DEBUGGER_WARNING_CAP entries (engine emits newest-first).
|
|
9
|
+
* - a `_view` block with honest counters and a hint pointing at includeAll.
|
|
10
|
+
* Shape-tolerant: anything unexpected passes through unchanged.
|
|
11
|
+
* Exported for unit tests.
|
|
12
|
+
*/
|
|
13
|
+
export declare function prioritizeDiagnostics(payload: unknown): unknown;
|
|
2
14
|
export declare function registerDebugTools(server: McpServer): void;
|
|
@@ -2,16 +2,115 @@ import { z } from "zod";
|
|
|
2
2
|
import { withEngine } from "./with-engine.js";
|
|
3
3
|
import { shapeEngineLogResponse } from "../../lib/log-filters.js";
|
|
4
4
|
import { createDebugReportArtifact } from "../../lib/debug-report.js";
|
|
5
|
+
// summer_get_diagnostics view shaping. The engine serves /api/state/diagnostics
|
|
6
|
+
// from a pre-published snapshot (empty args — query params are NOT forwarded on
|
|
7
|
+
// that route), so trimming has to happen here. The snapshot defaults bury
|
|
8
|
+
// task-specific failures under baseline info noise (50 console messages, up to
|
|
9
|
+
// 100 debugger entries); this reorders by severity and caps ONLY the
|
|
10
|
+
// low-severity bodies. All counts stay intact and honest. No pattern-matching
|
|
11
|
+
// against specific noise strings — severity + recency + caps only.
|
|
12
|
+
const CONSOLE_NOISE_TAIL_CAP = 10;
|
|
13
|
+
const DEBUGGER_WARNING_CAP = 20;
|
|
14
|
+
/**
|
|
15
|
+
* Prioritized, bounded view of the engine diagnostics payload:
|
|
16
|
+
* - console.messages reordered: errors first, then warnings, then a capped tail
|
|
17
|
+
* of info/std/editor noise (engine order is newest-first; that order is
|
|
18
|
+
* preserved within each severity bucket, so the kept tail is the most recent).
|
|
19
|
+
* - debugger.errors_data untouched; debugger.warnings_data capped to the most
|
|
20
|
+
* recent DEBUGGER_WARNING_CAP entries (engine emits newest-first).
|
|
21
|
+
* - a `_view` block with honest counters and a hint pointing at includeAll.
|
|
22
|
+
* Shape-tolerant: anything unexpected passes through unchanged.
|
|
23
|
+
* Exported for unit tests.
|
|
24
|
+
*/
|
|
25
|
+
export function prioritizeDiagnostics(payload) {
|
|
26
|
+
if (!payload || typeof payload !== "object")
|
|
27
|
+
return payload;
|
|
28
|
+
const root = payload;
|
|
29
|
+
if (!root.data || typeof root.data !== "object")
|
|
30
|
+
return payload;
|
|
31
|
+
const data = root.data;
|
|
32
|
+
const shapedData = { ...data };
|
|
33
|
+
let totalConsole = 0;
|
|
34
|
+
let shownConsole = 0;
|
|
35
|
+
let suppressedInfo = 0;
|
|
36
|
+
if (data.console && typeof data.console === "object") {
|
|
37
|
+
const c = data.console;
|
|
38
|
+
totalConsole = typeof c.total === "number" ? c.total : 0;
|
|
39
|
+
if (Array.isArray(c.messages)) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
const rest = [];
|
|
43
|
+
for (const m of c.messages) {
|
|
44
|
+
const type = m && typeof m === "object" ? m.type : undefined;
|
|
45
|
+
if (type === "error")
|
|
46
|
+
errors.push(m);
|
|
47
|
+
else if (type === "warning")
|
|
48
|
+
warnings.push(m);
|
|
49
|
+
else
|
|
50
|
+
rest.push(m);
|
|
51
|
+
}
|
|
52
|
+
const keptRest = rest.slice(0, CONSOLE_NOISE_TAIL_CAP);
|
|
53
|
+
suppressedInfo = rest.length - keptRest.length;
|
|
54
|
+
const messages = [...errors, ...warnings, ...keptRest];
|
|
55
|
+
shownConsole = messages.length;
|
|
56
|
+
shapedData.console = { ...c, messages, returned: messages.length };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let totalDebugger = 0;
|
|
60
|
+
let shownDebugger = 0;
|
|
61
|
+
let suppressedDebuggerWarnings = 0;
|
|
62
|
+
if (data.debugger && typeof data.debugger === "object") {
|
|
63
|
+
const g = data.debugger;
|
|
64
|
+
totalDebugger =
|
|
65
|
+
(typeof g.errors === "number" ? g.errors : 0) +
|
|
66
|
+
(typeof g.warnings === "number" ? g.warnings : 0);
|
|
67
|
+
const errorsData = Array.isArray(g.errors_data) ? g.errors_data : [];
|
|
68
|
+
if (Array.isArray(g.warnings_data)) {
|
|
69
|
+
const keptWarnings = g.warnings_data.slice(0, DEBUGGER_WARNING_CAP);
|
|
70
|
+
suppressedDebuggerWarnings = g.warnings_data.length - keptWarnings.length;
|
|
71
|
+
shownDebugger = errorsData.length + keptWarnings.length;
|
|
72
|
+
shapedData.debugger = { ...g, warnings_data: keptWarnings };
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
shownDebugger = errorsData.length;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...root,
|
|
80
|
+
data: shapedData,
|
|
81
|
+
_view: {
|
|
82
|
+
mode: "prioritized",
|
|
83
|
+
totalConsole,
|
|
84
|
+
shownConsole,
|
|
85
|
+
suppressedInfo,
|
|
86
|
+
totalDebugger,
|
|
87
|
+
shownDebugger,
|
|
88
|
+
suppressedDebuggerWarnings,
|
|
89
|
+
hint: "All counts are complete; only low-severity message bodies were trimmed. Re-call with includeAll: true for the untrimmed payload, or use summer_get_console / summer_get_debugger_warnings for targeted reads.",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
5
93
|
export function registerDebugTools(server) {
|
|
6
94
|
server.tool("summer_get_diagnostics", `Quick overview of all errors and warnings from both the editor console and the runtime debugger. Returns error counts and a guidance message.
|
|
7
95
|
|
|
8
96
|
ALWAYS call this FIRST before diving into summer_get_console or summer_get_debugger_errors. It tells you where to look.
|
|
9
97
|
|
|
98
|
+
By default the response is a prioritized view: errors first, then warnings, then a small capped tail of recent info/std noise. Counts (console totals, debugger totals) are always complete — only low-severity message bodies are trimmed, and a "_view" block reports exactly what was suppressed. Pass includeAll: true for the full untrimmed engine payload.
|
|
99
|
+
|
|
10
100
|
Typical workflow after making changes:
|
|
11
101
|
1. summer_get_diagnostics — are there issues?
|
|
12
102
|
2. If errors: summer_get_console or summer_get_debugger_errors for details
|
|
13
103
|
3. Fix the issues
|
|
14
|
-
4. summer_get_diagnostics again to verify`, {
|
|
104
|
+
4. summer_get_diagnostics again to verify`, {
|
|
105
|
+
includeAll: z
|
|
106
|
+
.boolean()
|
|
107
|
+
.optional()
|
|
108
|
+
.default(false)
|
|
109
|
+
.describe("Return the full engine diagnostics payload untrimmed (no severity reordering, no info-noise cap)."),
|
|
110
|
+
}, async ({ includeAll }) => withEngine(async (client) => {
|
|
111
|
+
const raw = await client.getDiagnostics();
|
|
112
|
+
return includeAll ? raw : prioritizeDiagnostics(raw);
|
|
113
|
+
}));
|
|
15
114
|
server.tool("summer_get_console", `Read recent messages from the editor's Output panel.
|
|
16
115
|
|
|
17
116
|
Output is post-processed for token economy: consecutive identical messages collapse into one entry with a "(×N)" count suffix, and the response carries a "_filter" summary so you can see what was hidden. Use errors_only=true (default) to drop info/std noise; use raw=true to bypass all shaping.
|
|
@@ -88,6 +88,62 @@ function getCurrentScene(projectState, sceneState, health) {
|
|
|
88
88
|
pickString(healthRoot, ["scene", "scenePath", "scene_path"]) ??
|
|
89
89
|
null);
|
|
90
90
|
}
|
|
91
|
+
function getScenePathFromSceneState(sceneState) {
|
|
92
|
+
const root = asRecord(sceneState);
|
|
93
|
+
return (pickString(asRecord(root?.provenance), ["scenePath", "scene_path"]) ??
|
|
94
|
+
pickString(asRecord(root?.data), ["scenePath", "scene_path"]) ??
|
|
95
|
+
null);
|
|
96
|
+
}
|
|
97
|
+
/** Default prefix set kept in summer_get_project_context when no settingsPrefix
|
|
98
|
+
* is given. The full settings dump measured 188KB (~47k tokens) on a real
|
|
99
|
+
* project; these prefixes cover what agents actually key on (main scene,
|
|
100
|
+
* window size, input actions, physics, renderer). */
|
|
101
|
+
const CONTEXT_SETTINGS_PREFIXES = [
|
|
102
|
+
"application/",
|
|
103
|
+
"display/",
|
|
104
|
+
"input/",
|
|
105
|
+
"physics/",
|
|
106
|
+
"rendering/",
|
|
107
|
+
];
|
|
108
|
+
/**
|
|
109
|
+
* Bound the project-state payload. The engine ignores ?prefix= on
|
|
110
|
+
* /api/state/project (snapshot-served with empty args — see
|
|
111
|
+
* ApiClient.getProjectState), so ALL filtering here is client-side:
|
|
112
|
+
* - settingsPrefix given: keep only entries under that prefix.
|
|
113
|
+
* - no prefix: keep only the curated CONTEXT_SETTINGS_PREFIXES.
|
|
114
|
+
* Trimming is always declared in the payload (settingsTruncated /
|
|
115
|
+
* totalSettings / hint); everything outside data.entries is preserved.
|
|
116
|
+
*/
|
|
117
|
+
function trimProjectSettings(projectState, settingsPrefix) {
|
|
118
|
+
const root = asRecord(projectState);
|
|
119
|
+
const data = asRecord(root?.data);
|
|
120
|
+
const entries = data?.entries;
|
|
121
|
+
if (!root || !data || !Array.isArray(entries))
|
|
122
|
+
return projectState;
|
|
123
|
+
const keyOf = (entry) => stringFrom(asRecord(entry)?.key);
|
|
124
|
+
const kept = settingsPrefix
|
|
125
|
+
? entries.filter((entry) => keyOf(entry)?.startsWith(settingsPrefix) ?? false)
|
|
126
|
+
: entries.filter((entry) => {
|
|
127
|
+
const key = keyOf(entry);
|
|
128
|
+
return key !== null && CONTEXT_SETTINGS_PREFIXES.some((p) => key.startsWith(p));
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
...root,
|
|
132
|
+
data: {
|
|
133
|
+
...data,
|
|
134
|
+
entries: kept,
|
|
135
|
+
settingsTruncated: kept.length < entries.length,
|
|
136
|
+
totalSettings: entries.length,
|
|
137
|
+
returnedSettings: kept.length,
|
|
138
|
+
...(settingsPrefix
|
|
139
|
+
? { settingsPrefix }
|
|
140
|
+
: {
|
|
141
|
+
settingsPrefixesIncluded: [...CONTEXT_SETTINGS_PREFIXES],
|
|
142
|
+
settingsHint: "Settings were trimmed to the curated prefixes above to bound payload size. Pass settingsPrefix (e.g. 'audio/' or 'layer_names/') to read another settings group.",
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
91
147
|
export function registerProjectTools(server) {
|
|
92
148
|
server.tool("summer_start_game_task", `Start here for any substantial AI game-building task.
|
|
93
149
|
|
|
@@ -180,12 +236,12 @@ anti-patterns, and recovery steps.`, {}, async () => ({
|
|
|
180
236
|
"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",
|
|
181
237
|
],
|
|
182
238
|
"4_interactive": [
|
|
183
|
-
"To prove input-driven behavior (does jump/move/shoot actually work),
|
|
239
|
+
"To prove input-driven behavior (does jump/move/shoot actually work), prefer RunVerification — a scripted, repeatable probe. SimulateInput is also reachable as a single op against the RUNNING game (see rawOpsViaBatch); do NOT hand this rung to the user while either route works.",
|
|
184
240
|
"RunVerification: spawn a hidden, disposable game instance that runs a GDScript probe (press inputs, read state, save frames) and dies — never touches the user's editor. Returns results.json + frames. Send it as a raw op through summer_batch (see rawOpsViaBatch).",
|
|
185
|
-
"Unlike the editor's own --headless mode, the verify instance renders REAL PIXELS, so save_frame() writes a real image and Performance.TIME_FPS reports a real number.",
|
|
241
|
+
"Unlike the editor's own --headless mode, the verify instance renders REAL PIXELS, so save_frame('name') writes a real image and Performance.TIME_FPS reports a real number. save_frame REQUIRES a name argument — save_frame() with no args is a script error (probe fails).",
|
|
242
|
+
"Mount probe scenes DEFERRED: get_tree().root.add_child.call_deferred(instance); await get_tree().process_frame; await settle(). A direct add_child during _ready can hit the parent-busy guard and 'succeed' while capturing a black frame.",
|
|
186
243
|
"press()/key() are COROUTINES — 'await press(\"move_right\", 500)' or the hold never elapses and the input does nothing.",
|
|
187
244
|
"Assert on physics-frame-derived state (positions after N 'await get_tree().physics_frame'), which is reproducible. press(hold_ms) waits on wall clock, so distance-travelled jitters run to run — assert 'moved more than X', never an exact value.",
|
|
188
|
-
"SimulateInput is NOT reachable from MCP — see rawOpsViaBatch. Do not attempt it as a fallback.",
|
|
189
245
|
],
|
|
190
246
|
},
|
|
191
247
|
// HONESTY — mirror the in-product agent's vision rules. A capture is
|
|
@@ -219,8 +275,9 @@ anti-patterns, and recovery steps.`, {}, async () => ({
|
|
|
219
275
|
// engine ops that have no dedicated tool are still reachable.
|
|
220
276
|
rawOpsViaBatch: [
|
|
221
277
|
"summer_batch runs an array of raw engine ops in one undo group; each op is passed through untouched, so newer engine ops with no dedicated tool are still callable.",
|
|
222
|
-
"RunVerification (hidden probe instance): summer_batch ops:[{op:'RunVerification', probe_source:'<gdscript>', max_seconds:20}]. probe_source extends SummerProbeBase and uses report()/save_frame()/press()/key()/finish(); returns {ok, results, frames, out_dir} or {ok:false, failure_reason: spawn_failed|timeout|bad_args|no_project|probe_not_node}.",
|
|
223
|
-
"SimulateInput
|
|
278
|
+
"RunVerification (hidden probe instance): summer_batch ops:[{op:'RunVerification', probe_source:'<gdscript>', max_seconds:20}]. probe_source extends SummerProbeBase and uses report(name, value)/save_frame(name)/press(action)/key(keycode)/finish(); returns {ok, results, frames, out_dir} or {ok:false, failure_reason: spawn_failed|timeout|bad_args|no_project|probe_not_node}. save_frame REQUIRES a name argument.",
|
|
279
|
+
"SimulateInput (drive the RUNNING game — summer_play first): summer_batch ops:[{op:'SimulateInput', type:'action', action:'jump', pressed:true}], sent ALONE as the only op. failure_reason 'not_running' = start the game first; 'unsupported' = the running game build predates the handler — use RunVerification instead.",
|
|
280
|
+
"SINGLE-OP CONTRACT: the engine rejects any multi-op batch containing SaveScene, InstantiateScene, ReplaceNode, SimulateInput, ViewportSnapshot, GameSnapshot, Run*/Import* or Git* ops (failure_reason 'unsupported_transport', nothing executes). summer_batch splits these into sequential requests for you; when composing raw batches keep them as their own call anyway.",
|
|
224
281
|
"WriteFile and ReplaceText are rejected here by design — use summer_write_file / summer_replace_text so project identity, content guards and same-file ordering are enforced.",
|
|
225
282
|
"You do not need an engine op to run a shell command: your own host already has a shell. The engine binary that runs project scripts is at OS.get_executable_path() (on macOS, /Applications/Summer.app/Contents/MacOS/Summer); see the summer-cli headless-scripting skill.",
|
|
226
283
|
"These are runtime ops, not scene mutations — the batch undo group is a harmless no-op for them.",
|
|
@@ -233,7 +290,7 @@ anti-patterns, and recovery steps.`, {}, async () => ({
|
|
|
233
290
|
"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.",
|
|
234
291
|
"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.",
|
|
235
292
|
"If a run op returns 'unsupported' / an unknown-op error: this engine build predates it — fall back to summer_play + summer_get_debugger_errors.",
|
|
236
|
-
"If
|
|
293
|
+
"If any op returns 'unsupported_transport': it was batched with other ops. Resend it as the ONLY op in the request (nothing from the rejected batch was applied).",
|
|
237
294
|
],
|
|
238
295
|
debugging: [
|
|
239
296
|
"Set SUMMER_MCP_DEBUG=1 in the MCP server's environment to log a structured stderr line per tool call (tool, ok, durationMs, terminalState, errorClass, failureReason, retried, boundProjectIdHash). With the flag OFF, only failures are logged. Use it to see exactly which op failed and why.",
|
|
@@ -249,19 +306,33 @@ anti-patterns, and recovery steps.`, {}, async () => ({
|
|
|
249
306
|
- main scene path from project settings
|
|
250
307
|
- lightweight .summer project memory summary when project path is available
|
|
251
308
|
|
|
252
|
-
Use this first in every fresh chat to avoid guessing scene filenames or editing the wrong scene
|
|
253
|
-
|
|
309
|
+
Use this first in every fresh chat to avoid guessing scene filenames or editing the wrong scene.
|
|
310
|
+
|
|
311
|
+
Project settings in project.data.entries are trimmed to a curated prefix set
|
|
312
|
+
(application/, display/, input/, physics/, rendering/) to bound payload size —
|
|
313
|
+
the untrimmed dump can exceed 45k tokens. The payload declares the trim
|
|
314
|
+
(settingsTruncated, totalSettings, settingsPrefixesIncluded). Pass
|
|
315
|
+
settingsPrefix to read a specific settings group (e.g. 'audio/') instead.`, {
|
|
316
|
+
settingsPrefix: z
|
|
317
|
+
.string()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe("Only return project settings whose key starts with this prefix, e.g. 'audio/' or 'application/config/'. Omit for the curated default set."),
|
|
320
|
+
}, async ({ settingsPrefix }) => withEngine(async (client) => {
|
|
321
|
+
const [health, fullProjectState, sceneState] = await Promise.all([
|
|
254
322
|
client.health(),
|
|
255
|
-
client.getProjectState(),
|
|
323
|
+
client.getProjectState(settingsPrefix),
|
|
256
324
|
client.getSceneState().catch((err) => ({
|
|
257
325
|
ok: false,
|
|
258
326
|
error: err instanceof Error ? err.message : String(err),
|
|
259
327
|
})),
|
|
260
328
|
]);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
const
|
|
329
|
+
// Derived fields read the UNtrimmed state so a narrow settingsPrefix
|
|
330
|
+
// cannot hide mainScene/projectPath from the context summary.
|
|
331
|
+
const projectState = trimProjectSettings(fullProjectState, settingsPrefix);
|
|
332
|
+
const projectPath = getProjectPath(fullProjectState, health);
|
|
333
|
+
const projectName = getProjectName(fullProjectState, health);
|
|
334
|
+
const mainScene = getMainScene(fullProjectState);
|
|
335
|
+
const currentScene = getCurrentScene(fullProjectState, sceneState, health);
|
|
265
336
|
// This tool is the deliberate (re)bind point: capture the currently-open
|
|
266
337
|
// project as the one this session's mutations are pinned to. After an
|
|
267
338
|
// engine project switch the agent calls this to intentionally follow the
|
|
@@ -326,9 +397,48 @@ Example: Bind jump to Space and W:
|
|
|
326
397
|
server.tool("summer_get_scene_tree", `Get a scene tree. Pass scenePath to read that exact in-memory/open scene;
|
|
327
398
|
omit it only when you intentionally want the currently visible editor scene.
|
|
328
399
|
Scene mutations load their explicit target, so a follow-up targeted read does
|
|
329
|
-
not require OpenScene
|
|
400
|
+
not require OpenScene.
|
|
401
|
+
|
|
402
|
+
The engine defaults to depth 2 and limit 200 nodes and SILENTLY truncates
|
|
403
|
+
deeper hierarchies (the response then carries truncated: true and a visited
|
|
404
|
+
count lower than the real node count). Pass an explicit depth (e.g. 10) to
|
|
405
|
+
read a full tree — a 102-node scene returns only 61 nodes at the defaults.`, {
|
|
330
406
|
scenePath: z.string().optional().describe("Exact res:// scene path to inspect"),
|
|
331
|
-
|
|
407
|
+
depth: z
|
|
408
|
+
.number()
|
|
409
|
+
.int()
|
|
410
|
+
.positive()
|
|
411
|
+
.optional()
|
|
412
|
+
.describe("Maximum tree depth to walk. Engine default is 2 — pass a larger value for deep hierarchies."),
|
|
413
|
+
limit: z
|
|
414
|
+
.number()
|
|
415
|
+
.int()
|
|
416
|
+
.positive()
|
|
417
|
+
.optional()
|
|
418
|
+
.describe("Maximum number of nodes to return. Engine default is 200."),
|
|
419
|
+
}, async ({ scenePath, depth, limit }) => withEngine(async (client) => {
|
|
420
|
+
if (depth === undefined && limit === undefined) {
|
|
421
|
+
return client.getSceneState(scenePath);
|
|
422
|
+
}
|
|
423
|
+
// The engine honors depth/limit ONLY on targeted (scene=) reads; an
|
|
424
|
+
// untargeted read is served from a pre-published snapshot built with
|
|
425
|
+
// the defaults, and its query params are dropped. Resolve the current
|
|
426
|
+
// scene path first so depth/limit actually take effect.
|
|
427
|
+
let target = scenePath;
|
|
428
|
+
if (!target) {
|
|
429
|
+
const snapshot = await client.getSceneState();
|
|
430
|
+
target = getScenePathFromSceneState(snapshot) ?? undefined;
|
|
431
|
+
if (!target) {
|
|
432
|
+
const record = asRecord(snapshot);
|
|
433
|
+
return {
|
|
434
|
+
...(record ?? { snapshot }),
|
|
435
|
+
depthLimitApplied: false,
|
|
436
|
+
note: "depth/limit were IGNORED: the current scene path could not be resolved, and the engine only honors depth/limit on scene-targeted reads. Pass scenePath explicitly to apply them.",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return client.getSceneState(target, { depth, limit });
|
|
441
|
+
}));
|
|
332
442
|
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.
|
|
333
443
|
|
|
334
444
|
Use this for:
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { withEngine } from "./with-engine.js";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { withEngine, extractOpError } from "./with-engine.js";
|
|
3
|
+
// Engine ops that MUST be dispatched as their own single-op request. Mirrors
|
|
4
|
+
// _summer_requires_single_async_dispatch (local_api_server.cpp, engine 0.5.60+):
|
|
5
|
+
// the engine rejects any multi-op batch containing one of these WHOLESALE —
|
|
6
|
+
// nothing in the batch executes, and the batch fails with per-op
|
|
7
|
+
// failure_reason "unsupported_transport"/"skipped". Git ops are covered by the
|
|
8
|
+
// prefix check below.
|
|
9
|
+
const SINGLE_ONLY_OPS = new Set([
|
|
10
|
+
"SaveScene", "InstantiateScene", "ReplaceNode",
|
|
11
|
+
"SimulateInput", "ViewportSnapshot", "GameSnapshot",
|
|
12
|
+
"RunCommand", "RunVerification", "RunEditorScript",
|
|
13
|
+
"ImportFromUrl", "ImportFromUrlBatch", "ExtractZipFromUrl",
|
|
14
|
+
]);
|
|
15
|
+
function isSingleOnlyOp(kind) {
|
|
16
|
+
return SINGLE_ONLY_OPS.has(kind) || kind.startsWith("Git");
|
|
17
|
+
}
|
|
5
18
|
function sceneMutationOps(ops) {
|
|
6
19
|
const saveIndexes = ops
|
|
7
20
|
.map((op, index) => op.op === "SaveScene" ? index : -1)
|
|
@@ -17,23 +30,72 @@ function sceneMutationOps(ops) {
|
|
|
17
30
|
}
|
|
18
31
|
return [...ops, { op: "SaveScene" }];
|
|
19
32
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
/** Split an op list into sequential engine requests: consecutive batchable ops
|
|
34
|
+
* stay grouped, each single-only op becomes its own request. Order preserved. */
|
|
35
|
+
function chunkOpsForDispatch(ops) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let current = [];
|
|
38
|
+
for (const op of ops) {
|
|
39
|
+
if (isSingleOnlyOp(String(op.op ?? ""))) {
|
|
40
|
+
if (current.length > 0) {
|
|
41
|
+
chunks.push(current);
|
|
42
|
+
current = [];
|
|
43
|
+
}
|
|
44
|
+
chunks.push([op]);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
current.push(op);
|
|
48
|
+
}
|
|
33
49
|
}
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
if (current.length > 0)
|
|
51
|
+
chunks.push(current);
|
|
52
|
+
return chunks;
|
|
53
|
+
}
|
|
54
|
+
/** Execute an op list as sequential engine requests honoring the single-op
|
|
55
|
+
* dispatch contract. Receipts from every request are preserved and merged, so
|
|
56
|
+
* an op that applied followed by one that failed is reported honestly instead
|
|
57
|
+
* of masked. */
|
|
58
|
+
async function executeOpsChunked(send, ops) {
|
|
59
|
+
const chunks = chunkOpsForDispatch(ops);
|
|
60
|
+
if (chunks.length === 1)
|
|
61
|
+
return send(chunks[0]);
|
|
62
|
+
const receipts = [];
|
|
63
|
+
const combinedResults = [];
|
|
64
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
65
|
+
const receipt = await send(chunks[i]);
|
|
66
|
+
receipts.push(receipt);
|
|
67
|
+
const envelope = (receipt ?? {});
|
|
68
|
+
if (Array.isArray(envelope.results)) {
|
|
69
|
+
combinedResults.push(...envelope.results);
|
|
70
|
+
}
|
|
71
|
+
if (extractOpError(receipt)) {
|
|
72
|
+
const failedKind = String(chunks[i][0]?.op ?? "batch");
|
|
73
|
+
const appliedOps = chunks.slice(0, i).flat().map((op) => String(op.op ?? ""));
|
|
74
|
+
const notSent = chunks.slice(i + 1).flat().map((op) => String(op.op ?? ""));
|
|
75
|
+
const baseError = (typeof envelope.error === "string" && envelope.error) || `Engine request failed (${failedKind}).`;
|
|
76
|
+
const honestError = appliedOps.length > 0
|
|
77
|
+
? `${baseError} NOTE: ${appliedOps.length} earlier op(s) already applied (${appliedOps.join(", ")})` +
|
|
78
|
+
(failedKind === "SaveScene"
|
|
79
|
+
? " — the scene is modified in the editor but NOT saved to disk. Fix the problem, then call summer_save_scene."
|
|
80
|
+
: ".") +
|
|
81
|
+
(notSent.length > 0 ? ` Not sent: ${notSent.join(", ")}.` : "")
|
|
82
|
+
: baseError;
|
|
83
|
+
return {
|
|
84
|
+
...envelope,
|
|
85
|
+
error: honestError,
|
|
86
|
+
results: combinedResults,
|
|
87
|
+
receipts,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
36
90
|
}
|
|
91
|
+
const last = (receipts[receipts.length - 1] ?? {});
|
|
92
|
+
return { ...last, results: combinedResults, requests: chunks.length, receipts };
|
|
93
|
+
}
|
|
94
|
+
/** Scene mutation entry point: appends the transaction-boundary SaveScene, then
|
|
95
|
+
* dispatches with the single-op contract (mutations batch together, SaveScene
|
|
96
|
+
* and other single-only ops travel alone). */
|
|
97
|
+
function executeSceneMutation(client, scenePath, ops, options) {
|
|
98
|
+
return executeOpsChunked((chunk) => client.executeIdentityBoundOps(chunk, { ...(options ?? {}), scenePath }), sceneMutationOps(ops));
|
|
37
99
|
}
|
|
38
100
|
function requireSuccessfulOps(result, context) {
|
|
39
101
|
const receipt = (result ?? {});
|
|
@@ -49,61 +111,94 @@ function requireSuccessfulOps(result, context) {
|
|
|
49
111
|
}
|
|
50
112
|
return receipt;
|
|
51
113
|
}
|
|
114
|
+
/** Compose the minimal valid .tscn text scene: header + one root node.
|
|
115
|
+
* `uid` and `load_steps` are optional in the engine parser
|
|
116
|
+
* (resource_format_text.cpp treats a missing uid as INVALID_ID); the editor
|
|
117
|
+
* assigns a uid on first save. */
|
|
118
|
+
function minimalSceneText(rootName, rootType) {
|
|
119
|
+
return `[gd_scene format=3]\n\n[node name="${rootName}" type="${rootType}"]\n`;
|
|
120
|
+
}
|
|
121
|
+
// Node names reject . : @ / " %; the type must be a plain class name.
|
|
122
|
+
// Both are interpolated into quoted .tscn fields, so validate before composing.
|
|
123
|
+
const VALID_ROOT_NAME = /^[A-Za-z_][A-Za-z0-9_\- ]*$/;
|
|
124
|
+
const VALID_ROOT_TYPE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
52
125
|
export function registerSceneTools(server) {
|
|
53
|
-
server.tool("summer_create_scene", `Create a new empty scene file
|
|
126
|
+
server.tool("summer_create_scene", `Create a new empty scene file. Writes a minimal .tscn through the identity-bound engine with a create-only guard: it fails if the path already exists, and it never touches the currently open scene.
|
|
54
127
|
|
|
55
|
-
|
|
56
|
-
- This tool uses a temporary mutation strategy: it opens a template scene, removes children, saves to a new path, then restores the previous scene.
|
|
57
|
-
- To prevent accidental destructive actions, you MUST explicitly set allow_temporary_scene_mutation=true.
|
|
58
|
-
- If you don't want this strategy, stop and ask the user for manual scene creation in the editor.
|
|
128
|
+
The new scene is on disk but NOT opened. Call summer_open_scene to start editing it, then summer_add_node to build it out.
|
|
59
129
|
|
|
60
130
|
Recommended workflow:
|
|
61
131
|
1) Call summer_get_project_context
|
|
62
|
-
2) Call
|
|
63
|
-
3) Call
|
|
132
|
+
2) Call summer_create_scene with a new res:// path
|
|
133
|
+
3) Call summer_open_scene with that path`, {
|
|
64
134
|
path: z.string().describe("New scene path, e.g. 'res://scenes/empty_level.tscn'"),
|
|
65
135
|
rootName: z.string().default("Main").describe("Root node name for the new scene"),
|
|
136
|
+
rootType: z
|
|
137
|
+
.string()
|
|
138
|
+
.default("Node3D")
|
|
139
|
+
.describe("Root node type. Default 'Node3D' (3D scenes); common alternatives: 'Node2D' (2D scenes), 'Control' (UI scenes), 'Node' (logic-only)."),
|
|
66
140
|
allow_temporary_scene_mutation: z
|
|
67
141
|
.boolean()
|
|
68
|
-
.
|
|
69
|
-
.describe("
|
|
70
|
-
}, async ({ path, rootName,
|
|
71
|
-
|
|
72
|
-
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("DEPRECATED, ignored. Scene creation no longer mutates any open scene; kept only so older callers don't break."),
|
|
144
|
+
}, async ({ path, rootName, rootType }) => withEngine(async (client) => {
|
|
145
|
+
const safePath = path.trim().replace(/\\/g, "/");
|
|
146
|
+
if (!safePath.startsWith("res://") || safePath.includes("..")) {
|
|
147
|
+
throw new Error("Scene path must be a traversal-free res:// project path.");
|
|
148
|
+
}
|
|
149
|
+
if (!safePath.endsWith(".tscn")) {
|
|
150
|
+
throw new Error("New scenes must use the text format: the path must end in .tscn.");
|
|
151
|
+
}
|
|
152
|
+
if (!VALID_ROOT_NAME.test(rootName)) {
|
|
153
|
+
throw new Error(`Invalid rootName "${rootName}": use letters, digits, underscores, hyphens, or spaces, starting with a letter or underscore.`);
|
|
154
|
+
}
|
|
155
|
+
if (!VALID_ROOT_TYPE.test(rootType)) {
|
|
156
|
+
throw new Error(`Invalid rootType "${rootType}": must be a plain class name like 'Node3D', 'Node2D', or 'Control'.`);
|
|
73
157
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
158
|
+
// Same engine-routed guarded write path summer_write_file uses:
|
|
159
|
+
// identity-bound, and mustNotExist makes the engine refuse (create-only)
|
|
160
|
+
// if the path already exists. The open scene is never touched.
|
|
161
|
+
const receipt = requireSuccessfulOps(await client.executeIdentityBoundOps([
|
|
162
|
+
{
|
|
163
|
+
op: "WriteFile",
|
|
164
|
+
path: safePath,
|
|
165
|
+
content: minimalSceneText(rootName, rootType),
|
|
166
|
+
mustNotExist: true,
|
|
167
|
+
},
|
|
168
|
+
]), `Creating ${safePath}`);
|
|
169
|
+
// Post-write verification: read the file back through the engine. The
|
|
170
|
+
// write receipt already passed; a read-back failure is reported honestly
|
|
171
|
+
// instead of failing the create or being masked.
|
|
172
|
+
let verified = false;
|
|
173
|
+
let verifyError;
|
|
174
|
+
try {
|
|
175
|
+
const read = (await client.readProjectFile(safePath, 4096));
|
|
176
|
+
const content = typeof read?.data?.content === "string" ? read.data.content : null;
|
|
177
|
+
if (read?.ok === false) {
|
|
178
|
+
verifyError = String(read.error ?? "engine could not read the file back");
|
|
179
|
+
}
|
|
180
|
+
else if (content === null) {
|
|
181
|
+
verifyError = "engine returned no text content on read-back";
|
|
182
|
+
}
|
|
183
|
+
else if (!content.includes(`[node name="${rootName}"`)) {
|
|
184
|
+
verifyError = "read-back content does not contain the expected root node";
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
verified = true;
|
|
188
|
+
}
|
|
81
189
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const children = tree.data?.children ?? tree.children ?? [];
|
|
85
|
-
const removeOps = children
|
|
86
|
-
.map((c) => c.path)
|
|
87
|
-
.filter((p) => typeof p === "string" && p.length > 0)
|
|
88
|
-
.map((p) => ({ op: "RemoveNode", path: `./${p}` }));
|
|
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}`);
|
|
97
|
-
if (currentScene && currentScene !== path) {
|
|
98
|
-
requireSuccessfulOps(await client.executeOps([{ op: "OpenScene", path: currentScene }]), `Restoring ${currentScene}`);
|
|
190
|
+
catch (err) {
|
|
191
|
+
verifyError = err instanceof Error ? err.message : String(err);
|
|
99
192
|
}
|
|
100
193
|
return {
|
|
101
194
|
ok: true,
|
|
102
|
-
created:
|
|
195
|
+
created: safePath,
|
|
103
196
|
rootName,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
197
|
+
rootType,
|
|
198
|
+
verified,
|
|
199
|
+
...(verifyError ? { verifyError } : {}),
|
|
200
|
+
receipt,
|
|
201
|
+
hint: "The new scene is on disk but not open. Call summer_open_scene to start editing it.",
|
|
107
202
|
};
|
|
108
203
|
}));
|
|
109
204
|
server.tool("summer_add_node", `Add a new node to the scene tree.
|
|
@@ -261,13 +356,18 @@ Each op in the array uses the same format as the individual tools:
|
|
|
261
356
|
- {"op": "SetResourceProperty", "nodePath": "Floor", "resourceProperty": "mesh", "subProperty": "size", "value": "Vector2(20, 20)"}
|
|
262
357
|
|
|
263
358
|
RAW RUNTIME OPS (interactive verification — structured failure_reason passes through verbatim):
|
|
264
|
-
- RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "
|
|
265
|
-
- SimulateInput
|
|
359
|
+
- RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "...", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report(name, value) / save_frame(name) / press(action) / key(keycode) / finish(). save_frame REQUIRES a name argument — save_frame() with no args is a script error. Mount scenes deferred: get_tree().root.add_child.call_deferred(instance); await get_tree().process_frame; await settle() — a direct add_child in _ready can hit the parent-busy guard and capture a black frame.
|
|
360
|
+
- SimulateInput — inject an action/key/mouse/axis into the RUNNING game (summer_play first): {"op": "SimulateInput", "type": "action", "action": "jump", "pressed": true}. It MUST be sent alone (single-op batch). failure_reason "not_running" = start the game first; "unsupported" = the running game build predates the handler — fall back to RunVerification or ask the user.
|
|
266
361
|
|
|
267
362
|
Do not mix OpenScene with scene mutations in one batch. OpenScene is a UI action;
|
|
268
363
|
send it separately. scenePath selects every mutation target. The tool appends one
|
|
269
364
|
final SaveScene when the batch mutates a scene; if supplied explicitly, SaveScene
|
|
270
|
-
must appear exactly once and be the final operation
|
|
365
|
+
must appear exactly once and be the final operation. The engine requires
|
|
366
|
+
SaveScene, InstantiateScene, ReplaceNode, SimulateInput, and the Run*/Import*
|
|
367
|
+
ops to travel as their own request, so this tool automatically splits your op
|
|
368
|
+
list into sequential requests around them — each split chunk is its own undo
|
|
369
|
+
step, and if a later chunk fails the receipt reports exactly which earlier ops
|
|
370
|
+
already applied.`, {
|
|
271
371
|
scenePath: z.string().optional().describe("Required when ops contains scene mutations; exact res:// target scene path"),
|
|
272
372
|
ops: z.array(z.record(z.unknown())).describe("Array of operation objects, each with 'op' plus its parameters"),
|
|
273
373
|
}, async ({ scenePath, ops }) => withEngine(async (client) => {
|
|
@@ -291,6 +391,6 @@ must appear exactly once and be the final operation.`, {
|
|
|
291
391
|
const options = { groupUndo: true, ...(scenePath ? { scenePath } : {}) };
|
|
292
392
|
return needsScenePath
|
|
293
393
|
? executeSceneMutation(client, scenePath, ops, options)
|
|
294
|
-
: client.executeOps(
|
|
394
|
+
: executeOpsChunked((chunk) => client.executeOps(chunk, options), ops);
|
|
295
395
|
}));
|
|
296
396
|
}
|
|
@@ -18,7 +18,7 @@ target:
|
|
|
18
18
|
It does NOT use the scene's environment/sky, and it injects a synthetic camera and light when the scene has none. The scene's WorldEnvironment — sky, fog, tonemap, glow, SSAO, ambient — is replaced by a flat preview environment. So this target CANNOT verify lighting, mood, or any material property that depends on the environment: change them and the frame comes back identical. For those, boot the game or run a RunVerification probe, whose instance renders the real environment.
|
|
19
19
|
"game" — a frame from the RUNNING game (real runtime state). Start the game first (summer_play). Not available over a plain local connection — needs the Summer desktop app bridge.
|
|
20
20
|
|
|
21
|
-
Static frame only — one moment, not motion. For a SEQUENCE of frames over time, or for anything lighting-dependent when the desktop bridge is unavailable, use a RunVerification probe's save_frame() — its instance has a real renderer.`, {
|
|
21
|
+
Static frame only — one moment, not motion. For a SEQUENCE of frames over time, or for anything lighting-dependent when the desktop bridge is unavailable, use a RunVerification probe's save_frame(name) — its instance has a real renderer.`, {
|
|
22
22
|
target: z
|
|
23
23
|
.enum(["viewport", "scene", "game"])
|
|
24
24
|
.optional()
|
|
@@ -29,9 +29,12 @@ Static frame only — one moment, not motion. For a SEQUENCE of frames over time
|
|
|
29
29
|
.optional()
|
|
30
30
|
.describe('target:"scene" only. Full scene path, e.g. "res://main.tscn". Omit to render the currently-open scene.'),
|
|
31
31
|
framing: z
|
|
32
|
-
.enum(["auto", "top", "front", "
|
|
32
|
+
.enum(["auto", "iso", "top", "front", "back", "left", "right"])
|
|
33
33
|
.optional()
|
|
34
|
-
.describe('target:"scene" only. Camera
|
|
34
|
+
.describe('target:"scene" only, 3D scenes. Camera direction preset: "iso" = 3/4 diagonal view, ' +
|
|
35
|
+
'"top" = straight down, "front" = camera at +Z, "back" = camera at -Z, ' +
|
|
36
|
+
'"left" = camera at -X, "right" = camera at +X. "auto" (default) is an alias of "iso". ' +
|
|
37
|
+
"The result reports the resolved framing."),
|
|
35
38
|
size: z
|
|
36
39
|
.array(z.number().int().positive())
|
|
37
40
|
.length(2)
|
|
@@ -40,7 +43,10 @@ Static frame only — one moment, not motion. For a SEQUENCE of frames over time
|
|
|
40
43
|
nodePath: z
|
|
41
44
|
.string()
|
|
42
45
|
.optional()
|
|
43
|
-
.describe('target:"scene" only.
|
|
46
|
+
.describe('target:"scene" only. Node path relative to the scene root (e.g. "Player/Mesh") to frame ' +
|
|
47
|
+
"INSTEAD of the whole scene — the camera fits that node's combined bounds (3D visual AABBs " +
|
|
48
|
+
"or 2D rects, children included). A bare unique name is also found recursively. " +
|
|
49
|
+
'Fails with failure_reason "node_not_found" when the path does not resolve (no silent whole-scene fallback).'),
|
|
44
50
|
}, async ({ target, scenePath, framing, size, nodePath }) => withEngine(async (client) => {
|
|
45
51
|
if (target === "game")
|
|
46
52
|
return client.gameSnapshot();
|
|
@@ -112,11 +118,28 @@ Static frame only — one moment, not motion. For a SEQUENCE of frames over time
|
|
|
112
118
|
if (snap.sceneHadLight === false) {
|
|
113
119
|
warnings.push("WARNING: this scene has no light — lit materials may appear black when played.");
|
|
114
120
|
}
|
|
121
|
+
// The engine ALWAYS synthesizes the preview camera (preview_ops.cpp
|
|
122
|
+
// sets used_synthetic_camera unconditionally) — the flag says nothing
|
|
123
|
+
// about the scene's own cameras. sceneHasCamera above is the
|
|
124
|
+
// authoritative "does this scene have a camera" answer.
|
|
115
125
|
if (snap.usedSyntheticCamera) {
|
|
116
|
-
warnings.push("NOTE: this preview
|
|
126
|
+
warnings.push("NOTE: this preview is framed by a synthetic render camera, NOT the scene's own camera — the played game will not frame like this image.");
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
|
-
|
|
129
|
+
// Scene-preview capture details: resolved framing ("auto" -> "iso"),
|
|
130
|
+
// which node was framed (confirms nodePath resolved), and how many
|
|
131
|
+
// blank-readback retries the engine needed (0 = omitted).
|
|
132
|
+
const details = [];
|
|
133
|
+
if (target === "scene") {
|
|
134
|
+
if (snap.framing)
|
|
135
|
+
details.push(`framing: ${snap.framing}`);
|
|
136
|
+
if (snap.framedNode)
|
|
137
|
+
details.push(`framed node: ${snap.framedNode}`);
|
|
138
|
+
if (snap.renderRetries)
|
|
139
|
+
details.push(`render retries: ${snap.renderRetries}`);
|
|
140
|
+
}
|
|
141
|
+
const detailNote = details.length ? `; ${details.join(", ")}` : "";
|
|
142
|
+
const caption = `${label} (${dims}${detailNote}). Saved to ${snap.localPath ?? "n/a"}. Review the image above and describe what you actually see.` +
|
|
120
143
|
(warnings.length ? `\n\n${warnings.join("\n")}` : "");
|
|
121
144
|
return [
|
|
122
145
|
{ type: "image", data: snap.base64, mimeType: snap.mime || "image/jpeg" },
|
|
@@ -9,6 +9,13 @@ function attachMeta(result, meta) {
|
|
|
9
9
|
});
|
|
10
10
|
return result;
|
|
11
11
|
}
|
|
12
|
+
function getFailureReason(value) {
|
|
13
|
+
return typeof value.failureReason === "string"
|
|
14
|
+
? value.failureReason
|
|
15
|
+
: typeof value.failure_reason === "string"
|
|
16
|
+
? value.failure_reason
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
12
19
|
// 0.5.34 Block E contract (publicsummerengine src/lib/tools/contract.ts §0.1).
|
|
13
20
|
// The async lifecycle (async-op-lifecycle.ts pollOpToTerminal) merges
|
|
14
21
|
// terminalState/errorClass onto the apply dict it returns. ONLY these two are
|
|
@@ -36,14 +43,11 @@ function readClassifiers(result) {
|
|
|
36
43
|
if (!result || typeof result !== "object")
|
|
37
44
|
return {};
|
|
38
45
|
const op = result;
|
|
46
|
+
const failed = op.results?.find((entry) => entry.ok === false);
|
|
39
47
|
return {
|
|
40
48
|
terminalState: typeof op.terminalState === "string" ? op.terminalState : undefined,
|
|
41
49
|
errorClass: typeof op.errorClass === "string" ? op.errorClass : undefined,
|
|
42
|
-
failureReason:
|
|
43
|
-
? op.failureReason
|
|
44
|
-
: typeof op.failure_reason === "string"
|
|
45
|
-
? op.failure_reason
|
|
46
|
-
: undefined,
|
|
50
|
+
failureReason: getFailureReason(op) ?? (failed ? getFailureReason(failed) : undefined),
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
@@ -63,25 +67,53 @@ export function extractOpError(result) {
|
|
|
63
67
|
if (!result || typeof result !== "object")
|
|
64
68
|
return null;
|
|
65
69
|
const op = result;
|
|
70
|
+
const firstFailed = op.results?.find((r) => r.ok === false);
|
|
71
|
+
// The engine stamps failure classifiers on the envelope OR per-op inside
|
|
72
|
+
// results[] (the batch gate does the latter) — read both.
|
|
73
|
+
const failureReason = getFailureReason(op) ?? (firstFailed ? getFailureReason(firstFailed) : undefined);
|
|
74
|
+
// Classified failures are rendered as JSON so callers can reliably read
|
|
75
|
+
// failure_reason instead of scraping a sentence. A plain-text "Hint:" line may
|
|
76
|
+
// follow the JSON in the final tool text. Unclassified failures stay plain.
|
|
77
|
+
const classify = (message) => {
|
|
78
|
+
if (!failureReason)
|
|
79
|
+
return message;
|
|
80
|
+
return JSON.stringify({
|
|
81
|
+
error: message,
|
|
82
|
+
failure_reason: failureReason,
|
|
83
|
+
...(typeof firstFailed?.error === "string" && firstFailed.error.length > 0 && firstFailed.error !== message
|
|
84
|
+
? { op_error: firstFailed.error }
|
|
85
|
+
: {}),
|
|
86
|
+
...(typeof firstFailed?.op === "string" ? { op: firstFailed.op } : {}),
|
|
87
|
+
...(typeof op.terminalState === "string" ? { terminalState: op.terminalState } : {}),
|
|
88
|
+
...(typeof op.errorClass === "string" ? { errorClass: op.errorClass } : {}),
|
|
89
|
+
}, null, 2);
|
|
90
|
+
};
|
|
66
91
|
// Failure terminalState takes precedence — it is set by the async lifecycle
|
|
67
92
|
// exactly when the op did not land (timeout, backpressure, lease/identity
|
|
68
|
-
// rejection, cancellation),
|
|
93
|
+
// rejection, cancellation), sometimes with NO results[] to inspect. Prefer the
|
|
94
|
+
// precise engine rejection (envelope error, then the failed op's error) over
|
|
95
|
+
// the generic terminal-state sentence.
|
|
69
96
|
const ts = op.terminalState;
|
|
70
97
|
if (typeof ts === "string" && ts.length > 0 && !SUCCESS_TERMINAL_STATES.has(ts)) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
const message = (typeof op.error === "string" && op.error.length > 0 && op.error) ||
|
|
99
|
+
(typeof firstFailed?.error === "string" && firstFailed.error.length > 0 && firstFailed.error) ||
|
|
100
|
+
TERMINAL_STATE_MESSAGES[ts] ||
|
|
101
|
+
`Engine operation failed (terminalState: ${ts}).`;
|
|
102
|
+
return classify(message);
|
|
74
103
|
}
|
|
75
|
-
// An explicit ok:false / status:"error"
|
|
76
|
-
// omitted an error string — surface it rather
|
|
77
|
-
// contract `isFailureSignal`).
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
// An explicit ok:false / status:"error" / failed op inside results[] is a
|
|
105
|
+
// failure even when the engine omitted an error string — surface it rather
|
|
106
|
+
// than mask it (matches the web contract `isFailureSignal`). The envelope
|
|
107
|
+
// error is kept when present; the failed op's own error backs it up.
|
|
108
|
+
if (op.ok === false || op.status === "error" || firstFailed) {
|
|
109
|
+
const message = (typeof op.error === "string" && op.error.length > 0 && op.error) ||
|
|
110
|
+
(typeof firstFailed?.error === "string" && firstFailed.error.length > 0 && firstFailed.error) ||
|
|
111
|
+
(firstFailed
|
|
112
|
+
? `Engine op failed${firstFailed.op ? ` (${firstFailed.op})` : ""}.`
|
|
113
|
+
: op.ok === false
|
|
114
|
+
? "Engine operation failed (ok: false)."
|
|
115
|
+
: "Engine operation failed (status: error).");
|
|
116
|
+
return classify(message);
|
|
85
117
|
}
|
|
86
118
|
return null;
|
|
87
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer-engine",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.1",
|
|
4
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",
|