summer-engine 2.6.3 → 2.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.factory-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +31 -0
- package/GEMINI.md +2 -1
- package/README.md +52 -18
- package/dist/lib/api-client.d.ts +65 -1
- package/dist/lib/api-client.js +193 -10
- package/dist/lib/cloud/engine-bridge.js +9 -1
- package/dist/lib/game-task-plan.js +6 -2
- package/dist/mcp/server.js +50 -17
- package/dist/mcp/tools/asset-tools.js +14 -4
- package/dist/mcp/tools/generate-tools.js +6 -0
- package/dist/mcp/tools/project-tools.js +72 -5
- package/dist/mcp/tools/scene-tools.js +6 -2
- package/dist/mcp/tools/visual-tools.js +86 -11
- package/dist/mcp/tools/with-engine.d.ts +32 -7
- package/dist/mcp/tools/with-engine.js +91 -54
- package/package.json +6 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "summer",
|
|
4
4
|
"version": "2.5.1",
|
|
5
|
-
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a
|
|
5
|
+
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge to the local desktop app.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Summer Engine",
|
|
8
8
|
"email": "founders@summerengine.com",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer",
|
|
3
3
|
"version": "2.5.1",
|
|
4
|
-
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a
|
|
4
|
+
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge. Build games by talking.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Summer Engine",
|
|
7
7
|
"email": "founders@summerengine.com",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"interface": {
|
|
93
93
|
"displayName": "Summer",
|
|
94
94
|
"shortDescription": "Superpowers for AI game dev. Build games by talking.",
|
|
95
|
-
"longDescription": "Summer turns Codex into a game-dev studio for Summer Engine, the AI-native game engine. It adds game-dev skills, a using-summer workflow skill, lifecycle hooks, and a
|
|
95
|
+
"longDescription": "Summer turns Codex into a game-dev studio for Summer Engine, the AI-native game engine. It adds game-dev skills, a using-summer workflow skill, lifecycle hooks, and a 52-tool MCP bridge for scene editing, asset import, play mode, diagnostics, and generation. File, git, shell, and grep stay with the host agent. Summer Engine is compatible with Godot 4.5. Write code in GDScript and drive the engine through MCP. Session-start orients the session. Pre-commit doctor (opt-in) blocks bad commits.",
|
|
96
96
|
"developerName": "Summer Engine",
|
|
97
97
|
"category": "Coding",
|
|
98
98
|
"capabilities": ["Interactive", "Read", "Write"],
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "summer",
|
|
4
4
|
"displayName": "Summer",
|
|
5
5
|
"version": "2.5.1",
|
|
6
|
-
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a
|
|
6
|
+
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge to the local desktop app.",
|
|
7
7
|
"author": { "name": "Summer Engine", "email": "founders@summerengine.com" },
|
|
8
8
|
"publisher": "summer-engine",
|
|
9
9
|
"homepage": "https://summerengine.com",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer",
|
|
3
|
-
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a
|
|
3
|
+
"description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge to the local desktop app.",
|
|
4
4
|
"version": "2.5.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Summer Engine",
|
package/AGENTS.md
CHANGED
|
@@ -22,11 +22,12 @@ This file is for any AI agent that loads context from `AGENTS.md` (Codex CLI, Fa
|
|
|
22
22
|
|
|
23
23
|
## MCP tool palette (engine on `localhost:6550`)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
52 tools total. Categories:
|
|
26
26
|
|
|
27
27
|
- Scene: `summer_get_scene_tree`, `summer_open_scene`, `summer_create_scene`, `summer_add_node`, `summer_set_prop`, `summer_set_resource_property`, `summer_remove_node`, `summer_save_scene`, `summer_instantiate_scene`, `summer_replace_node`, `summer_select_node`, `summer_inspect_node`, `summer_inspect_resource`, `summer_connect_signal`, `summer_batch`.
|
|
28
28
|
- Diagnostics: `summer_get_script_errors`, `summer_get_diagnostics`, `summer_get_console`, `summer_clear_console`, `summer_get_debugger_errors`, `summer_get_debugger_warnings`.
|
|
29
29
|
- Runtime: `summer_play`, `summer_stop`, `summer_is_running`.
|
|
30
|
+
- Visual: `summer_screenshot` (capture the editor viewport or running game as an image to verify it).
|
|
30
31
|
- Project: `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind`, `summer_get_agent_playbook`.
|
|
31
32
|
- Assets: `summer_search_assets`, `summer_list_my_assets`, `summer_get_asset`, `summer_get_asset_download_url`, `summer_import_asset`, `summer_import_asset_by_id`, `summer_import_from_url`, `summer_import_from_url_batch`.
|
|
32
33
|
- Generation: `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video`, `summer_generate_motion`, `summer_check_job`.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
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
|
+
## [2.6.6] — 2026-07-15 — "Project-bound engine requests"
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Every local engine request now carries the engine instance ID, stable project ID, project ID hash, and identity protocol version captured when the CLI connects. This lets compatible engine builds reject stale requests after a project or engine switch instead of acting on the wrong target.
|
|
9
|
+
- An explicit project rebind now refreshes the complete engine and project identity, while keeping the existing project-hash mutation guard and screenshot drift checks.
|
|
10
|
+
- Summer Cloud's engine bridge now binds its save, rescan, and scene reload requests to the project it verified on disk.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Summer Cloud is documented as an optional Research Preview instead of part of the core local CLI and MCP workflow.
|
|
14
|
+
- Release metadata and the manual npm runbook now pin the public registry and require a clean, reviewed public source checkout.
|
|
15
|
+
|
|
16
|
+
## [2.6.5] — 2026-07-04 — "Cloud tools don't need the engine"
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Tool descriptions now say explicitly which tools run in Summer's cloud and work WITHOUT the engine open (`summer_generate_*`, `summer_search_assets`, `summer_list_my_assets`, `summer_get_asset`, `summer_get_asset_download_url`, `summer_check_job`) and which need the engine (imports, scene ops). Agents were misreading a missing `npx summer-engine login` as "MCP requires the engine".
|
|
20
|
+
- The "Summer Engine is not running" error now tells the agent that cloud tools still work without the engine.
|
|
21
|
+
|
|
22
|
+
## [2.6.4] — 2026-07-03 — "See-Work + project binding" (unpublished, ships with 2.6.5)
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- MCP session binds to its project; the engine rejects wrong-project writes (`identity_mismatch`) instead of applying them to whatever project is open.
|
|
26
|
+
- Structured per-tool-call stderr logging; agent playbook rewritten around the MCP verification ladder; honest game-capture failure states and identity-stamped reads.
|
|
27
|
+
|
|
28
|
+
## [2.6.3] — 2026-06-30 — "Agent vision"
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `summer_screenshot` MCP tool: capture the editor viewport or the running game as an image the agent sees directly (`target: "viewport" | "game"`, viewport by default). Lets the agent visually verify scene layout, asset placement, scale, framing, and runtime state — the client reads the actual frame, with no description step in between. Total MCP tool surface is now 52.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- MCP/CLI session now reconnects automatically after a transient engine restart (the engine rotates its api-token and can move its port on relaunch), instead of surfacing as a "disconnected" error.
|
|
35
|
+
|
|
5
36
|
## [2.6.0] — 2026-06-10 — "Summer Cloud"
|
|
6
37
|
|
|
7
38
|
### Added
|
package/GEMINI.md
CHANGED
|
@@ -20,12 +20,13 @@ The single most important skill to know is `summer:using-summer` — it explains
|
|
|
20
20
|
|
|
21
21
|
## MCP tools (when the engine is running on `localhost:6550`)
|
|
22
22
|
|
|
23
|
-
The `summer-engine` MCP server exposes
|
|
23
|
+
The `summer-engine` MCP server exposes 52 focused tools. The most important:
|
|
24
24
|
|
|
25
25
|
- **Scene mutation**: `summer_add_node`, `summer_set_prop`, `summer_set_resource_property`, `summer_remove_node`, `summer_save_scene`, `summer_replace_node`, `summer_batch`.
|
|
26
26
|
- **Scene inspection**: `summer_get_scene_tree`, `summer_inspect_node`, `summer_inspect_resource`.
|
|
27
27
|
- **Diagnostics**: `summer_get_script_errors` (cheapest), `summer_get_diagnostics`, `summer_get_console`, `summer_get_debugger_errors`, `summer_get_debugger_warnings`.
|
|
28
28
|
- **Run game**: `summer_play`, `summer_stop`, `summer_is_running`.
|
|
29
|
+
- **Visual**: `summer_screenshot` (capture the viewport or running game as an image you can see).
|
|
29
30
|
- **Project**: `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind`.
|
|
30
31
|
- **Assets**: `summer_search_assets`, `summer_import_asset`, `summer_import_from_url`, `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video`, `summer_check_job`.
|
|
31
32
|
|
package/README.md
CHANGED
|
@@ -18,27 +18,11 @@ It just works. Open your agent, say *"let's make an FPS in Summer Engine,"* and
|
|
|
18
18
|
Three names, one npm package (`summer-engine`):
|
|
19
19
|
|
|
20
20
|
- **Summer CLI** (`npx -y summer-engine@latest`): installs the engine, signs you in, scaffolds and runs projects, and writes your agent's config. See [www.summerengine.com/cli](https://www.summerengine.com/cli).
|
|
21
|
-
- **Summer MCP**: the local MCP server that gives your agent
|
|
21
|
+
- **Summer MCP**: the local MCP server that gives your agent 52 engine tools to build, run, and debug a real game. See [www.summerengine.com/mcp](https://www.summerengine.com/mcp).
|
|
22
22
|
- **Summer agent layer**: the game-dev skills, hooks, and plugin manifests that give your AI agent judgment, not just a chat box.
|
|
23
23
|
|
|
24
24
|
All MIT, all free to use. One paste sets up all three.
|
|
25
25
|
|
|
26
|
-
## Summer Cloud (new in 2.6.0)
|
|
27
|
-
|
|
28
|
-
`summer cloud` syncs your whole game project across machines: scenes, scripts, settings, and the big binary assets that never fit in git. Files are stored by content hash, every push becomes a restorable version, and local checkpoints are taken before anything destructive touches your disk.
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
summer cloud init # bind this project to your account
|
|
32
|
-
summer cloud push # upload what changed (only missing bytes travel)
|
|
33
|
-
summer cloud pull # make this machine match the cloud, verified by hash
|
|
34
|
-
summer cloud status # what differs between here and the cloud
|
|
35
|
-
summer cloud restore # roll the project back to any version
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
The same operations are available to agents as MCP tools (`summer_cloud_push`, `summer_cloud_pull`, and friends). See [www.summerengine.com/cloud](https://www.summerengine.com/cloud).
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
26
|
## Get started: one prompt
|
|
43
27
|
|
|
44
28
|
Open your AI agent (Claude Code, Cursor, Codex, Copilot, Devin Desktop, etc.) and paste:
|
|
@@ -269,13 +253,15 @@ Two pieces, plus the glue.
|
|
|
269
253
|
|
|
270
254
|
Skills don't list steps. They encode the **order of operations**: diagnose before editing, scope before building, ask before guessing. [Agent Skills](https://agentskills.io) format, so any conformant tool picks them up.
|
|
271
255
|
|
|
272
|
-
**MCP bridge.** The `summer-engine` MCP server gives the agent
|
|
256
|
+
**MCP bridge.** The `summer-engine` MCP server gives the agent 52 tools that talk to your local engine on `localhost:6550`:
|
|
273
257
|
|
|
274
258
|
| | |
|
|
275
259
|
|---|---|
|
|
276
260
|
| Scene | `summer_add_node`, `summer_set_prop`, `summer_instantiate_scene`, `summer_replace_node`, `summer_get_scene_tree`, `summer_inspect_node`, `summer_batch` |
|
|
277
261
|
| Diagnostics | `summer_get_script_errors`, `summer_get_diagnostics`, `summer_get_console`, `summer_get_debugger_errors`, `summer_get_debugger_warnings` |
|
|
278
262
|
| Runtime | `summer_play`, `summer_stop`, `summer_is_running` |
|
|
263
|
+
| Visual | `summer_screenshot` (see the editor viewport, an offscreen scene render, or the running game) |
|
|
264
|
+
| Interactive | `SimulateInput` / `RunVerification` raw ops via `summer_batch` — drive the running game or run a hidden GDScript probe (engine-build dependent) |
|
|
279
265
|
| Project | `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind` |
|
|
280
266
|
| Assets | `summer_search_assets`, `summer_import_asset`, `summer_import_from_url`, `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video` |
|
|
281
267
|
|
|
@@ -285,6 +271,36 @@ File ops, git, shell, and grep already belong to your agent. We don't shadow the
|
|
|
285
271
|
|
|
286
272
|
For the full architecture, see [docs/OVERVIEW.md](docs/OVERVIEW.md).
|
|
287
273
|
|
|
274
|
+
### Using Summer via MCP: the verification loop
|
|
275
|
+
|
|
276
|
+
There is no single "verify" tool. You **compose** the checks, climbing only as high as the change demands. Call `summer_get_agent_playbook` for the full ladder; the short version:
|
|
277
|
+
|
|
278
|
+
**1. Does it compile?** After writing a `.gd`, call `summer_get_script_errors res://scripts/player.gd` — no game boot. For a project-wide sweep, `summer_get_diagnostics` (it tells you whether to then read `summer_get_console` / `summer_get_debugger_errors`).
|
|
279
|
+
|
|
280
|
+
**2. Does it look right?** `summer_screenshot` returns the actual pixels — you see them, no description layer. Pick the target on purpose:
|
|
281
|
+
- `viewport` (default) — the editor's current tab, as it looks now.
|
|
282
|
+
- `scene` — an offscreen render of a scene *file* (`scenePath`). No boot; **static at t=0**. Confesses if the scene has no `Camera3D` (renders grey/black when played) or no light — read those warnings.
|
|
283
|
+
- `game` — a frame from the **running** game (`summer_play` first). Over plain local HTTP this returns an honest failure until the desktop bridge is present; when it fails, drop to `viewport`/`scene` or ask the user.
|
|
284
|
+
|
|
285
|
+
**3. Does it run?** Compose the loop:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
summer_clear_console # clean slate
|
|
289
|
+
summer_play # boot the game (or a specific scene)
|
|
290
|
+
summer_get_debugger_errors # runtime errors: null refs, missing nodes, physics
|
|
291
|
+
summer_screenshot target:game # optional: look at the live frame
|
|
292
|
+
summer_stop # ALWAYS stop — scene edits are refused while running
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**4. Does the interaction work?** To prove input-driven behavior (jump/move/shoot), send a raw op through `summer_batch` (engine-build dependent — a `failure_reason:"unsupported"` comes back verbatim on older builds):
|
|
296
|
+
- `SimulateInput` — inject into the **running** game: `summer_batch ops:[{op:"SimulateInput", type:"action", action:"jump", pressed:true}]`, then re-check screenshot/debugger.
|
|
297
|
+
- `RunVerification` — spawn a hidden, disposable game instance that runs a GDScript probe (press inputs, read state, save frames) and dies, never touching the editor: `summer_batch ops:[{op:"RunVerification", probe_source:"…", max_seconds:20}]` → `{ok, results, frames, out_dir}`.
|
|
298
|
+
- If neither is available on this build, **ask the user to try it** and report what they see — don't fake it.
|
|
299
|
+
|
|
300
|
+
**Honesty.** Never describe an image you didn't receive; a failed capture is a *result* — report it and climb down (`scene`→`viewport`) or ask the user. Pass structured failures (`failure_reason`, `terminalState`, `identity_mismatch`) through verbatim. `summer_get_project_context` binds the session to the open project; a `projectMismatch` warning on a screenshot means the engine switched projects and the frame may be from the wrong one — rebind before trusting it.
|
|
301
|
+
|
|
302
|
+
**Debugging the MCP itself.** Set `SUMMER_MCP_DEBUG=1` in the server's environment to log a structured stderr line per tool call (`tool`, `ok`, `durationMs`, `terminalState`, `errorClass`, `failureReason`, `retried`). With the flag off, only failures are logged.
|
|
303
|
+
|
|
288
304
|
---
|
|
289
305
|
|
|
290
306
|
## The basic workflow
|
|
@@ -524,6 +540,24 @@ Skills evolve fast. Two ways to help:
|
|
|
524
540
|
|
|
525
541
|
---
|
|
526
542
|
|
|
543
|
+
## Research Preview: Summer Cloud
|
|
544
|
+
|
|
545
|
+
Summer Cloud is an experimental R&D preview. It is optional, it may change or break, and it is **not** the core Summer CLI or MCP workflow. You do not need it to install or run Summer Engine, work with local projects, or use the local MCP server. The stable workflow is the local engine, CLI, MCP bridge, and agent skills described above.
|
|
546
|
+
|
|
547
|
+
The preview explores content-addressed project sync across machines, including large assets that do not fit comfortably in git:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
summer cloud init
|
|
551
|
+
summer cloud status
|
|
552
|
+
summer cloud push
|
|
553
|
+
summer cloud pull
|
|
554
|
+
summer cloud restore
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Matching `summer_cloud_*` MCP tools are also experimental. Keep an independent backup and do not use this preview as the only recovery path for an important project. See [www.summerengine.com/cloud](https://www.summerengine.com/cloud).
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
527
561
|
## License
|
|
528
562
|
|
|
529
563
|
MIT for everything in this repo. Summer Engine itself is proprietary. See [What's open and what's not](#whats-open-and-whats-not).
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -16,12 +16,49 @@ export type EngineSnapshot = {
|
|
|
16
16
|
bytes?: number;
|
|
17
17
|
error?: string;
|
|
18
18
|
metadata?: Record<string, unknown>;
|
|
19
|
+
/** Structured failure classifier the engine returns on a game snapshot over
|
|
20
|
+
* local HTTP (409 `bridge_required`) — surfaced verbatim so the tool can give
|
|
21
|
+
* an honest, actionable message instead of a truncated generic 409 string.
|
|
22
|
+
* When P4.4 lands (game snapshots answer 200/202 over HTTP) this simply
|
|
23
|
+
* never fires and the tool works. */
|
|
24
|
+
failureReason?: string;
|
|
25
|
+
/** Scene-preview confession fields (P4.3). Populated only for target:"scene".
|
|
26
|
+
* A scene with no Camera3D renders grey/black when played; the engine reports
|
|
27
|
+
* whether it had to synthesize a camera/light so the model can warn honestly. */
|
|
28
|
+
sceneHasCamera?: boolean;
|
|
29
|
+
sceneHadLight?: boolean;
|
|
30
|
+
usedSyntheticCamera?: boolean;
|
|
31
|
+
/** Set true when the bound projectIdHash no longer matches the engine's live
|
|
32
|
+
* health hash at capture time — the frame may be from the WRONG project
|
|
33
|
+
* (item 4, client-side drift check). */
|
|
34
|
+
projectMismatch?: boolean;
|
|
35
|
+
};
|
|
36
|
+
type EngineTargetIdentity = {
|
|
37
|
+
instanceId?: string;
|
|
38
|
+
projectId?: string;
|
|
39
|
+
projectIdHash?: string;
|
|
19
40
|
};
|
|
20
41
|
export declare class EngineApiClient {
|
|
21
42
|
private port;
|
|
22
43
|
private token;
|
|
23
|
-
|
|
44
|
+
private targetIdentity;
|
|
45
|
+
constructor(port: number, token: string, targetIdentity?: EngineTargetIdentity | string);
|
|
24
46
|
static connect(): Promise<EngineApiClient>;
|
|
47
|
+
/** The projectIdHash this session is bound to (undefined if none was reported
|
|
48
|
+
* at connect — e.g. no project open yet). */
|
|
49
|
+
getBoundProjectIdHash(): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Re-read health and rebind to the currently-open project. Called by
|
|
52
|
+
* summer_get_project_context so the agent can INTENTIONALLY follow a project
|
|
53
|
+
* switch (the deliberate escape hatch after an identity_mismatch). Returns the
|
|
54
|
+
* new bound hash. The direct health probe is intentionally unscoped, so it can
|
|
55
|
+
* reach the current engine even when bound requests would be rejected.
|
|
56
|
+
*/
|
|
57
|
+
rebind(): Promise<string | undefined>;
|
|
58
|
+
/** Identity to attach to a MUTATING command's options so the engine can reject
|
|
59
|
+
* a wrong-project write. Empty when unbound (engine then skips the check). */
|
|
60
|
+
private identityOptions;
|
|
61
|
+
private targetUrl;
|
|
25
62
|
private request;
|
|
26
63
|
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
27
64
|
* the status (202 queued vs 200 legacy). */
|
|
@@ -51,8 +88,34 @@ export declare class EngineApiClient {
|
|
|
51
88
|
getSelection(): Promise<unknown>;
|
|
52
89
|
private writeSnapshotFile;
|
|
53
90
|
private snapshot;
|
|
91
|
+
/** Shared: turn an ops/snapshot response envelope into an EngineSnapshot.
|
|
92
|
+
* Used by viewport/game snapshots and by scenePreview. */
|
|
93
|
+
private _parseSnapshotResponse;
|
|
94
|
+
/**
|
|
95
|
+
* Probe /api/snapshot/game for the structural 409 `bridge_required` shape
|
|
96
|
+
* (tool_net_thread.cpp:495-503). Returns a structured failure only when the
|
|
97
|
+
* engine actually answers 409 with `bridge_required` / `unsupported_transport`;
|
|
98
|
+
* ANY other status (200/202/other) returns null so the caller proceeds to the
|
|
99
|
+
* normal queued path — so the moment the engine starts answering over HTTP
|
|
100
|
+
* (P4.4), game capture just works with no code change here.
|
|
101
|
+
*/
|
|
102
|
+
private _detectBridgeRequired;
|
|
54
103
|
viewportSnapshot(): Promise<EngineSnapshot>;
|
|
55
104
|
gameSnapshot(): Promise<EngineSnapshot>;
|
|
105
|
+
/**
|
|
106
|
+
* Offscreen scene render via the `ScenePreview` op over /api/ops (no game
|
|
107
|
+
* boot; physics/animations are static). Mirrors the web previewScene op input
|
|
108
|
+
* (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
|
|
109
|
+
* node_path? } — scene_path optional, engine defaults to the open scene. The
|
|
110
|
+
* result carries image_base64 + mime (+ width/height) plus the P4.3 confession
|
|
111
|
+
* fields (scene_has_camera / scene_had_light / used_synthetic_camera).
|
|
112
|
+
*/
|
|
113
|
+
scenePreview(input?: {
|
|
114
|
+
scenePath?: string;
|
|
115
|
+
framing?: "auto" | "top" | "front" | "iso";
|
|
116
|
+
size?: [number, number];
|
|
117
|
+
nodePath?: string;
|
|
118
|
+
}): Promise<EngineSnapshot>;
|
|
56
119
|
getPort(): number;
|
|
57
120
|
/**
|
|
58
121
|
* Cheap drift probe for the MCP client cache. The engine mints a fresh
|
|
@@ -68,3 +131,4 @@ export declare class EngineApiClient {
|
|
|
68
131
|
*/
|
|
69
132
|
credentialsChanged(): Promise<boolean>;
|
|
70
133
|
}
|
|
134
|
+
export {};
|
package/dist/lib/api-client.js
CHANGED
|
@@ -14,6 +14,9 @@ function stringFrom(value) {
|
|
|
14
14
|
function numberFrom(value) {
|
|
15
15
|
return typeof value === "number" ? value : undefined;
|
|
16
16
|
}
|
|
17
|
+
function boolFrom(value) {
|
|
18
|
+
return typeof value === "boolean" ? value : undefined;
|
|
19
|
+
}
|
|
17
20
|
function findSnapshotPayload(value) {
|
|
18
21
|
const record = asRecord(value);
|
|
19
22
|
if (!record)
|
|
@@ -21,7 +24,8 @@ function findSnapshotPayload(value) {
|
|
|
21
24
|
if (typeof record.base64 === "string" ||
|
|
22
25
|
typeof record.image_base64 === "string" ||
|
|
23
26
|
record.op === "ViewportSnapshot" ||
|
|
24
|
-
record.op === "GameSnapshot"
|
|
27
|
+
record.op === "GameSnapshot" ||
|
|
28
|
+
record.op === "ScenePreview") {
|
|
25
29
|
return record;
|
|
26
30
|
}
|
|
27
31
|
if (Array.isArray(record.results)) {
|
|
@@ -63,9 +67,19 @@ function withoutImageData(payload) {
|
|
|
63
67
|
export class EngineApiClient {
|
|
64
68
|
port;
|
|
65
69
|
token;
|
|
66
|
-
|
|
70
|
+
// The engine and project this session is bound to. Every request carries the
|
|
71
|
+
// identity captured from health so the engine can reject requests after an
|
|
72
|
+
// in-place project switch or an unexpected instance change. The string form
|
|
73
|
+
// preserves the pre-2.6.6 constructor contract for existing callers that only
|
|
74
|
+
// supplied a projectIdHash.
|
|
75
|
+
targetIdentity;
|
|
76
|
+
constructor(port, token, targetIdentity = {}) {
|
|
67
77
|
this.port = port;
|
|
68
78
|
this.token = token;
|
|
79
|
+
this.targetIdentity =
|
|
80
|
+
typeof targetIdentity === "string"
|
|
81
|
+
? { projectIdHash: targetIdentity }
|
|
82
|
+
: { ...targetIdentity };
|
|
69
83
|
}
|
|
70
84
|
static async connect() {
|
|
71
85
|
const port = await getApiPort();
|
|
@@ -77,10 +91,62 @@ export class EngineApiClient {
|
|
|
77
91
|
if (!health) {
|
|
78
92
|
throw new Error(`Summer Engine is not responding on port ${port}. Make sure it's open.`);
|
|
79
93
|
}
|
|
80
|
-
|
|
94
|
+
// Bind to whatever project is open right now. On a genuine engine restart the
|
|
95
|
+
// token rotates and getClient() rebuilds this client, so a restart naturally
|
|
96
|
+
// rebinds to the current project. An in-place project switch keeps the same
|
|
97
|
+
// token, so the cached client retains its original binding and the engine
|
|
98
|
+
// rejects mismatched mutations until the agent explicitly rebinds.
|
|
99
|
+
return new EngineApiClient(port, token, {
|
|
100
|
+
instanceId: health.instanceId,
|
|
101
|
+
projectId: health.projectId,
|
|
102
|
+
projectIdHash: health.projectIdHash,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** The projectIdHash this session is bound to (undefined if none was reported
|
|
106
|
+
* at connect — e.g. no project open yet). */
|
|
107
|
+
getBoundProjectIdHash() {
|
|
108
|
+
return this.targetIdentity.projectIdHash;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Re-read health and rebind to the currently-open project. Called by
|
|
112
|
+
* summer_get_project_context so the agent can INTENTIONALLY follow a project
|
|
113
|
+
* switch (the deliberate escape hatch after an identity_mismatch). Returns the
|
|
114
|
+
* new bound hash. The direct health probe is intentionally unscoped, so it can
|
|
115
|
+
* reach the current engine even when bound requests would be rejected.
|
|
116
|
+
*/
|
|
117
|
+
async rebind() {
|
|
118
|
+
const health = await checkEngineHealth(this.port);
|
|
119
|
+
if (health) {
|
|
120
|
+
this.targetIdentity = {
|
|
121
|
+
instanceId: health.instanceId,
|
|
122
|
+
projectId: health.projectId,
|
|
123
|
+
projectIdHash: health.projectIdHash,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return this.targetIdentity.projectIdHash;
|
|
127
|
+
}
|
|
128
|
+
/** Identity to attach to a MUTATING command's options so the engine can reject
|
|
129
|
+
* a wrong-project write. Empty when unbound (engine then skips the check). */
|
|
130
|
+
identityOptions() {
|
|
131
|
+
const projectIdHash = this.targetIdentity.projectIdHash;
|
|
132
|
+
return projectIdHash ? { projectIdHash } : {};
|
|
133
|
+
}
|
|
134
|
+
targetUrl(path) {
|
|
135
|
+
const url = new URL(path, `http://127.0.0.1:${this.port}`);
|
|
136
|
+
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
137
|
+
if (instanceId)
|
|
138
|
+
url.searchParams.set("instanceId", instanceId);
|
|
139
|
+
if (projectId)
|
|
140
|
+
url.searchParams.set("projectId", projectId);
|
|
141
|
+
if (projectIdHash)
|
|
142
|
+
url.searchParams.set("projectIdHash", projectIdHash);
|
|
143
|
+
if (instanceId && projectId && projectIdHash) {
|
|
144
|
+
url.searchParams.set("projectIdentityVersion", "1");
|
|
145
|
+
}
|
|
146
|
+
return url.toString();
|
|
81
147
|
}
|
|
82
148
|
async request(method, path, body) {
|
|
83
|
-
const url =
|
|
149
|
+
const url = this.targetUrl(path);
|
|
84
150
|
const headers = {
|
|
85
151
|
Authorization: `Bearer ${this.token}`,
|
|
86
152
|
"Content-Type": "application/json",
|
|
@@ -100,7 +166,7 @@ export class EngineApiClient {
|
|
|
100
166
|
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
101
167
|
* the status (202 queued vs 200 legacy). */
|
|
102
168
|
async _fetchRaw(method, path, body, timeoutMs) {
|
|
103
|
-
return fetch(
|
|
169
|
+
return fetch(this.targetUrl(path), {
|
|
104
170
|
method,
|
|
105
171
|
headers: {
|
|
106
172
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -150,7 +216,11 @@ export class EngineApiClient {
|
|
|
150
216
|
// Ops may include long-running work (ImportFromUrlBatch, GitCommit); the
|
|
151
217
|
// engine keeps it "running" and we poll. 120s budget. Resolves legacy-200 or
|
|
152
218
|
// async-202 (Block E).
|
|
153
|
-
|
|
219
|
+
// Attach the bound project identity so the engine rejects a write aimed at a
|
|
220
|
+
// different project (identity_mismatch, atomic — before any op applies).
|
|
221
|
+
const merged = { ...this.identityOptions(), ...(options ?? {}) };
|
|
222
|
+
const body = Object.keys(merged).length ? { ops, options: merged } : { ops };
|
|
223
|
+
return this._requestQueued("POST", "/api/ops", body, 120_000);
|
|
154
224
|
}
|
|
155
225
|
async getSceneState() {
|
|
156
226
|
return this.request("GET", "/api/state/scene");
|
|
@@ -174,11 +244,14 @@ export class EngineApiClient {
|
|
|
174
244
|
// Cold-load on large projects can take 25-40s. 60s budget.
|
|
175
245
|
// The engine reads play params from body.options (tool_net_thread.cpp:503),
|
|
176
246
|
// and the play handler reads options["scene"] — a top-level { scene } is
|
|
177
|
-
// dropped, so the scene MUST be nested inside options.
|
|
178
|
-
|
|
247
|
+
// dropped, so the scene MUST be nested inside options. The bound identity
|
|
248
|
+
// rides in the same options dict so play is refused on a mismatched project.
|
|
249
|
+
const options = { ...this.identityOptions(), ...(scene ? { scene } : {}) };
|
|
250
|
+
return this._requestQueued("POST", "/api/play", Object.keys(options).length ? { options } : {}, 60_000);
|
|
179
251
|
}
|
|
180
252
|
async stop() {
|
|
181
|
-
|
|
253
|
+
const options = this.identityOptions();
|
|
254
|
+
return this._requestQueued("POST", "/api/stop", Object.keys(options).length ? { options } : undefined, 15_000);
|
|
182
255
|
}
|
|
183
256
|
async readFile(path, maxBytes) {
|
|
184
257
|
const params = new URLSearchParams({ path });
|
|
@@ -211,6 +284,18 @@ export class EngineApiClient {
|
|
|
211
284
|
// The terminal result is the apply dict; the image rides as base64 inside it
|
|
212
285
|
// (no raw-binary channel). Legacy/dormant engines answer 200 synchronously
|
|
213
286
|
// with the same payload shape — _requestQueued resolves both.
|
|
287
|
+
//
|
|
288
|
+
// Game capture over local HTTP structurally 409s today with a STRUCTURED
|
|
289
|
+
// reason (`failure_reason:"unsupported_transport"`, `bridge_required:true`,
|
|
290
|
+
// tool_net_thread.cpp:495-503). Detect that specific shape and return it
|
|
291
|
+
// verbatim so the tool can give an honest message — do NOT hardcode "game
|
|
292
|
+
// always fails": once the engine answers 200/202 (P4.4), the normal path
|
|
293
|
+
// below just works.
|
|
294
|
+
if (kind === "game") {
|
|
295
|
+
const bridge = await this._detectBridgeRequired(`/api/snapshot/${kind}`);
|
|
296
|
+
if (bridge)
|
|
297
|
+
return bridge;
|
|
298
|
+
}
|
|
214
299
|
let response;
|
|
215
300
|
try {
|
|
216
301
|
response = await this._requestQueued("GET", `/api/snapshot/${kind}`, undefined, 60_000);
|
|
@@ -218,11 +303,16 @@ export class EngineApiClient {
|
|
|
218
303
|
catch (err) {
|
|
219
304
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
220
305
|
}
|
|
306
|
+
return this._parseSnapshotResponse(response, kind);
|
|
307
|
+
}
|
|
308
|
+
/** Shared: turn an ops/snapshot response envelope into an EngineSnapshot.
|
|
309
|
+
* Used by viewport/game snapshots and by scenePreview. */
|
|
310
|
+
async _parseSnapshotResponse(response, kind) {
|
|
221
311
|
const payload = findSnapshotPayload(response);
|
|
222
312
|
if (!payload) {
|
|
223
313
|
return {
|
|
224
314
|
ok: false,
|
|
225
|
-
error: "Snapshot response did not include
|
|
315
|
+
error: "Snapshot response did not include an image payload.",
|
|
226
316
|
};
|
|
227
317
|
}
|
|
228
318
|
const base64 = stringFrom(payload.base64) ?? stringFrom(payload.image_base64);
|
|
@@ -230,12 +320,34 @@ export class EngineApiClient {
|
|
|
230
320
|
return {
|
|
231
321
|
ok: false,
|
|
232
322
|
error: stringFrom(payload.error) ?? "Snapshot response did not include image data.",
|
|
323
|
+
failureReason: stringFrom(payload.failure_reason),
|
|
324
|
+
sceneHasCamera: boolFrom(payload.scene_has_camera),
|
|
325
|
+
sceneHadLight: boolFrom(payload.scene_had_light),
|
|
326
|
+
usedSyntheticCamera: boolFrom(payload.used_synthetic_camera),
|
|
233
327
|
metadata: withoutImageData(payload),
|
|
234
328
|
};
|
|
235
329
|
}
|
|
236
330
|
const { format, mime, ext } = snapshotFormat(payload);
|
|
237
331
|
const buffer = Buffer.from(base64, "base64");
|
|
238
332
|
const localPath = await this.writeSnapshotFile(kind, buffer, ext);
|
|
333
|
+
// Client-side identity drift check (item 4). Current engines validate the
|
|
334
|
+
// identity query on snapshot reads, but older builds may ignore it. Re-read
|
|
335
|
+
// health and compare to the bound hash so a frame from a switched project is
|
|
336
|
+
// still marked as suspect. Best-effort: a failed health read never blocks the
|
|
337
|
+
// already captured image.
|
|
338
|
+
let projectMismatch;
|
|
339
|
+
const boundProjectIdHash = this.targetIdentity.projectIdHash;
|
|
340
|
+
if (boundProjectIdHash) {
|
|
341
|
+
try {
|
|
342
|
+
const health = await checkEngineHealth(this.port);
|
|
343
|
+
if (health?.projectIdHash && health.projectIdHash !== boundProjectIdHash) {
|
|
344
|
+
projectMismatch = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// ignore — never fail a capture on a transient health read
|
|
349
|
+
}
|
|
350
|
+
}
|
|
239
351
|
return {
|
|
240
352
|
ok: payload.ok !== false,
|
|
241
353
|
localPath,
|
|
@@ -249,15 +361,86 @@ export class EngineApiClient {
|
|
|
249
361
|
op: stringFrom(payload.op),
|
|
250
362
|
provenance: payload.provenance,
|
|
251
363
|
bytes: buffer.byteLength,
|
|
364
|
+
sceneHasCamera: boolFrom(payload.scene_has_camera),
|
|
365
|
+
sceneHadLight: boolFrom(payload.scene_had_light),
|
|
366
|
+
usedSyntheticCamera: boolFrom(payload.used_synthetic_camera),
|
|
367
|
+
projectMismatch,
|
|
252
368
|
metadata: withoutImageData(payload),
|
|
253
369
|
};
|
|
254
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Probe /api/snapshot/game for the structural 409 `bridge_required` shape
|
|
373
|
+
* (tool_net_thread.cpp:495-503). Returns a structured failure only when the
|
|
374
|
+
* engine actually answers 409 with `bridge_required` / `unsupported_transport`;
|
|
375
|
+
* ANY other status (200/202/other) returns null so the caller proceeds to the
|
|
376
|
+
* normal queued path — so the moment the engine starts answering over HTTP
|
|
377
|
+
* (P4.4), game capture just works with no code change here.
|
|
378
|
+
*/
|
|
379
|
+
async _detectBridgeRequired(path) {
|
|
380
|
+
let res;
|
|
381
|
+
try {
|
|
382
|
+
res = await this._fetchRaw("GET", path, undefined, 15_000);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Transport error — let the normal path surface it uniformly.
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (res.status !== 409)
|
|
389
|
+
return null;
|
|
390
|
+
const body = (await res.json().catch(() => ({})));
|
|
391
|
+
const reason = stringFrom(body.failure_reason);
|
|
392
|
+
const bridge = body.bridge_required === true;
|
|
393
|
+
if (!bridge && reason !== "unsupported_transport") {
|
|
394
|
+
// A 409 we don't recognize — surface it as a plain error rather than
|
|
395
|
+
// silently claiming "bridge required".
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
error: stringFrom(body.error) ?? "Engine returned 409 for game snapshot.",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
failureReason: reason ?? "bridge_required",
|
|
404
|
+
error: stringFrom(body.error) ?? "Game snapshots require the desktop bridge async transport.",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
255
407
|
async viewportSnapshot() {
|
|
256
408
|
return this.snapshot("viewport");
|
|
257
409
|
}
|
|
258
410
|
async gameSnapshot() {
|
|
259
411
|
return this.snapshot("game");
|
|
260
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Offscreen scene render via the `ScenePreview` op over /api/ops (no game
|
|
415
|
+
* boot; physics/animations are static). Mirrors the web previewScene op input
|
|
416
|
+
* (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
|
|
417
|
+
* node_path? } — scene_path optional, engine defaults to the open scene. The
|
|
418
|
+
* result carries image_base64 + mime (+ width/height) plus the P4.3 confession
|
|
419
|
+
* fields (scene_has_camera / scene_had_light / used_synthetic_camera).
|
|
420
|
+
*/
|
|
421
|
+
async scenePreview(input) {
|
|
422
|
+
const opInput = { op: "ScenePreview" };
|
|
423
|
+
const trimmed = input?.scenePath?.trim();
|
|
424
|
+
if (trimmed && trimmed !== "." && trimmed !== "./")
|
|
425
|
+
opInput.scene_path = trimmed;
|
|
426
|
+
if (input?.framing)
|
|
427
|
+
opInput.framing = input.framing;
|
|
428
|
+
if (input?.size)
|
|
429
|
+
opInput.size = input.size;
|
|
430
|
+
if (input?.nodePath)
|
|
431
|
+
opInput.node_path = input.nodePath;
|
|
432
|
+
let response;
|
|
433
|
+
try {
|
|
434
|
+
// executeOps stamps the bound identity so a drifted project is rejected
|
|
435
|
+
// (identity_mismatch) before rendering — ScenePreview reads no game state
|
|
436
|
+
// but still targets a project's resources.
|
|
437
|
+
response = await this.executeOps([opInput]);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
441
|
+
}
|
|
442
|
+
return this._parseSnapshotResponse(response, "scene");
|
|
443
|
+
}
|
|
261
444
|
getPort() {
|
|
262
445
|
return this.port;
|
|
263
446
|
}
|
|
@@ -50,7 +50,15 @@ async function connectForProject(projectRoot) {
|
|
|
50
50
|
// rescan; the executor rejects mismatches too, but skip the round trip.
|
|
51
51
|
if (health.projectIdHash && health.projectIdHash !== projectIdHash)
|
|
52
52
|
return null;
|
|
53
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
client: new EngineApiClient(port, token, {
|
|
55
|
+
instanceId: health.instanceId,
|
|
56
|
+
projectId: health.projectId,
|
|
57
|
+
projectIdHash,
|
|
58
|
+
}),
|
|
59
|
+
projectIdHash,
|
|
60
|
+
scene: health.scene,
|
|
61
|
+
};
|
|
54
62
|
}
|
|
55
63
|
catch {
|
|
56
64
|
return null;
|