summer-engine 1.2.0 → 1.2.1
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/README.md +58 -24
- package/dist/bin/summer.js +4 -0
- package/dist/commands/doctor.d.ts +26 -0
- package/dist/commands/doctor.js +248 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.js +29 -0
- package/dist/commands/mcp.js +49 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +128 -0
- package/dist/commands/skills.js +246 -107
- package/dist/lib/agent-config.d.ts +39 -0
- package/dist/lib/agent-config.js +284 -0
- package/dist/lib/agent-config.test.d.ts +1 -0
- package/dist/lib/agent-config.test.js +80 -0
- package/dist/lib/api-client.d.ts +19 -2
- package/dist/lib/api-client.js +129 -14
- package/dist/lib/skills-registry.d.ts +69 -0
- package/dist/lib/skills-registry.js +118 -0
- package/dist/mcp/tools/project-tools.js +100 -23
- package/dist/mcp/tools/with-engine.js +3 -53
- package/package.json +5 -2
- package/skills/gdscript-patterns/SKILL.md +2 -2
- package/skills/make-game/SKILL.md +3 -3
- package/skills/scene-composition/SKILL.md +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
export const SUMMER_MCP_SERVER_NAME = "summer-engine";
|
|
7
|
+
export const supportedAgents = [
|
|
8
|
+
"codex",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"cursor",
|
|
11
|
+
"windsurf",
|
|
12
|
+
];
|
|
13
|
+
const agentAliases = {
|
|
14
|
+
claude: "claude-code",
|
|
15
|
+
"claude-code": "claude-code",
|
|
16
|
+
codex: "codex",
|
|
17
|
+
cursor: "cursor",
|
|
18
|
+
windsurf: "windsurf",
|
|
19
|
+
};
|
|
20
|
+
export function parseAgent(value) {
|
|
21
|
+
if (!value)
|
|
22
|
+
return null;
|
|
23
|
+
return agentAliases[value.trim().toLowerCase()] ?? null;
|
|
24
|
+
}
|
|
25
|
+
export function parseScope(value) {
|
|
26
|
+
if (!value)
|
|
27
|
+
return "user";
|
|
28
|
+
const normalized = value.trim().toLowerCase();
|
|
29
|
+
if (normalized === "user" || normalized === "project")
|
|
30
|
+
return normalized;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export async function configureAgentMcp(options) {
|
|
34
|
+
const env = options.env ?? process.env;
|
|
35
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
36
|
+
const server = createSummerMcpServerConfig(Boolean(options.localDev));
|
|
37
|
+
const target = resolveConfigTarget(options.agent, options.scope, cwd, env);
|
|
38
|
+
const snippet = renderConfigSnippet(options.agent, server);
|
|
39
|
+
const dryRun = Boolean(options.dryRun);
|
|
40
|
+
const print = Boolean(options.print);
|
|
41
|
+
const shouldWrite = !dryRun && !print;
|
|
42
|
+
const update = print
|
|
43
|
+
? { changed: true }
|
|
44
|
+
: target.format === "toml"
|
|
45
|
+
? await upsertCodexConfig(target.path, server, shouldWrite)
|
|
46
|
+
: await upsertJsonMcpConfig(target.path, server, shouldWrite);
|
|
47
|
+
return {
|
|
48
|
+
agent: options.agent,
|
|
49
|
+
scope: options.scope,
|
|
50
|
+
path: target.path,
|
|
51
|
+
serverName: SUMMER_MCP_SERVER_NAME,
|
|
52
|
+
server,
|
|
53
|
+
format: target.format,
|
|
54
|
+
snippet,
|
|
55
|
+
changed: update.changed,
|
|
56
|
+
wrote: shouldWrite && update.changed,
|
|
57
|
+
dryRun,
|
|
58
|
+
print,
|
|
59
|
+
localDev: Boolean(options.localDev),
|
|
60
|
+
warnings: target.warnings,
|
|
61
|
+
nextSteps: createNextSteps(options.agent, options.scope, target.path),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function createSummerMcpServerConfig(localDev) {
|
|
65
|
+
if (localDev) {
|
|
66
|
+
return {
|
|
67
|
+
command: "node",
|
|
68
|
+
args: [resolveLocalCliPath(), "mcp"],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
command: "npx",
|
|
73
|
+
args: ["summer-engine", "mcp"],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function renderConfigSnippet(agent, server) {
|
|
77
|
+
if (agent === "codex") {
|
|
78
|
+
return renderCodexServerTable(server);
|
|
79
|
+
}
|
|
80
|
+
return (JSON.stringify({
|
|
81
|
+
mcpServers: {
|
|
82
|
+
[SUMMER_MCP_SERVER_NAME]: server,
|
|
83
|
+
},
|
|
84
|
+
}, null, 2) + "\n");
|
|
85
|
+
}
|
|
86
|
+
function resolveLocalCliPath() {
|
|
87
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
88
|
+
return resolve(dirname(thisFile), "..", "bin", "summer.js");
|
|
89
|
+
}
|
|
90
|
+
function resolveConfigTarget(agent, scope, cwd, env) {
|
|
91
|
+
const override = getConfigPathOverride(agent, env);
|
|
92
|
+
const warnings = [];
|
|
93
|
+
if (override) {
|
|
94
|
+
return {
|
|
95
|
+
path: resolve(override),
|
|
96
|
+
format: agent === "codex" ? "toml" : "json",
|
|
97
|
+
warnings,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (agent === "codex") {
|
|
101
|
+
return {
|
|
102
|
+
path: scope === "user"
|
|
103
|
+
? join(homedir(), ".codex", "config.toml")
|
|
104
|
+
: join(cwd, ".codex", "config.toml"),
|
|
105
|
+
format: "toml",
|
|
106
|
+
warnings,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (agent === "claude-code") {
|
|
110
|
+
return {
|
|
111
|
+
path: scope === "user"
|
|
112
|
+
? join(homedir(), ".claude.json")
|
|
113
|
+
: join(cwd, ".mcp.json"),
|
|
114
|
+
format: "json",
|
|
115
|
+
warnings,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (agent === "cursor") {
|
|
119
|
+
return {
|
|
120
|
+
path: scope === "user"
|
|
121
|
+
? join(homedir(), ".cursor", "mcp.json")
|
|
122
|
+
: join(cwd, ".cursor", "mcp.json"),
|
|
123
|
+
format: "json",
|
|
124
|
+
warnings,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (scope === "project") {
|
|
128
|
+
warnings.push("Windsurf documents MCP configuration as user-scoped; project scope writes .windsurf/mcp_config.json for teams that load workspace config.");
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
path: scope === "user"
|
|
132
|
+
? join(homedir(), ".codeium", "windsurf", "mcp_config.json")
|
|
133
|
+
: join(cwd, ".windsurf", "mcp_config.json"),
|
|
134
|
+
format: "json",
|
|
135
|
+
warnings,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function getConfigPathOverride(agent, env) {
|
|
139
|
+
if (agent === "codex")
|
|
140
|
+
return env.SUMMER_CODEX_CONFIG_FILE;
|
|
141
|
+
if (agent === "claude-code")
|
|
142
|
+
return env.SUMMER_CLAUDE_CONFIG_FILE;
|
|
143
|
+
if (agent === "cursor")
|
|
144
|
+
return env.SUMMER_CURSOR_MCP_CONFIG_FILE;
|
|
145
|
+
return env.SUMMER_WINDSURF_MCP_CONFIG_FILE;
|
|
146
|
+
}
|
|
147
|
+
async function upsertJsonMcpConfig(path, server, write) {
|
|
148
|
+
const current = await readJsonConfig(path);
|
|
149
|
+
const next = copyJsonObject(current);
|
|
150
|
+
next.mcpServers = mergeMcpServers(next.mcpServers, server);
|
|
151
|
+
const currentRendered = renderJsonFile(current);
|
|
152
|
+
const nextRendered = renderJsonFile(next);
|
|
153
|
+
const changed = currentRendered !== nextRendered;
|
|
154
|
+
if (write && changed) {
|
|
155
|
+
await writeTextFile(path, nextRendered);
|
|
156
|
+
}
|
|
157
|
+
return { changed };
|
|
158
|
+
}
|
|
159
|
+
function mergeMcpServers(value, server) {
|
|
160
|
+
if (value !== undefined && !isJsonObject(value)) {
|
|
161
|
+
throw new Error("Existing mcpServers value must be a JSON object.");
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...(isJsonObject(value) ? value : {}),
|
|
165
|
+
[SUMMER_MCP_SERVER_NAME]: server,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function readJsonConfig(path) {
|
|
169
|
+
if (!existsSync(path))
|
|
170
|
+
return {};
|
|
171
|
+
let content;
|
|
172
|
+
try {
|
|
173
|
+
content = await readFile(path, "utf-8");
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw new Error(`Could not read ${path}: ${formatError(error)}`);
|
|
177
|
+
}
|
|
178
|
+
if (content.trim() === "")
|
|
179
|
+
return {};
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(content);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
throw new Error(`Could not parse JSON in ${path}: ${formatError(error)}`);
|
|
186
|
+
}
|
|
187
|
+
if (!isJsonObject(parsed)) {
|
|
188
|
+
throw new Error(`Expected ${path} to contain a JSON object.`);
|
|
189
|
+
}
|
|
190
|
+
return parsed;
|
|
191
|
+
}
|
|
192
|
+
async function upsertCodexConfig(path, server, write) {
|
|
193
|
+
const current = await readTextFileIfExists(path);
|
|
194
|
+
const next = upsertTomlTable(current, renderCodexServerTable(server));
|
|
195
|
+
const changed = current !== next;
|
|
196
|
+
if (write && changed) {
|
|
197
|
+
await writeTextFile(path, next);
|
|
198
|
+
}
|
|
199
|
+
return { changed };
|
|
200
|
+
}
|
|
201
|
+
async function readTextFileIfExists(path) {
|
|
202
|
+
if (!existsSync(path))
|
|
203
|
+
return "";
|
|
204
|
+
try {
|
|
205
|
+
return await readFile(path, "utf-8");
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw new Error(`Could not read ${path}: ${formatError(error)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function upsertTomlTable(content, table) {
|
|
212
|
+
const lines = content.split(/\r?\n/);
|
|
213
|
+
const serverHeader = /^\s*\[mcp_servers\.(?:"summer-engine"|summer-engine)(?:\.|\])/;
|
|
214
|
+
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.(?:"summer-engine"|summer-engine)\]\s*(?:#.*)?$/.test(line));
|
|
215
|
+
if (start === -1) {
|
|
216
|
+
const trimmed = content.endsWith("\n") || content === "" ? content : `${content}\n`;
|
|
217
|
+
return `${trimmed}${trimmed === "" ? "" : "\n"}${table}`;
|
|
218
|
+
}
|
|
219
|
+
let end = lines.length;
|
|
220
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
221
|
+
if (/^\s*\[/.test(lines[index]) && !serverHeader.test(lines[index])) {
|
|
222
|
+
end = index;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const replacement = table.trimEnd().split("\n");
|
|
227
|
+
if (lines[end] && lines[end].trim() !== "") {
|
|
228
|
+
replacement.push("");
|
|
229
|
+
}
|
|
230
|
+
const nextLines = [
|
|
231
|
+
...lines.slice(0, start),
|
|
232
|
+
...replacement,
|
|
233
|
+
...lines.slice(end),
|
|
234
|
+
];
|
|
235
|
+
return ensureTrailingNewline(nextLines.join("\n"));
|
|
236
|
+
}
|
|
237
|
+
function renderCodexServerTable(server) {
|
|
238
|
+
const lines = [
|
|
239
|
+
`[mcp_servers.${SUMMER_MCP_SERVER_NAME}]`,
|
|
240
|
+
`command = ${tomlString(server.command)}`,
|
|
241
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
242
|
+
];
|
|
243
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
244
|
+
lines.push(`env = { ${Object.entries(server.env)
|
|
245
|
+
.map(([key, value]) => `${key} = ${tomlString(value)}`)
|
|
246
|
+
.join(", ")} }`);
|
|
247
|
+
}
|
|
248
|
+
return `${lines.join("\n")}\n`;
|
|
249
|
+
}
|
|
250
|
+
function tomlString(value) {
|
|
251
|
+
return JSON.stringify(value);
|
|
252
|
+
}
|
|
253
|
+
function renderJsonFile(value) {
|
|
254
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
255
|
+
}
|
|
256
|
+
function copyJsonObject(value) {
|
|
257
|
+
return JSON.parse(JSON.stringify(value));
|
|
258
|
+
}
|
|
259
|
+
function isJsonObject(value) {
|
|
260
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
261
|
+
}
|
|
262
|
+
async function writeTextFile(path, content) {
|
|
263
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
264
|
+
await writeFile(path, content, { encoding: "utf-8", mode: 0o600 });
|
|
265
|
+
}
|
|
266
|
+
function ensureTrailingNewline(value) {
|
|
267
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
268
|
+
}
|
|
269
|
+
function createNextSteps(agent, scope, path) {
|
|
270
|
+
const restart = agent === "claude-code"
|
|
271
|
+
? "Restart Claude Code or run /mcp in a new session."
|
|
272
|
+
: agent === "codex"
|
|
273
|
+
? "Restart Codex or run /mcp in a new session."
|
|
274
|
+
: agent === "cursor"
|
|
275
|
+
? "Restart Cursor and enable the summer-engine MCP server if prompted."
|
|
276
|
+
: "Restart Windsurf and refresh MCP servers from Cascade settings.";
|
|
277
|
+
const projectTrust = scope === "project" && agent === "codex"
|
|
278
|
+
? "Codex only loads project .codex/config.toml from trusted projects."
|
|
279
|
+
: null;
|
|
280
|
+
return [projectTrust, `Updated ${path}.`, restart].filter((step) => Boolean(step));
|
|
281
|
+
}
|
|
282
|
+
function formatError(error) {
|
|
283
|
+
return error instanceof Error ? error.message : String(error);
|
|
284
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { configureAgentMcp } from "./agent-config.js";
|
|
6
|
+
function tmp() {
|
|
7
|
+
return mkdtempSync(join(tmpdir(), "summer-agent-config-"));
|
|
8
|
+
}
|
|
9
|
+
describe("configureAgentMcp", () => {
|
|
10
|
+
it("writes a fresh claude-code config", async () => {
|
|
11
|
+
const dir = tmp();
|
|
12
|
+
const path = join(dir, ".claude.json");
|
|
13
|
+
const result = await configureAgentMcp({
|
|
14
|
+
agent: "claude-code",
|
|
15
|
+
scope: "user",
|
|
16
|
+
env: { SUMMER_CLAUDE_CONFIG_FILE: path },
|
|
17
|
+
});
|
|
18
|
+
expect(result.wrote).toBe(true);
|
|
19
|
+
const written = JSON.parse(readFileSync(path, "utf-8"));
|
|
20
|
+
expect(written.mcpServers["summer-engine"].command).toBe("npx");
|
|
21
|
+
expect(written.mcpServers["summer-engine"].args).toEqual([
|
|
22
|
+
"summer-engine",
|
|
23
|
+
"mcp",
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
it("preserves unrelated keys when merging claude-code config", async () => {
|
|
27
|
+
const dir = tmp();
|
|
28
|
+
const path = join(dir, ".claude.json");
|
|
29
|
+
writeFileSync(path, JSON.stringify({
|
|
30
|
+
theme: "dark",
|
|
31
|
+
mcpServers: {
|
|
32
|
+
other: { command: "node", args: ["other.js"] },
|
|
33
|
+
},
|
|
34
|
+
}, null, 2));
|
|
35
|
+
await configureAgentMcp({
|
|
36
|
+
agent: "claude-code",
|
|
37
|
+
scope: "user",
|
|
38
|
+
env: { SUMMER_CLAUDE_CONFIG_FILE: path },
|
|
39
|
+
});
|
|
40
|
+
const written = JSON.parse(readFileSync(path, "utf-8"));
|
|
41
|
+
expect(written.theme).toBe("dark");
|
|
42
|
+
expect(written.mcpServers.other.command).toBe("node");
|
|
43
|
+
expect(written.mcpServers["summer-engine"].command).toBe("npx");
|
|
44
|
+
});
|
|
45
|
+
it("upserts a codex TOML server table", async () => {
|
|
46
|
+
const dir = tmp();
|
|
47
|
+
const path = join(dir, "config.toml");
|
|
48
|
+
writeFileSync(path, ['[mcp_servers.other]', 'command = "node"', 'args = ["other.js"]', ''].join("\n"));
|
|
49
|
+
const first = await configureAgentMcp({
|
|
50
|
+
agent: "codex",
|
|
51
|
+
scope: "user",
|
|
52
|
+
env: { SUMMER_CODEX_CONFIG_FILE: path },
|
|
53
|
+
});
|
|
54
|
+
expect(first.wrote).toBe(true);
|
|
55
|
+
let content = readFileSync(path, "utf-8");
|
|
56
|
+
expect(content).toContain("[mcp_servers.other]");
|
|
57
|
+
expect(content).toContain("[mcp_servers.summer-engine]");
|
|
58
|
+
const second = await configureAgentMcp({
|
|
59
|
+
agent: "codex",
|
|
60
|
+
scope: "user",
|
|
61
|
+
env: { SUMMER_CODEX_CONFIG_FILE: path },
|
|
62
|
+
});
|
|
63
|
+
expect(second.wrote).toBe(false);
|
|
64
|
+
content = readFileSync(path, "utf-8");
|
|
65
|
+
const occurrences = content.match(/\[mcp_servers\.summer-engine\]/g) ?? [];
|
|
66
|
+
expect(occurrences.length).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
it("dry-run does not write", async () => {
|
|
69
|
+
const dir = tmp();
|
|
70
|
+
const path = join(dir, ".claude.json");
|
|
71
|
+
const result = await configureAgentMcp({
|
|
72
|
+
agent: "claude-code",
|
|
73
|
+
scope: "user",
|
|
74
|
+
dryRun: true,
|
|
75
|
+
env: { SUMMER_CLAUDE_CONFIG_FILE: path },
|
|
76
|
+
});
|
|
77
|
+
expect(result.wrote).toBe(false);
|
|
78
|
+
expect(() => readFileSync(path, "utf-8")).toThrow();
|
|
79
|
+
});
|
|
80
|
+
});
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export type EngineSnapshot = {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
localPath?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
format?: string;
|
|
8
|
+
mime?: string;
|
|
9
|
+
context?: string;
|
|
10
|
+
op?: string;
|
|
11
|
+
provenance?: unknown;
|
|
12
|
+
bytes?: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
};
|
|
1
16
|
export declare class EngineApiClient {
|
|
2
17
|
private port;
|
|
3
18
|
private token;
|
|
@@ -17,7 +32,9 @@ export declare class EngineApiClient {
|
|
|
17
32
|
readFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
18
33
|
getFsTree(root?: string, limit?: number): Promise<unknown>;
|
|
19
34
|
getSelection(): Promise<unknown>;
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
private writeSnapshotFile;
|
|
36
|
+
private snapshot;
|
|
37
|
+
viewportSnapshot(): Promise<EngineSnapshot>;
|
|
38
|
+
gameSnapshot(): Promise<EngineSnapshot>;
|
|
22
39
|
getPort(): number;
|
|
23
40
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,4 +1,64 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
1
4
|
import { getApiToken, getApiPort, checkEngineHealth } from "./engine.js";
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: null;
|
|
9
|
+
}
|
|
10
|
+
function stringFrom(value) {
|
|
11
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function numberFrom(value) {
|
|
14
|
+
return typeof value === "number" ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
function findSnapshotPayload(value) {
|
|
17
|
+
const record = asRecord(value);
|
|
18
|
+
if (!record)
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof record.base64 === "string" ||
|
|
21
|
+
typeof record.image_base64 === "string" ||
|
|
22
|
+
record.op === "ViewportSnapshot" ||
|
|
23
|
+
record.op === "GameSnapshot") {
|
|
24
|
+
return record;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(record.results)) {
|
|
27
|
+
for (const result of record.results) {
|
|
28
|
+
const payload = findSnapshotPayload(result);
|
|
29
|
+
if (payload)
|
|
30
|
+
return payload;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return findSnapshotPayload(record.data);
|
|
34
|
+
}
|
|
35
|
+
function snapshotFormat(payload) {
|
|
36
|
+
const explicitMime = stringFrom(payload.mime)?.toLowerCase();
|
|
37
|
+
const explicitFormat = stringFrom(payload.format)?.toLowerCase();
|
|
38
|
+
const format = explicitFormat ??
|
|
39
|
+
(explicitMime?.includes("png")
|
|
40
|
+
? "png"
|
|
41
|
+
: explicitMime?.includes("webp")
|
|
42
|
+
? "webp"
|
|
43
|
+
: "jpeg");
|
|
44
|
+
const mime = explicitMime ??
|
|
45
|
+
(format === "png"
|
|
46
|
+
? "image/png"
|
|
47
|
+
: format === "webp"
|
|
48
|
+
? "image/webp"
|
|
49
|
+
: "image/jpeg");
|
|
50
|
+
const ext = format === "jpeg" ? "jpg" : format;
|
|
51
|
+
return { format, mime, ext };
|
|
52
|
+
}
|
|
53
|
+
function withoutImageData(payload) {
|
|
54
|
+
const metadata = {};
|
|
55
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
56
|
+
if (key !== "base64" && key !== "image_base64") {
|
|
57
|
+
metadata[key] = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return metadata;
|
|
61
|
+
}
|
|
2
62
|
export class EngineApiClient {
|
|
3
63
|
port;
|
|
4
64
|
token;
|
|
@@ -82,25 +142,80 @@ export class EngineApiClient {
|
|
|
82
142
|
async getSelection() {
|
|
83
143
|
return this.request("GET", "/api/state/selection");
|
|
84
144
|
}
|
|
85
|
-
async
|
|
86
|
-
const
|
|
145
|
+
async writeSnapshotFile(kind, buffer, ext) {
|
|
146
|
+
const dir = join(tmpdir(), "summer-engine", "snapshots");
|
|
147
|
+
await mkdir(dir, { recursive: true });
|
|
148
|
+
const filename = `${kind}-${Date.now()}-${Math.random()
|
|
149
|
+
.toString(36)
|
|
150
|
+
.slice(2, 8)}.${ext}`;
|
|
151
|
+
const localPath = join(dir, filename);
|
|
152
|
+
await writeFile(localPath, buffer);
|
|
153
|
+
return localPath;
|
|
154
|
+
}
|
|
155
|
+
async snapshot(kind) {
|
|
156
|
+
const url = `http://127.0.0.1:${this.port}/api/snapshot/${kind}`;
|
|
87
157
|
const res = await fetch(url, {
|
|
88
158
|
headers: { Authorization: `Bearer ${this.token}` },
|
|
89
|
-
signal: AbortSignal.timeout(
|
|
159
|
+
signal: AbortSignal.timeout(30000),
|
|
90
160
|
});
|
|
91
|
-
if (!res.ok)
|
|
92
|
-
|
|
93
|
-
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const text = await res.text().catch(() => "");
|
|
163
|
+
throw new Error(`Snapshot failed ${res.status}: ${text.slice(0, 200)}`);
|
|
164
|
+
}
|
|
165
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
166
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
167
|
+
const { format, mime, ext } = snapshotFormat({ mime: contentType });
|
|
168
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
169
|
+
const localPath = await this.writeSnapshotFile(kind, buffer, ext);
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
localPath,
|
|
173
|
+
path: localPath,
|
|
174
|
+
format,
|
|
175
|
+
mime,
|
|
176
|
+
context: kind === "viewport" ? "scene" : "game",
|
|
177
|
+
bytes: buffer.byteLength,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const response = await res.json();
|
|
181
|
+
const payload = findSnapshotPayload(response);
|
|
182
|
+
if (!payload) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: "Snapshot response did not include a viewport/game payload.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const base64 = stringFrom(payload.base64) ?? stringFrom(payload.image_base64);
|
|
189
|
+
if (!base64) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: stringFrom(payload.error) ?? "Snapshot response did not include image data.",
|
|
193
|
+
metadata: withoutImageData(payload),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const { format, mime, ext } = snapshotFormat(payload);
|
|
197
|
+
const buffer = Buffer.from(base64, "base64");
|
|
198
|
+
const localPath = await this.writeSnapshotFile(kind, buffer, ext);
|
|
199
|
+
return {
|
|
200
|
+
ok: payload.ok !== false,
|
|
201
|
+
localPath,
|
|
202
|
+
path: localPath,
|
|
203
|
+
width: numberFrom(payload.width),
|
|
204
|
+
height: numberFrom(payload.height),
|
|
205
|
+
format,
|
|
206
|
+
mime,
|
|
207
|
+
context: stringFrom(payload.context),
|
|
208
|
+
op: stringFrom(payload.op),
|
|
209
|
+
provenance: payload.provenance,
|
|
210
|
+
bytes: buffer.byteLength,
|
|
211
|
+
metadata: withoutImageData(payload),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async viewportSnapshot() {
|
|
215
|
+
return this.snapshot("viewport");
|
|
94
216
|
}
|
|
95
217
|
async gameSnapshot() {
|
|
96
|
-
|
|
97
|
-
const res = await fetch(url, {
|
|
98
|
-
headers: { Authorization: `Bearer ${this.token}` },
|
|
99
|
-
signal: AbortSignal.timeout(15000),
|
|
100
|
-
});
|
|
101
|
-
if (!res.ok)
|
|
102
|
-
throw new Error(`Game snapshot failed: ${res.status}`);
|
|
103
|
-
return Buffer.from(await res.arrayBuffer());
|
|
218
|
+
return this.snapshot("game");
|
|
104
219
|
}
|
|
105
220
|
getPort() {
|
|
106
221
|
return this.port;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare const AGENT_CLIENTS: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
2
|
+
export type AgentClient = (typeof AGENT_CLIENTS)[number];
|
|
3
|
+
export type SkillCategory = "workflow" | "scripting" | "scene" | "gameplay" | "lighting" | "assets" | "ui";
|
|
4
|
+
export interface SkillRegistryEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
public: boolean;
|
|
7
|
+
category: SkillCategory;
|
|
8
|
+
clients: readonly AgentClient[];
|
|
9
|
+
recommended: boolean;
|
|
10
|
+
requiresMcpTools: readonly string[];
|
|
11
|
+
testScenario: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const SKILL_REGISTRY: readonly [{
|
|
14
|
+
readonly name: "gdscript-patterns";
|
|
15
|
+
readonly public: true;
|
|
16
|
+
readonly category: "scripting";
|
|
17
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
18
|
+
readonly recommended: true;
|
|
19
|
+
readonly requiresMcpTools: readonly ["summer_set_prop", "summer_connect_signal", "summer_input_map_bind", "summer_get_script_errors"];
|
|
20
|
+
readonly testScenario: "Create a typed GDScript with a signal, attach it to a node, connect the signal, and verify script errors.";
|
|
21
|
+
}, {
|
|
22
|
+
readonly name: "scene-composition";
|
|
23
|
+
readonly public: true;
|
|
24
|
+
readonly category: "scene";
|
|
25
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
26
|
+
readonly recommended: true;
|
|
27
|
+
readonly requiresMcpTools: readonly ["summer_create_scene", "summer_open_scene", "summer_get_scene_tree", "summer_add_node", "summer_instantiate_scene", "summer_save_scene"];
|
|
28
|
+
readonly testScenario: "Build a small reusable player scene, instantiate it in a main scene, and save both scenes through Summer tools.";
|
|
29
|
+
}, {
|
|
30
|
+
readonly name: "fps-controller";
|
|
31
|
+
readonly public: true;
|
|
32
|
+
readonly category: "gameplay";
|
|
33
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
34
|
+
readonly recommended: true;
|
|
35
|
+
readonly requiresMcpTools: readonly ["summer_add_node", "summer_set_prop", "summer_set_resource_property", "summer_input_map_bind"];
|
|
36
|
+
readonly testScenario: "Create a CharacterBody3D player with collision, camera, input actions, and a host-edited controller script.";
|
|
37
|
+
}, {
|
|
38
|
+
readonly name: "3d-lighting";
|
|
39
|
+
readonly public: true;
|
|
40
|
+
readonly category: "lighting";
|
|
41
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
42
|
+
readonly recommended: true;
|
|
43
|
+
readonly requiresMcpTools: readonly ["summer_add_node", "summer_set_prop", "summer_set_resource_property"];
|
|
44
|
+
readonly testScenario: "Add sun, fill lighting, WorldEnvironment, and shadow settings to a simple 3D scene.";
|
|
45
|
+
}, {
|
|
46
|
+
readonly name: "ui-basics";
|
|
47
|
+
readonly public: true;
|
|
48
|
+
readonly category: "ui";
|
|
49
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
50
|
+
readonly recommended: true;
|
|
51
|
+
readonly requiresMcpTools: readonly ["summer_add_node", "summer_set_prop", "summer_connect_signal"];
|
|
52
|
+
readonly testScenario: "Create a HUD CanvasLayer with labels, a ProgressBar, and a connected Button signal.";
|
|
53
|
+
}, {
|
|
54
|
+
readonly name: "asset-strategy";
|
|
55
|
+
readonly public: true;
|
|
56
|
+
readonly category: "assets";
|
|
57
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
58
|
+
readonly recommended: true;
|
|
59
|
+
readonly requiresMcpTools: readonly ["summer_search_assets", "summer_import_asset", "summer_import_from_url", "summer_generate_image", "summer_generate_3d", "summer_generate_audio"];
|
|
60
|
+
readonly testScenario: "Choose library, generated, or primitive assets for a small level and import one asset into the project.";
|
|
61
|
+
}, {
|
|
62
|
+
readonly name: "make-game";
|
|
63
|
+
readonly public: true;
|
|
64
|
+
readonly category: "workflow";
|
|
65
|
+
readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf"];
|
|
66
|
+
readonly recommended: false;
|
|
67
|
+
readonly requiresMcpTools: readonly ["summer_get_agent_playbook", "summer_get_project_context", "summer_create_scene", "summer_open_scene", "summer_save_scene", "summer_add_node", "summer_set_prop", "summer_set_resource_property", "summer_instantiate_scene", "summer_play", "summer_stop", "summer_get_diagnostics"];
|
|
68
|
+
readonly testScenario: "Build one narrow playable prototype from requirements through scene setup, host-edited scripts, and diagnostics.";
|
|
69
|
+
}];
|