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
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { getClient, resetClient } from "../server.js";
|
|
2
2
|
import { recordMcpSession } from "../../lib/telemetry.js";
|
|
3
|
+
export const WITH_ENGINE_META = Symbol("summerWithEngineMeta");
|
|
4
|
+
function attachMeta(result, meta) {
|
|
5
|
+
Object.defineProperty(result, WITH_ENGINE_META, {
|
|
6
|
+
value: meta,
|
|
7
|
+
enumerable: false,
|
|
8
|
+
configurable: true,
|
|
9
|
+
});
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
3
12
|
// 0.5.34 Block E contract (publicsummerengine src/lib/tools/contract.ts §0.1).
|
|
4
13
|
// The async lifecycle (async-op-lifecycle.ts pollOpToTerminal) merges
|
|
5
14
|
// terminalState/errorClass onto the apply dict it returns. ONLY these two are
|
|
@@ -17,6 +26,23 @@ const TERMINAL_STATE_MESSAGES = {
|
|
|
17
26
|
denied: "Operation denied (terminalState: denied). Nothing was applied.",
|
|
18
27
|
canceled: "Operation canceled (terminalState: canceled). Nothing was applied.",
|
|
19
28
|
};
|
|
29
|
+
/** Pull the failure classifiers off an engine envelope for logging (best-effort,
|
|
30
|
+
* shape-tolerant). Does not decide success/failure — that stays in
|
|
31
|
+
* extractOpError. */
|
|
32
|
+
function readClassifiers(result) {
|
|
33
|
+
if (!result || typeof result !== "object")
|
|
34
|
+
return {};
|
|
35
|
+
const op = result;
|
|
36
|
+
return {
|
|
37
|
+
terminalState: typeof op.terminalState === "string" ? op.terminalState : undefined,
|
|
38
|
+
errorClass: typeof op.errorClass === "string" ? op.errorClass : undefined,
|
|
39
|
+
failureReason: typeof op.failureReason === "string"
|
|
40
|
+
? op.failureReason
|
|
41
|
+
: typeof op.failure_reason === "string"
|
|
42
|
+
? op.failure_reason
|
|
43
|
+
: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
20
46
|
/**
|
|
21
47
|
* Decide whether an engine result envelope represents a FAILURE, and if so
|
|
22
48
|
* return a model-visible message. Returns null only for genuine success.
|
|
@@ -56,45 +82,35 @@ export function extractOpError(result) {
|
|
|
56
82
|
}
|
|
57
83
|
return null;
|
|
58
84
|
}
|
|
59
|
-
// Terminal states where the engine GUARANTEES the op never landed, so dropping
|
|
60
|
-
// the cached client and retrying once is safe (no double-mutation). Everything
|
|
61
|
-
// else — timed_out (may still be running), content_mismatch / denied / canceled
|
|
62
|
-
// (intentional) — must surface, never silently retry.
|
|
63
|
-
const RECONNECTABLE_TERMINAL_STATES = new Set([
|
|
64
|
-
"not_connected",
|
|
65
|
-
"identity_mismatch",
|
|
66
|
-
]);
|
|
67
|
-
function terminalStateOf(result) {
|
|
68
|
-
if (!result || typeof result !== "object")
|
|
69
|
-
return null;
|
|
70
|
-
const ts = result.terminalState;
|
|
71
|
-
return typeof ts === "string" && ts.length > 0 ? ts : null;
|
|
72
|
-
}
|
|
73
85
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
86
|
+
* An auth failure is the ONE class of thrown error that is provably pre-apply:
|
|
87
|
+
* the engine rejects on the Bearer-token check (tool_net_thread.cpp::_validate_auth)
|
|
88
|
+
* BEFORE the op is queued or applied, so reconnecting with the fresh api-token and
|
|
89
|
+
* retrying cannot double-apply a mutation. This is exactly the stale-token case
|
|
90
|
+
* after the engine rotates its api-token on relaunch.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately NARROW. A generic connection error (ECONNREFUSED / "fetch failed")
|
|
93
|
+
* thrown from inside the tool closure is NOT retriable here, because it may be a
|
|
94
|
+
* disconnect mid-operation where a mutation already partially applied — and the
|
|
95
|
+
* MCP sends no idempotency key the engine can dedup on, so a blind retry would
|
|
96
|
+
* re-apply it. The restart-between-calls case is handled proactively by
|
|
97
|
+
* getClient()'s credential-drift check; the connect/preflight-failure case is
|
|
98
|
+
* retried separately in withEngine (nothing is submitted there).
|
|
79
99
|
*
|
|
80
100
|
* Exported for unit tests.
|
|
81
101
|
*/
|
|
82
|
-
export function
|
|
102
|
+
export function isAuthError(err) {
|
|
83
103
|
const m = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
84
|
-
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
return (m.includes("401") ||
|
|
88
|
-
m.includes("403") ||
|
|
89
|
-
m.includes("unauthorized") ||
|
|
90
|
-
m.includes("econnrefused") ||
|
|
91
|
-
m.includes("econnreset") ||
|
|
92
|
-
m.includes("fetch failed") ||
|
|
93
|
-
m.includes("not running") ||
|
|
94
|
-
m.includes("not responding"));
|
|
104
|
+
return m.includes("401") || m.includes("403") || m.includes("unauthorized");
|
|
95
105
|
}
|
|
96
106
|
function buildActionHint(message) {
|
|
97
107
|
const normalized = message.toLowerCase();
|
|
108
|
+
if (normalized.includes("identity_mismatch") ||
|
|
109
|
+
normalized.includes("projectidhash mismatch") ||
|
|
110
|
+
normalized.includes("projectid mismatch") ||
|
|
111
|
+
normalized.includes("wrong project/instance")) {
|
|
112
|
+
return "Summer Engine is now on a DIFFERENT project than this session is bound to — nothing was applied (your edit did NOT land in the wrong project). If you meant to work on the now-open project, call `summer_get_project_context` to rebind to it, then retry. If not, switch the engine back to the original project first.";
|
|
113
|
+
}
|
|
98
114
|
if (normalized.includes("no scene open") || normalized.includes("no edited scene")) {
|
|
99
115
|
return "No scene is currently open. Call `summer_get_project_context` first, then `summer_open_main_scene` (or `summer_open_scene` with a known .tscn path).";
|
|
100
116
|
}
|
|
@@ -110,48 +126,69 @@ export async function withEngine(fn, opts) {
|
|
|
110
126
|
// Best-effort, fire-and-forget: count this MCP session as DAU for attribution.
|
|
111
127
|
// No await, no throw, no quota gating.
|
|
112
128
|
recordMcpSession();
|
|
113
|
-
// The engine rotates its api-token and can move ports on every launch
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
129
|
+
// The engine rotates its api-token (and can move ports) on every launch. The
|
|
130
|
+
// proactive credential-drift check in getClient() reconnects when a restart
|
|
131
|
+
// happened BETWEEN calls; this loop only covers the race where the restart
|
|
132
|
+
// lands DURING a call. We retry ONLY where nothing could have been applied:
|
|
133
|
+
// - a connect/preflight failure (getClient threw — request never submitted)
|
|
134
|
+
// - a stale-token 401/403 (engine rejects at auth, before queue/apply)
|
|
135
|
+
// A generic connection error thrown from inside fn() is NOT retried: it may be
|
|
136
|
+
// a disconnect mid-operation where a mutation already partially applied, and
|
|
137
|
+
// the MCP sends no idempotency key the engine can dedup on. A soft failure
|
|
138
|
+
// terminal state (e.g. identity_mismatch from a project switch) is surfaced,
|
|
139
|
+
// never silently retried — a retry cannot fix a project switch.
|
|
120
140
|
const MAX_ATTEMPTS = 2;
|
|
121
141
|
let lastError;
|
|
142
|
+
let retried = false;
|
|
143
|
+
let boundProjectIdHash;
|
|
122
144
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
145
|
+
if (attempt > 1)
|
|
146
|
+
retried = true;
|
|
147
|
+
let client;
|
|
148
|
+
try {
|
|
149
|
+
client = await getClient();
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
// Pre-submission: the request never went out, so reconnect and retry.
|
|
153
|
+
resetClient();
|
|
154
|
+
lastError = err;
|
|
155
|
+
if (attempt < MAX_ATTEMPTS)
|
|
156
|
+
continue;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
boundProjectIdHash =
|
|
160
|
+
typeof client.getBoundProjectIdHash === "function"
|
|
161
|
+
? client.getBoundProjectIdHash()
|
|
162
|
+
: undefined;
|
|
123
163
|
try {
|
|
124
|
-
const client = await getClient();
|
|
125
164
|
const result = await fn(client);
|
|
126
165
|
const opError = extractOpError(result);
|
|
127
166
|
if (opError) {
|
|
128
|
-
const ts = terminalStateOf(result);
|
|
129
|
-
if (ts && RECONNECTABLE_TERMINAL_STATES.has(ts) && attempt < MAX_ATTEMPTS) {
|
|
130
|
-
resetClient();
|
|
131
|
-
lastError = new Error(opError);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
167
|
const hint = buildActionHint(opError);
|
|
135
168
|
const message = hint ? `${opError}\n\nHint: ${hint}` : opError;
|
|
136
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
169
|
+
return attachMeta({ content: [{ type: "text", text: message }], isError: true }, { ...readClassifiers(result), retried, boundProjectIdHash });
|
|
170
|
+
}
|
|
171
|
+
const meta = { ...readClassifiers(result), retried, boundProjectIdHash };
|
|
172
|
+
if (opts?.onResult) {
|
|
173
|
+
const short = opts.onResult(result);
|
|
174
|
+
if (short)
|
|
175
|
+
return attachMeta(short, meta);
|
|
137
176
|
}
|
|
138
177
|
if (opts?.toContent) {
|
|
139
|
-
return { content: opts.toContent(result) };
|
|
178
|
+
return attachMeta({ content: opts.toContent(result) }, meta);
|
|
140
179
|
}
|
|
141
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
180
|
+
return attachMeta({ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }, meta);
|
|
142
181
|
}
|
|
143
182
|
catch (err) {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// connection-class throws that provably did not mutate.
|
|
183
|
+
// Drop the cached client (it may point at a dead/rotated engine). Retry
|
|
184
|
+
// once only for a provably pre-apply auth failure; anything else surfaces.
|
|
147
185
|
resetClient();
|
|
148
186
|
lastError = err;
|
|
149
|
-
if (attempt < MAX_ATTEMPTS &&
|
|
187
|
+
if (attempt < MAX_ATTEMPTS && isAuthError(err))
|
|
150
188
|
continue;
|
|
151
|
-
}
|
|
152
189
|
break;
|
|
153
190
|
}
|
|
154
191
|
}
|
|
155
192
|
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
156
|
-
return { content: [{ type: "text", text: msg }], isError: true };
|
|
193
|
+
return attachMeta({ content: [{ type: "text", text: msg }], isError: true }, { errorClass: "transport", retried, boundProjectIdHash });
|
|
157
194
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer-engine",
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"description": "Summer CLI and MCP server for Summer Engine
|
|
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",
|