summer-engine 2.6.5 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
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
+
5
16
  ## [2.6.5] — 2026-07-04 — "Cloud tools don't need the engine"
6
17
 
7
18
  ### Fixed
package/README.md CHANGED
@@ -23,22 +23,6 @@ Three names, one npm package (`summer-engine`):
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:
@@ -556,6 +540,24 @@ Skills evolve fast. Two ways to help:
556
540
 
557
541
  ---
558
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
+
559
561
  ## License
560
562
 
561
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).
@@ -33,11 +33,16 @@ export type EngineSnapshot = {
33
33
  * (item 4, client-side drift check). */
34
34
  projectMismatch?: boolean;
35
35
  };
36
+ type EngineTargetIdentity = {
37
+ instanceId?: string;
38
+ projectId?: string;
39
+ projectIdHash?: string;
40
+ };
36
41
  export declare class EngineApiClient {
37
42
  private port;
38
43
  private token;
39
- private boundProjectIdHash?;
40
- constructor(port: number, token: string, boundProjectIdHash?: string);
44
+ private targetIdentity;
45
+ constructor(port: number, token: string, targetIdentity?: EngineTargetIdentity | string);
41
46
  static connect(): Promise<EngineApiClient>;
42
47
  /** The projectIdHash this session is bound to (undefined if none was reported
43
48
  * at connect — e.g. no project open yet). */
@@ -46,13 +51,14 @@ export declare class EngineApiClient {
46
51
  * Re-read health and rebind to the currently-open project. Called by
47
52
  * summer_get_project_context so the agent can INTENTIONALLY follow a project
48
53
  * switch (the deliberate escape hatch after an identity_mismatch). Returns the
49
- * new bound hash. Reads carry no identity, so this always reaches the engine
50
- * even when a mutation would be rejected.
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.
51
56
  */
52
57
  rebind(): Promise<string | undefined>;
53
58
  /** Identity to attach to a MUTATING command's options so the engine can reject
54
59
  * a wrong-project write. Empty when unbound (engine then skips the check). */
55
60
  private identityOptions;
61
+ private targetUrl;
56
62
  private request;
57
63
  /** Raw fetch with auth + timeout — returns the Response so callers can inspect
58
64
  * the status (202 queued vs 200 legacy). */
@@ -125,3 +131,4 @@ export declare class EngineApiClient {
125
131
  */
126
132
  credentialsChanged(): Promise<boolean>;
127
133
  }
134
+ export {};
@@ -67,17 +67,19 @@ function withoutImageData(payload) {
67
67
  export class EngineApiClient {
68
68
  port;
69
69
  token;
70
- // The project this session is BOUND to — the projectIdHash reported by the
71
- // engine when this client first connected. Sent on every mutating op so the
72
- // engine's identity guard (local_api_server.cpp ~271-302) atomically rejects a
73
- // write aimed at a DIFFERENT project (e.g. after the user switched projects
74
- // in-place). Health is the only identity signal the MCP has — /api/state/project
75
- // carries no path and /api/health exposes only projectIdHash, not the raw id.
76
- boundProjectIdHash;
77
- constructor(port, token, boundProjectIdHash) {
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 = {}) {
78
77
  this.port = port;
79
78
  this.token = token;
80
- this.boundProjectIdHash = boundProjectIdHash;
79
+ this.targetIdentity =
80
+ typeof targetIdentity === "string"
81
+ ? { projectIdHash: targetIdentity }
82
+ : { ...targetIdentity };
81
83
  }
82
84
  static async connect() {
83
85
  const port = await getApiPort();
@@ -94,34 +96,57 @@ export class EngineApiClient {
94
96
  // rebinds to the current project. An in-place project switch keeps the same
95
97
  // token, so the cached client retains its original binding and the engine
96
98
  // rejects mismatched mutations until the agent explicitly rebinds.
97
- return new EngineApiClient(port, token, health.projectIdHash);
99
+ return new EngineApiClient(port, token, {
100
+ instanceId: health.instanceId,
101
+ projectId: health.projectId,
102
+ projectIdHash: health.projectIdHash,
103
+ });
98
104
  }
99
105
  /** The projectIdHash this session is bound to (undefined if none was reported
100
106
  * at connect — e.g. no project open yet). */
101
107
  getBoundProjectIdHash() {
102
- return this.boundProjectIdHash;
108
+ return this.targetIdentity.projectIdHash;
103
109
  }
104
110
  /**
105
111
  * Re-read health and rebind to the currently-open project. Called by
106
112
  * summer_get_project_context so the agent can INTENTIONALLY follow a project
107
113
  * switch (the deliberate escape hatch after an identity_mismatch). Returns the
108
- * new bound hash. Reads carry no identity, so this always reaches the engine
109
- * even when a mutation would be rejected.
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.
110
116
  */
111
117
  async rebind() {
112
118
  const health = await checkEngineHealth(this.port);
113
119
  if (health) {
114
- this.boundProjectIdHash = health.projectIdHash;
120
+ this.targetIdentity = {
121
+ instanceId: health.instanceId,
122
+ projectId: health.projectId,
123
+ projectIdHash: health.projectIdHash,
124
+ };
115
125
  }
116
- return this.boundProjectIdHash;
126
+ return this.targetIdentity.projectIdHash;
117
127
  }
118
128
  /** Identity to attach to a MUTATING command's options so the engine can reject
119
129
  * a wrong-project write. Empty when unbound (engine then skips the check). */
120
130
  identityOptions() {
121
- return this.boundProjectIdHash ? { projectIdHash: this.boundProjectIdHash } : {};
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();
122
147
  }
123
148
  async request(method, path, body) {
124
- const url = `http://127.0.0.1:${this.port}${path}`;
149
+ const url = this.targetUrl(path);
125
150
  const headers = {
126
151
  Authorization: `Bearer ${this.token}`,
127
152
  "Content-Type": "application/json",
@@ -141,7 +166,7 @@ export class EngineApiClient {
141
166
  /** Raw fetch with auth + timeout — returns the Response so callers can inspect
142
167
  * the status (202 queued vs 200 legacy). */
143
168
  async _fetchRaw(method, path, body, timeoutMs) {
144
- return fetch(`http://127.0.0.1:${this.port}${path}`, {
169
+ return fetch(this.targetUrl(path), {
145
170
  method,
146
171
  headers: {
147
172
  Authorization: `Bearer ${this.token}`,
@@ -305,18 +330,17 @@ export class EngineApiClient {
305
330
  const { format, mime, ext } = snapshotFormat(payload);
306
331
  const buffer = Buffer.from(base64, "base64");
307
332
  const localPath = await this.writeSnapshotFile(kind, buffer, ext);
308
- // Client-side identity drift check (item 4). The engine's identity guard only
309
- // acts on options.projectIdHash inside a queued command; a GET snapshot from
310
- // `fetch` carries no reliable body, so we cannot count on the engine to reject
311
- // a drifted read. Re-read health and compare to the bound hash — if the engine
312
- // has switched projects since we bound, this frame may be from the WRONG
313
- // project. Best-effort: a failed health read never blocks the (already
314
- // captured) image.
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.
315
338
  let projectMismatch;
316
- if (this.boundProjectIdHash) {
339
+ const boundProjectIdHash = this.targetIdentity.projectIdHash;
340
+ if (boundProjectIdHash) {
317
341
  try {
318
342
  const health = await checkEngineHealth(this.port);
319
- if (health?.projectIdHash && health.projectIdHash !== this.boundProjectIdHash) {
343
+ if (health?.projectIdHash && health.projectIdHash !== boundProjectIdHash) {
320
344
  projectMismatch = true;
321
345
  }
322
346
  }
@@ -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 { client: new EngineApiClient(port, token), projectIdHash, scene: health.scene };
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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "summer-engine",
3
- "version": "2.6.5",
4
- "description": "Summer CLI and MCP server for Summer Engine, the AI game engine. Install the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games. Skills, hooks, and plugins included.",
3
+ "version": "2.6.6",
4
+ "description": "Local Summer CLI and MCP server for Summer Engine. Install and run the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games with bundled skills, hooks, and plugins.",
5
5
  "keywords": [
6
6
  "summer-engine",
7
7
  "summer-cli",
@@ -27,6 +27,10 @@
27
27
  "type": "git",
28
28
  "url": "git+https://github.com/SummerEngine/summer-engine-agent.git"
29
29
  },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org"
33
+ },
30
34
  "license": "MIT",
31
35
  "author": "Summer Engine <founders@summerengine.com>",
32
36
  "type": "module",