summer-engine 2.7.0 → 2.8.0
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 +2 -1
- package/.codex-plugin/plugin.json +3 -2
- package/.cursor-plugin/plugin.json +2 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +10 -0
- package/GEMINI.md +5 -4
- package/README.md +58 -19
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/dist/bin/summer.js +8 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +123 -36
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/lib/api-client.d.ts +4 -2
- package/dist/lib/api-client.js +28 -4
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +8 -0
- package/dist/lib/skills-registry.js +9 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +8 -1
- package/dist/mcp/server.js +25 -4
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/generate-tools.js +16 -6
- package/dist/mcp/tools/project-tools.js +12 -7
- package/dist/mcp/tools/scene-tools.js +6 -6
- package/dist/mcp/tools/visual-tools.js +4 -3
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +52 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +56 -23
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +3 -3
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +12 -2
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- package/skills/workflow/writing-skills/render-graphs.js +0 -0
- package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
package/dist/lib/engine.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { lstat, readFile, readdir, realpath, stat } from "fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "path";
|
|
3
3
|
import { getSummerDir } from "./auth.js";
|
|
4
4
|
const DEFAULT_PORT = 6550;
|
|
5
|
-
|
|
5
|
+
const INSTANCE_SCHEMA_VERSION = 1;
|
|
6
|
+
const INSTANCE_STALE_MS = 180_000;
|
|
7
|
+
const MAX_INSTANCE_FILE_BYTES = 64 * 1024;
|
|
8
|
+
export async function getApiToken(summerDir = getSummerDir()) {
|
|
6
9
|
try {
|
|
7
|
-
const token = await readFile(join(
|
|
10
|
+
const token = await readFile(join(summerDir, "api-token"), "utf-8");
|
|
8
11
|
return token.trim() || null;
|
|
9
12
|
}
|
|
10
13
|
catch {
|
|
11
14
|
return null;
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
|
-
export async function getApiPort() {
|
|
17
|
+
export async function getApiPort(summerDir = getSummerDir()) {
|
|
15
18
|
try {
|
|
16
|
-
const port = await readFile(join(
|
|
19
|
+
const port = await readFile(join(summerDir, "api-port"), "utf-8");
|
|
17
20
|
const parsed = parseInt(port.trim(), 10);
|
|
18
21
|
return isNaN(parsed) ? DEFAULT_PORT : parsed;
|
|
19
22
|
}
|
|
@@ -21,6 +24,237 @@ export async function getApiPort() {
|
|
|
21
24
|
return DEFAULT_PORT;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
27
|
+
function stringFromUnknown(value) {
|
|
28
|
+
return typeof value === "string" && value.trim().length > 0
|
|
29
|
+
? value.trim()
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
function numberFromUnknown(value) {
|
|
33
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
34
|
+
? value
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
37
|
+
function processIsAlive(pid) {
|
|
38
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid, 0);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return error.code === "EPERM";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function canonicalPath(path) {
|
|
49
|
+
const absolute = resolve(path);
|
|
50
|
+
try {
|
|
51
|
+
return await realpath(absolute);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return absolute;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function findProjectRoot(startPath) {
|
|
58
|
+
let current = await canonicalPath(startPath);
|
|
59
|
+
try {
|
|
60
|
+
if (!(await stat(current)).isDirectory()) {
|
|
61
|
+
current = dirname(current);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
while (true) {
|
|
68
|
+
try {
|
|
69
|
+
if ((await stat(join(current, "project.godot"))).isFile()) {
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Keep walking toward the filesystem root.
|
|
75
|
+
}
|
|
76
|
+
const parent = dirname(current);
|
|
77
|
+
if (parent === current)
|
|
78
|
+
return null;
|
|
79
|
+
current = parent;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseEngineInstance(value) {
|
|
83
|
+
const record = asRecord(value);
|
|
84
|
+
if (!record || record.schemaVersion !== INSTANCE_SCHEMA_VERSION)
|
|
85
|
+
return null;
|
|
86
|
+
const instanceId = stringFromUnknown(record.instanceId);
|
|
87
|
+
const pid = numberFromUnknown(record.pid);
|
|
88
|
+
const port = numberFromUnknown(record.port);
|
|
89
|
+
const token = stringFromUnknown(record.token);
|
|
90
|
+
const resourceRoot = stringFromUnknown(record.resourceRoot);
|
|
91
|
+
const heartbeatAt = numberFromUnknown(record.heartbeatAt);
|
|
92
|
+
if (!instanceId ||
|
|
93
|
+
!pid ||
|
|
94
|
+
!Number.isInteger(pid) ||
|
|
95
|
+
!port ||
|
|
96
|
+
!Number.isInteger(port) ||
|
|
97
|
+
port < 1 ||
|
|
98
|
+
port > 65535 ||
|
|
99
|
+
!token ||
|
|
100
|
+
!resourceRoot ||
|
|
101
|
+
!heartbeatAt) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
instanceId,
|
|
107
|
+
pid,
|
|
108
|
+
port,
|
|
109
|
+
token,
|
|
110
|
+
projectId: stringFromUnknown(record.projectId),
|
|
111
|
+
projectIdHash: stringFromUnknown(record.projectIdHash),
|
|
112
|
+
resourceRoot,
|
|
113
|
+
projectName: stringFromUnknown(record.projectName),
|
|
114
|
+
heartbeatAt,
|
|
115
|
+
engineVersion: stringFromUnknown(record.engineVersion),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export async function listEngineInstances(nowMs = Date.now(), summerDir = getSummerDir()) {
|
|
119
|
+
const instancesDir = join(summerDir, "instances");
|
|
120
|
+
try {
|
|
121
|
+
const dirInfo = await lstat(instancesDir);
|
|
122
|
+
if (!dirInfo.isDirectory() || dirInfo.isSymbolicLink())
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const entries = await readdir(instancesDir, { withFileTypes: true });
|
|
129
|
+
const instances = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
132
|
+
continue;
|
|
133
|
+
const path = join(instancesDir, entry.name);
|
|
134
|
+
try {
|
|
135
|
+
const info = await lstat(path);
|
|
136
|
+
if (!info.isFile() ||
|
|
137
|
+
info.isSymbolicLink() ||
|
|
138
|
+
info.size > MAX_INSTANCE_FILE_BYTES) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const parsed = parseEngineInstance(JSON.parse(await readFile(path, "utf-8")));
|
|
142
|
+
if (!parsed || !processIsAlive(parsed.pid))
|
|
143
|
+
continue;
|
|
144
|
+
if (nowMs - parsed.heartbeatAt * 1000 > INSTANCE_STALE_MS)
|
|
145
|
+
continue;
|
|
146
|
+
parsed.resourceRoot = await canonicalPath(parsed.resourceRoot);
|
|
147
|
+
instances.push(parsed);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// One malformed or concurrently-replaced entry must not hide other editors.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return instances.sort((a, b) => a.resourceRoot.localeCompare(b.resourceRoot) ||
|
|
154
|
+
a.instanceId.localeCompare(b.instanceId));
|
|
155
|
+
}
|
|
156
|
+
function formatInstances(instances) {
|
|
157
|
+
return instances
|
|
158
|
+
.map((instance) => {
|
|
159
|
+
const label = instance.projectName
|
|
160
|
+
? `${instance.projectName} (${instance.resourceRoot})`
|
|
161
|
+
: instance.resourceRoot;
|
|
162
|
+
return ` - ${label} [${instance.instanceId}]`;
|
|
163
|
+
})
|
|
164
|
+
.join("\n");
|
|
165
|
+
}
|
|
166
|
+
async function connectionForInstance(instance, selection) {
|
|
167
|
+
const health = await checkEngineHealth(instance.port);
|
|
168
|
+
if (!health) {
|
|
169
|
+
throw new Error(`Summer Engine instance ${instance.instanceId} is not responding on port ${instance.port}.`);
|
|
170
|
+
}
|
|
171
|
+
if (health.instanceId !== instance.instanceId) {
|
|
172
|
+
throw new Error(`Summer Engine registry identity changed on port ${instance.port}; refusing to connect to the wrong editor.`);
|
|
173
|
+
}
|
|
174
|
+
if (instance.projectIdHash &&
|
|
175
|
+
health.projectIdHash &&
|
|
176
|
+
instance.projectIdHash !== health.projectIdHash) {
|
|
177
|
+
throw new Error(`Summer Engine project identity changed on port ${instance.port}; refusing to connect to the wrong project.`);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
port: instance.port,
|
|
181
|
+
token: instance.token,
|
|
182
|
+
health,
|
|
183
|
+
instance,
|
|
184
|
+
selection: { ...selection },
|
|
185
|
+
source: "registry",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export async function resolveEngineConnection(selection = {}, options = {}) {
|
|
189
|
+
const summerDir = options.summerDir ?? getSummerDir();
|
|
190
|
+
const explicitInstanceId = stringFromUnknown(selection.instanceId);
|
|
191
|
+
const explicitProjectPath = stringFromUnknown(selection.projectPath);
|
|
192
|
+
const cwd = stringFromUnknown(selection.cwd);
|
|
193
|
+
const requestedProjectRoot = explicitProjectPath
|
|
194
|
+
? await findProjectRoot(explicitProjectPath)
|
|
195
|
+
: !explicitInstanceId && cwd
|
|
196
|
+
? await findProjectRoot(cwd)
|
|
197
|
+
: null;
|
|
198
|
+
if (explicitProjectPath && !requestedProjectRoot) {
|
|
199
|
+
throw new Error(`No project.godot found at or above ${resolve(explicitProjectPath)}.`);
|
|
200
|
+
}
|
|
201
|
+
const instances = await listEngineInstances(options.nowMs ?? Date.now(), summerDir);
|
|
202
|
+
let candidates = instances;
|
|
203
|
+
if (explicitInstanceId) {
|
|
204
|
+
candidates = candidates.filter((instance) => instance.instanceId === explicitInstanceId);
|
|
205
|
+
}
|
|
206
|
+
if (requestedProjectRoot) {
|
|
207
|
+
const canonicalRoot = await canonicalPath(requestedProjectRoot);
|
|
208
|
+
candidates = candidates.filter((instance) => instance.resourceRoot === canonicalRoot);
|
|
209
|
+
}
|
|
210
|
+
if (explicitInstanceId || requestedProjectRoot) {
|
|
211
|
+
if (candidates.length === 1) {
|
|
212
|
+
return connectionForInstance(candidates[0], {
|
|
213
|
+
...selection,
|
|
214
|
+
projectPath: requestedProjectRoot ?? selection.projectPath,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (candidates.length > 1) {
|
|
218
|
+
throw new Error(`More than one Summer editor matches this project. Select one with \`summer mcp --instance <id>\`:\n${formatInstances(candidates)}`);
|
|
219
|
+
}
|
|
220
|
+
const canUseLegacyForInferredProject = instances.length === 0 &&
|
|
221
|
+
!explicitInstanceId &&
|
|
222
|
+
!explicitProjectPath &&
|
|
223
|
+
Boolean(requestedProjectRoot);
|
|
224
|
+
if (!canUseLegacyForInferredProject) {
|
|
225
|
+
const target = explicitInstanceId
|
|
226
|
+
? `instance ${explicitInstanceId}`
|
|
227
|
+
: `project ${requestedProjectRoot}`;
|
|
228
|
+
throw new Error(`No running Summer editor matches ${target}. Open that project in Summer Engine and retry.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (instances.length === 1) {
|
|
232
|
+
return connectionForInstance(instances[0], selection);
|
|
233
|
+
}
|
|
234
|
+
if (instances.length > 1) {
|
|
235
|
+
throw new Error("Multiple Summer editors are running and this agent is not inside a Summer project. " +
|
|
236
|
+
"Start the agent from the project directory, or pass `summer mcp --project <path>`.\n" +
|
|
237
|
+
formatInstances(instances));
|
|
238
|
+
}
|
|
239
|
+
const [port, token] = await Promise.all([
|
|
240
|
+
getApiPort(summerDir),
|
|
241
|
+
getApiToken(summerDir),
|
|
242
|
+
]);
|
|
243
|
+
if (!token) {
|
|
244
|
+
throw new Error("Summer Engine is not running (no api-token found). Open Summer Engine first.");
|
|
245
|
+
}
|
|
246
|
+
const health = await checkEngineHealth(port);
|
|
247
|
+
if (!health) {
|
|
248
|
+
throw new Error(`Summer Engine is not responding on port ${port}. Make sure it's open.`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
port,
|
|
252
|
+
token,
|
|
253
|
+
health,
|
|
254
|
+
selection: null,
|
|
255
|
+
source: "legacy",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
24
258
|
function asRecord(value) {
|
|
25
259
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
26
260
|
? value
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PathFlavor {
|
|
2
|
+
isAbsolute(path: string): boolean;
|
|
3
|
+
relative(from: string, to: string): string;
|
|
4
|
+
resolve(...paths: string[]): string;
|
|
5
|
+
sep: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PublicSurfaceRoots {
|
|
8
|
+
packageRoot: string;
|
|
9
|
+
engineRoot: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function snapshotPublicPath(pathFlavor: PathFlavor, roots: PublicSurfaceRoots, file: string): string;
|
|
12
|
+
export declare function compareByCodePoint(left: string, right: string): number;
|
|
13
|
+
export declare function stablePublicPathInventory(pathFlavor: PathFlavor, roots: PublicSurfaceRoots, files: readonly string[]): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function relativeWithin(pathFlavor, root, file) {
|
|
2
|
+
const resolvedRoot = pathFlavor.resolve(root);
|
|
3
|
+
const resolvedFile = pathFlavor.resolve(file);
|
|
4
|
+
const candidate = pathFlavor.relative(resolvedRoot, resolvedFile);
|
|
5
|
+
if (pathFlavor.isAbsolute(candidate) ||
|
|
6
|
+
candidate === ".." ||
|
|
7
|
+
candidate.startsWith(`..${pathFlavor.sep}`)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return candidate;
|
|
11
|
+
}
|
|
12
|
+
function toPosix(pathFlavor, path) {
|
|
13
|
+
return path.split(pathFlavor.sep).join("/");
|
|
14
|
+
}
|
|
15
|
+
export function snapshotPublicPath(pathFlavor, roots, file) {
|
|
16
|
+
const packageRelative = relativeWithin(pathFlavor, roots.packageRoot, file);
|
|
17
|
+
if (packageRelative !== null) {
|
|
18
|
+
return `cli/${toPosix(pathFlavor, packageRelative)}`;
|
|
19
|
+
}
|
|
20
|
+
const engineRelative = relativeWithin(pathFlavor, roots.engineRoot, file);
|
|
21
|
+
if (engineRelative !== null) {
|
|
22
|
+
return `engine/${toPosix(pathFlavor, engineRelative)}`;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Public surface escapes both inventory roots: ${file}`);
|
|
25
|
+
}
|
|
26
|
+
export function compareByCodePoint(left, right) {
|
|
27
|
+
const leftPoints = [...left].map((character) => character.codePointAt(0));
|
|
28
|
+
const rightPoints = [...right].map((character) => character.codePointAt(0));
|
|
29
|
+
const sharedLength = Math.min(leftPoints.length, rightPoints.length);
|
|
30
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
31
|
+
if (leftPoints[index] !== rightPoints[index]) {
|
|
32
|
+
return leftPoints[index] - rightPoints[index];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return leftPoints.length - rightPoints.length;
|
|
36
|
+
}
|
|
37
|
+
export function stablePublicPathInventory(pathFlavor, roots, files) {
|
|
38
|
+
return files
|
|
39
|
+
.map((file) => snapshotPublicPath(pathFlavor, roots, file))
|
|
40
|
+
.sort(compareByCodePoint)
|
|
41
|
+
.join("\n");
|
|
42
|
+
}
|
|
@@ -545,6 +545,14 @@ export declare const SKILL_REGISTRY: readonly [{
|
|
|
545
545
|
readonly recommended: true;
|
|
546
546
|
readonly requiresMcpTools: readonly ["summer_play", "summer_stop", "summer_get_console", "summer_get_debugger_errors", "summer_get_diagnostics"];
|
|
547
547
|
readonly testScenario: "Just finished a gameplay feature — skill drives a golden-path + edge-case playthrough with diagnostics capture before the feature can be marked done.";
|
|
548
|
+
}, {
|
|
549
|
+
readonly name: "headless-scripting";
|
|
550
|
+
readonly category: "workflow";
|
|
551
|
+
readonly public: true;
|
|
552
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "kilo-code", "gemini", "github-copilot", "vscode-copilot", "opencode"];
|
|
553
|
+
readonly recommended: true;
|
|
554
|
+
readonly requiresMcpTools: readonly ["summer_get_project_context", "summer_write_file"];
|
|
555
|
+
readonly testScenario: "User needs a navmesh baked, collision generated, an Animation authored, or raw assets re-imported — skill runs a GDScript file against the real engine binary instead of reporting the capability as missing, and blocks the 'run it headless and screenshot it' plan that silently returns null.";
|
|
548
556
|
}, {
|
|
549
557
|
readonly name: "debugging-game-feel";
|
|
550
558
|
readonly category: "workflow";
|
|
@@ -1023,6 +1023,15 @@ export const SKILL_REGISTRY = [
|
|
|
1023
1023
|
],
|
|
1024
1024
|
testScenario: "Just finished a gameplay feature — skill drives a golden-path + edge-case playthrough with diagnostics capture before the feature can be marked done.",
|
|
1025
1025
|
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "headless-scripting",
|
|
1028
|
+
category: "workflow",
|
|
1029
|
+
public: true,
|
|
1030
|
+
clients: ALL_CLIENTS,
|
|
1031
|
+
recommended: true,
|
|
1032
|
+
requiresMcpTools: ["summer_get_project_context", "summer_write_file"],
|
|
1033
|
+
testScenario: "User needs a navmesh baked, collision generated, an Animation authored, or raw assets re-imported — skill runs a GDScript file against the real engine binary instead of reporting the capability as missing, and blocks the 'run it headless and screenshot it' plan that silently returns null.",
|
|
1034
|
+
},
|
|
1026
1035
|
{
|
|
1027
1036
|
name: "debugging-game-feel",
|
|
1028
1037
|
category: "workflow",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class SummerStoreError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
constructor(code: string, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function getSummerDir(): string;
|
|
6
|
+
/** Test-only seam. Passing null restores the real ~/.summer path. */
|
|
7
|
+
export declare function setSummerDirForTests(path: string | null): void;
|
|
8
|
+
export declare function ensureSummerStore(): Promise<string>;
|
|
9
|
+
export declare function readStoreText(name: string): Promise<string | null>;
|
|
10
|
+
export declare function readStoreJson<T>(name: string): Promise<T | null>;
|
|
11
|
+
export declare function writeStoreText(name: string, value: string): Promise<void>;
|
|
12
|
+
export declare function writeStoreJson(name: string, value: unknown): Promise<void>;
|
|
13
|
+
export declare function appendStoreJsonLine(name: string, value: unknown): Promise<void>;
|
|
14
|
+
export declare function removeStoreFile(name: string): Promise<boolean>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { appendFile, chmod, lstat, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
let summerDirOverride = null;
|
|
6
|
+
export class SummerStoreError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "SummerStoreError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function getSummerDir() {
|
|
15
|
+
return summerDirOverride ?? join(homedir(), ".summer");
|
|
16
|
+
}
|
|
17
|
+
/** Test-only seam. Passing null restores the real ~/.summer path. */
|
|
18
|
+
export function setSummerDirForTests(path) {
|
|
19
|
+
summerDirOverride = path;
|
|
20
|
+
}
|
|
21
|
+
function storePath(name) {
|
|
22
|
+
if (!name || basename(name) !== name || name === "." || name === "..") {
|
|
23
|
+
throw new SummerStoreError("invalid_store_path", `Invalid Summer store filename "${name}". Recovery: use a single filename inside ~/.summer/.`);
|
|
24
|
+
}
|
|
25
|
+
return join(getSummerDir(), name);
|
|
26
|
+
}
|
|
27
|
+
async function hardenMode(path, mode) {
|
|
28
|
+
try {
|
|
29
|
+
await chmod(path, mode);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const code = error.code;
|
|
33
|
+
// Windows does not implement POSIX permission bits consistently.
|
|
34
|
+
if (process.platform !== "win32" && code !== "EPERM")
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function ensureSummerStore() {
|
|
39
|
+
const dir = getSummerDir();
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
42
|
+
const info = await lstat(dir);
|
|
43
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
44
|
+
throw new SummerStoreError("unsafe_store_directory", `${dir} must be a real directory, not a file or symlink. Recovery: move it aside, create a directory at ${dir}, and run the command again.`);
|
|
45
|
+
}
|
|
46
|
+
await hardenMode(dir, 0o700);
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof SummerStoreError)
|
|
51
|
+
throw error;
|
|
52
|
+
throw new SummerStoreError("store_unavailable", `Cannot secure ${dir}. Recovery: make sure the directory is owned and writable by your user, then run the command again.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function assertSafeExistingFile(path) {
|
|
56
|
+
try {
|
|
57
|
+
const info = await lstat(path);
|
|
58
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
59
|
+
throw new SummerStoreError("unsafe_store_file", `${path} must be a regular file, not a directory or symlink. Recovery: move it aside and run the command again.`);
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code === "ENOENT")
|
|
65
|
+
return false;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function readStoreText(name) {
|
|
70
|
+
const path = storePath(name);
|
|
71
|
+
if (!(await assertSafeExistingFile(path)))
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
await hardenMode(path, 0o600);
|
|
75
|
+
return await readFile(path, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof SummerStoreError)
|
|
79
|
+
throw error;
|
|
80
|
+
throw new SummerStoreError("store_read_failed", `Cannot read ${path}. Recovery: make sure the file is owned and readable by your user, then run the command again.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function readStoreJson(name) {
|
|
84
|
+
const raw = await readStoreText(name);
|
|
85
|
+
if (raw === null)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new SummerStoreError("invalid_store_json", `${storePath(name)} is not valid JSON. Recovery: move that file aside and run the command again.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function writeStoreText(name, value) {
|
|
95
|
+
const dir = await ensureSummerStore();
|
|
96
|
+
const destination = storePath(name);
|
|
97
|
+
await assertSafeExistingFile(destination);
|
|
98
|
+
const temporary = join(dir, `.${name}.${process.pid}.${randomUUID()}.tmp`);
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(temporary, value, {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
mode: 0o600,
|
|
103
|
+
flag: "wx",
|
|
104
|
+
});
|
|
105
|
+
await hardenMode(temporary, 0o600);
|
|
106
|
+
await rename(temporary, destination);
|
|
107
|
+
await hardenMode(destination, 0o600);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
111
|
+
if (error instanceof SummerStoreError)
|
|
112
|
+
throw error;
|
|
113
|
+
throw new SummerStoreError("store_write_failed", `Cannot write ${destination}. Recovery: make sure ${dir} is owned and writable by your user, then run the command again.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function writeStoreJson(name, value) {
|
|
117
|
+
await writeStoreText(name, `${JSON.stringify(value, null, 2)}\n`);
|
|
118
|
+
}
|
|
119
|
+
export async function appendStoreJsonLine(name, value) {
|
|
120
|
+
await ensureSummerStore();
|
|
121
|
+
const path = storePath(name);
|
|
122
|
+
await assertSafeExistingFile(path);
|
|
123
|
+
try {
|
|
124
|
+
await appendFile(path, `${JSON.stringify(value)}\n`, {
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
mode: 0o600,
|
|
127
|
+
});
|
|
128
|
+
await hardenMode(path, 0o600);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new SummerStoreError("store_append_failed", `Cannot append to ${path}. Recovery: make sure the file is owned and writable by your user, then run the command again.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function removeStoreFile(name) {
|
|
135
|
+
const path = storePath(name);
|
|
136
|
+
if (!(await assertSafeExistingFile(path)))
|
|
137
|
+
return false;
|
|
138
|
+
try {
|
|
139
|
+
await rm(path);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new SummerStoreError("store_remove_failed", `Cannot remove ${path}. Recovery: make sure the file is owned and writable by your user, then run the command again.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summer CLI compatibility values.
|
|
3
|
+
*
|
|
4
|
+
* Keep the current technical base in one place. The project feature tag is
|
|
5
|
+
* derived from it so `summer create` cannot drift to a different line. This
|
|
6
|
+
* source is cross-checked against the repository compatibility contract by
|
|
7
|
+
* `summer-compatibility.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
const CURRENT_TECHNICAL_BASE_VERSION = "4.6.1";
|
|
10
|
+
function featureTagFor(version) {
|
|
11
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
12
|
+
if (!match) {
|
|
13
|
+
throw new Error(`Invalid Summer Engine technical base version: ${version}`);
|
|
14
|
+
}
|
|
15
|
+
return `${match[1]}.${match[2]}`;
|
|
16
|
+
}
|
|
17
|
+
export const SUMMER_ENGINE_COMPATIBILITY = Object.freeze({
|
|
18
|
+
currentTechnicalBaseVersion: CURRENT_TECHNICAL_BASE_VERSION,
|
|
19
|
+
projectFeatureTag: featureTagFor(CURRENT_TECHNICAL_BASE_VERSION),
|
|
20
|
+
plannedNextTechnicalBaseVersion: "4.7.1",
|
|
21
|
+
upstreamPolicy: "continuous-upstream",
|
|
22
|
+
});
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { EngineApiClient } from "../lib/api-client.js";
|
|
2
|
+
import type { EngineSelection } from "../lib/engine.js";
|
|
2
3
|
import { getCachedBootDriftNotice as getCachedNotice } from "../lib/mcp-boot-notice.js";
|
|
3
4
|
export declare const getCachedBootDriftNotice: typeof getCachedNotice;
|
|
5
|
+
export declare function configureMcpEngineSelection(selection?: EngineSelection): void;
|
|
4
6
|
export declare function getClient(): Promise<EngineApiClient>;
|
|
5
7
|
export declare function resetClient(): void;
|
|
6
|
-
export
|
|
8
|
+
export interface StartMcpServerOptions {
|
|
9
|
+
projectPath?: string;
|
|
10
|
+
instanceId?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function startMcpServer(options?: StartMcpServerOptions): Promise<void>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { registerFileTools } from "./tools/file-tools.js";
|
|
|
11
11
|
import { registerAssetTools } from "./tools/asset-tools.js";
|
|
12
12
|
import { registerGenerateTools } from "./tools/generate-tools.js";
|
|
13
13
|
import { registerCloudTools } from "./tools/cloud-tools.js";
|
|
14
|
+
import { registerCreatorTools } from "./tools/creator-tools.js";
|
|
14
15
|
import { getCachedBootDriftNotice as getCachedNotice, setCachedBootDriftNotice, } from "../lib/mcp-boot-notice.js";
|
|
15
16
|
import { appendMcpLogEvent } from "../lib/mcp-log.js";
|
|
16
17
|
import { buildBootDriftNotice, fetchLatestRegistryVersion, } from "../lib/version-check.js";
|
|
@@ -31,6 +32,11 @@ async function probeBootDrift() {
|
|
|
31
32
|
setCachedBootDriftNotice(buildBootDriftNotice(version, registry));
|
|
32
33
|
}
|
|
33
34
|
let cachedClient = null;
|
|
35
|
+
let engineSelection;
|
|
36
|
+
export function configureMcpEngineSelection(selection) {
|
|
37
|
+
engineSelection = selection ? { ...selection } : undefined;
|
|
38
|
+
cachedClient = null;
|
|
39
|
+
}
|
|
34
40
|
export async function getClient() {
|
|
35
41
|
if (cachedClient) {
|
|
36
42
|
// The engine rotates its api-token (and can change ports) on every launch, so
|
|
@@ -46,12 +52,16 @@ export async function getClient() {
|
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
try {
|
|
49
|
-
cachedClient = await EngineApiClient.connect();
|
|
55
|
+
cachedClient = await EngineApiClient.connect(engineSelection);
|
|
50
56
|
return cachedClient;
|
|
51
57
|
}
|
|
52
|
-
catch {
|
|
58
|
+
catch (error) {
|
|
53
59
|
cachedClient = null;
|
|
54
|
-
|
|
60
|
+
const reason = error instanceof Error
|
|
61
|
+
? error.message
|
|
62
|
+
: "Summer Engine is not running.";
|
|
63
|
+
throw new Error(reason + "\n" +
|
|
64
|
+
"Open the intended project in Summer Engine, or run: npx summer-engine run\n" +
|
|
55
65
|
"Note: only tools that touch the local project need the engine. Cloud tools " +
|
|
56
66
|
"(summer_generate_*, summer_search_assets, summer_list_my_assets, summer_get_asset, " +
|
|
57
67
|
"summer_check_job) work right now without it — they only need 'npx summer-engine login'.");
|
|
@@ -203,13 +213,23 @@ function installResultSizeLogger(server) {
|
|
|
203
213
|
};
|
|
204
214
|
return () => registeredToolCount;
|
|
205
215
|
}
|
|
206
|
-
export async function startMcpServer() {
|
|
216
|
+
export async function startMcpServer(options = {}) {
|
|
217
|
+
configureMcpEngineSelection({
|
|
218
|
+
instanceId: options.instanceId ?? process.env.SUMMER_ENGINE_INSTANCE_ID,
|
|
219
|
+
projectPath: options.projectPath ?? process.env.SUMMER_ENGINE_PROJECT,
|
|
220
|
+
cwd: options.cwd ?? process.cwd(),
|
|
221
|
+
});
|
|
207
222
|
installMcpProcessDiagnostics();
|
|
208
223
|
appendMcpLogEvent("mcp:start", {
|
|
209
224
|
version,
|
|
210
225
|
pid: process.pid,
|
|
211
226
|
node: process.version,
|
|
212
227
|
cwd: process.cwd(),
|
|
228
|
+
selection: {
|
|
229
|
+
instanceId: options.instanceId ? "explicit" : undefined,
|
|
230
|
+
projectPath: options.projectPath ? "explicit" : undefined,
|
|
231
|
+
cwd: options.cwd ?? process.cwd(),
|
|
232
|
+
},
|
|
213
233
|
});
|
|
214
234
|
const server = new McpServer({
|
|
215
235
|
name: "summer-engine",
|
|
@@ -226,6 +246,7 @@ export async function startMcpServer() {
|
|
|
226
246
|
registerAssetTools(server);
|
|
227
247
|
registerGenerateTools(server);
|
|
228
248
|
registerCloudTools(server);
|
|
249
|
+
registerCreatorTools(server);
|
|
229
250
|
// Fire-and-forget — never block tool registration on the npm registry.
|
|
230
251
|
void probeBootDrift().catch((error) => {
|
|
231
252
|
setCachedBootDriftNotice(null);
|