summer-engine 2.6.1 → 2.6.5
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 +20 -0
- package/GEMINI.md +2 -1
- package/README.md +61 -9
- package/dist/bin/summer.js +1 -1
- package/dist/commands/setup.js +9 -1
- package/dist/commands/skills.js +15 -5
- package/dist/lib/agent-config.d.ts +1 -1
- package/dist/lib/agent-config.js +53 -15
- package/dist/lib/api-client.d.ts +75 -1
- package/dist/lib/api-client.js +191 -8
- package/dist/lib/game-task-plan.js +6 -2
- package/dist/lib/skills-registry.d.ts +66 -66
- package/dist/lib/skills-registry.js +1 -0
- package/dist/mcp/server.js +63 -18
- 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.d.ts +9 -0
- package/dist/mcp/tools/visual-tools.js +126 -0
- package/dist/mcp/tools/with-engine.d.ts +52 -5
- package/dist/mcp/tools/with-engine.js +117 -15
- package/package.json +1 -1
|
@@ -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,8 +82,35 @@ export function extractOpError(result) {
|
|
|
56
82
|
}
|
|
57
83
|
return null;
|
|
58
84
|
}
|
|
85
|
+
/**
|
|
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).
|
|
99
|
+
*
|
|
100
|
+
* Exported for unit tests.
|
|
101
|
+
*/
|
|
102
|
+
export function isAuthError(err) {
|
|
103
|
+
const m = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
104
|
+
return m.includes("401") || m.includes("403") || m.includes("unauthorized");
|
|
105
|
+
}
|
|
59
106
|
function buildActionHint(message) {
|
|
60
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
|
+
}
|
|
61
114
|
if (normalized.includes("no scene open") || normalized.includes("no edited scene")) {
|
|
62
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).";
|
|
63
116
|
}
|
|
@@ -69,24 +122,73 @@ function buildActionHint(message) {
|
|
|
69
122
|
}
|
|
70
123
|
return null;
|
|
71
124
|
}
|
|
72
|
-
export async function withEngine(fn) {
|
|
125
|
+
export async function withEngine(fn, opts) {
|
|
73
126
|
// Best-effort, fire-and-forget: count this MCP session as DAU for attribution.
|
|
74
127
|
// No await, no throw, no quota gating.
|
|
75
128
|
recordMcpSession();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
140
|
+
const MAX_ATTEMPTS = 2;
|
|
141
|
+
let lastError;
|
|
142
|
+
let retried = false;
|
|
143
|
+
let boundProjectIdHash;
|
|
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;
|
|
163
|
+
try {
|
|
164
|
+
const result = await fn(client);
|
|
165
|
+
const opError = extractOpError(result);
|
|
166
|
+
if (opError) {
|
|
167
|
+
const hint = buildActionHint(opError);
|
|
168
|
+
const message = hint ? `${opError}\n\nHint: ${hint}` : opError;
|
|
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);
|
|
176
|
+
}
|
|
177
|
+
if (opts?.toContent) {
|
|
178
|
+
return attachMeta({ content: opts.toContent(result) }, meta);
|
|
179
|
+
}
|
|
180
|
+
return attachMeta({ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }, meta);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
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.
|
|
185
|
+
resetClient();
|
|
186
|
+
lastError = err;
|
|
187
|
+
if (attempt < MAX_ATTEMPTS && isAuthError(err))
|
|
188
|
+
continue;
|
|
189
|
+
break;
|
|
84
190
|
}
|
|
85
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
resetClient();
|
|
89
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
-
return { content: [{ type: "text", text: msg }], isError: true };
|
|
91
191
|
}
|
|
192
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
193
|
+
return attachMeta({ content: [{ type: "text", text: msg }], isError: true }, { errorClass: "transport", retried, boundProjectIdHash });
|
|
92
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer-engine",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.5",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"summer-engine",
|