summer-engine 2.6.6 → 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.
Files changed (168) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/plugin.json +6 -2
  4. package/.cursor-plugin/plugin.json +5 -1
  5. package/.opencode/INSTALL.md +1 -1
  6. package/.opencode/plugins/summer.js +2 -2
  7. package/AGENTS.md +11 -7
  8. package/CHANGELOG.md +40 -0
  9. package/GEMINI.md +9 -7
  10. package/README.md +63 -21
  11. package/_persona/summer/SKILL.md +1 -0
  12. package/assets/autopilot/README.md +51 -0
  13. package/assets/autopilot/autopilot.gd +222 -0
  14. package/assets/autopilot/probe_base.gd +262 -0
  15. package/assets/autopilot/run.sh +78 -0
  16. package/commands/gameskill.md +11 -2
  17. package/commands/summer.md +13 -0
  18. package/dist/bin/summer.js +12 -0
  19. package/dist/commands/config.d.ts +2 -0
  20. package/dist/commands/config.js +53 -0
  21. package/dist/commands/create.d.ts +1 -0
  22. package/dist/commands/create.js +38 -6
  23. package/dist/commands/debug.d.ts +2 -0
  24. package/dist/commands/debug.js +44 -0
  25. package/dist/commands/doctor.js +112 -8
  26. package/dist/commands/list.js +1 -1
  27. package/dist/commands/login.d.ts +19 -0
  28. package/dist/commands/login.js +123 -36
  29. package/dist/commands/logout.js +3 -16
  30. package/dist/commands/logs.d.ts +2 -0
  31. package/dist/commands/logs.js +19 -0
  32. package/dist/commands/mcp.js +7 -2
  33. package/dist/commands/open.js +1 -1
  34. package/dist/commands/publish.d.ts +2 -0
  35. package/dist/commands/publish.js +20 -0
  36. package/dist/commands/releases.d.ts +2 -0
  37. package/dist/commands/releases.js +20 -0
  38. package/dist/commands/run.js +1 -1
  39. package/dist/commands/skills.js +1 -1
  40. package/dist/lib/api-client.d.ts +15 -4
  41. package/dist/lib/api-client.js +138 -13
  42. package/dist/lib/async-op-lifecycle.d.ts +7 -2
  43. package/dist/lib/async-op-lifecycle.js +32 -9
  44. package/dist/lib/auth.d.ts +40 -12
  45. package/dist/lib/auth.js +173 -37
  46. package/dist/lib/cloud/engine-bridge.d.ts +17 -4
  47. package/dist/lib/cloud/engine-bridge.js +32 -6
  48. package/dist/lib/config.d.ts +20 -0
  49. package/dist/lib/config.js +142 -0
  50. package/dist/lib/creator.d.ts +93 -0
  51. package/dist/lib/creator.js +393 -0
  52. package/dist/lib/debug-report.d.ts +50 -0
  53. package/dist/lib/debug-report.js +308 -0
  54. package/dist/lib/engine.d.ts +35 -2
  55. package/dist/lib/engine.js +240 -6
  56. package/dist/lib/game-task-plan.js +4 -4
  57. package/dist/lib/mcp-boot-notice.d.ts +3 -0
  58. package/dist/lib/mcp-boot-notice.js +7 -0
  59. package/dist/lib/mcp-log.d.ts +3 -0
  60. package/dist/lib/mcp-log.js +72 -0
  61. package/dist/lib/public-surface-paths.d.ts +13 -0
  62. package/dist/lib/public-surface-paths.js +42 -0
  63. package/dist/lib/skills-registry.d.ts +33 -1
  64. package/dist/lib/skills-registry.js +55 -0
  65. package/dist/lib/store.d.ts +14 -0
  66. package/dist/lib/store.js +145 -0
  67. package/dist/lib/summer-compatibility.d.ts +6 -0
  68. package/dist/lib/summer-compatibility.js +22 -0
  69. package/dist/mcp/server.d.ts +10 -3
  70. package/dist/mcp/server.js +104 -11
  71. package/dist/mcp/tools/asset-tools.js +33 -12
  72. package/dist/mcp/tools/cloud-tools.js +15 -15
  73. package/dist/mcp/tools/creator-tools.d.ts +2 -0
  74. package/dist/mcp/tools/creator-tools.js +105 -0
  75. package/dist/mcp/tools/debug-tools.js +44 -1
  76. package/dist/mcp/tools/file-tools.d.ts +2 -0
  77. package/dist/mcp/tools/file-tools.js +154 -0
  78. package/dist/mcp/tools/generate-tools.js +159 -11
  79. package/dist/mcp/tools/project-tools.js +37 -27
  80. package/dist/mcp/tools/scene-tools.js +106 -29
  81. package/dist/mcp/tools/visual-tools.js +4 -3
  82. package/dist/mcp/tools/with-engine.js +4 -4
  83. package/hooks/session-start.sh +5 -5
  84. package/package.json +3 -1
  85. package/references/collaborative-protocol.md +50 -0
  86. package/references/gd-style.md +121 -0
  87. package/references/godot-version.md +51 -0
  88. package/references/mcp-tools-reference.md +199 -0
  89. package/references/public-surface-language.md +30 -0
  90. package/references/summer-folder.md +90 -0
  91. package/references/template-registry.md +73 -0
  92. package/skills/2d-assets/character-portrait/SKILL.md +17 -12
  93. package/skills/2d-assets/concept-art/SKILL.md +10 -9
  94. package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
  95. package/skills/2d-assets/pixel-art/SKILL.md +36 -20
  96. package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
  97. package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
  98. package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
  99. package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
  100. package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
  101. package/skills/3d-assets/character-model/SKILL.md +34 -19
  102. package/skills/3d-assets/environment-kit/SKILL.md +11 -5
  103. package/skills/3d-assets/organic-model/SKILL.md +8 -5
  104. package/skills/3d-assets/prop-model/SKILL.md +9 -3
  105. package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
  106. package/skills/README.md +1 -1
  107. package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
  108. package/skills/animation/animation-tree/SKILL.md +39 -15
  109. package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
  110. package/skills/animation/generate-motion/SKILL.md +47 -15
  111. package/skills/animation/procedural-animation/SKILL.md +31 -13
  112. package/skills/animation/retarget/SKILL.md +127 -79
  113. package/skills/audio/adaptive-music/SKILL.md +24 -20
  114. package/skills/audio/ambient-bed/SKILL.md +22 -16
  115. package/skills/audio/audio-direction/SKILL.md +15 -10
  116. package/skills/audio/music-track/SKILL.md +18 -12
  117. package/skills/audio/sound-effect/SKILL.md +17 -12
  118. package/skills/audio/voice-line/SKILL.md +42 -42
  119. package/skills/character-controllers/fps-controller/SKILL.md +26 -16
  120. package/skills/debugging/debug/SKILL.md +74 -20
  121. package/skills/deployment/export-and-ship/SKILL.md +105 -38
  122. package/skills/deployment/remote-deploy/SKILL.md +140 -0
  123. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
  124. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
  125. package/skills/level-design/design-level/SKILL.md +22 -8
  126. package/skills/level-design/scene-to-level/SKILL.md +13 -7
  127. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
  128. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
  129. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
  130. package/skills/performance/tune-performance/SKILL.md +51 -32
  131. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
  132. package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
  133. package/skills/scene-and-project/brainstorm-game/SKILL.md +7 -9
  134. package/skills/scene-and-project/make-game/SKILL.md +2 -1
  135. package/skills/scene-and-project/play/SKILL.md +10 -4
  136. package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
  137. package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
  138. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
  139. package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
  140. package/skills/video/animated-loop/SKILL.md +63 -39
  141. package/skills/video/cinematic-cutscene/SKILL.md +66 -30
  142. package/skills/video/trailer-shot/SKILL.md +30 -21
  143. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
  144. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
  145. package/skills/visual-effects/game-feel/SKILL.md +51 -33
  146. package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
  147. package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
  148. package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
  149. package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
  150. package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
  151. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
  152. package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
  153. package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
  154. package/skills/workflow/brainstorming/SKILL.md +1 -1
  155. package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
  156. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
  157. package/skills/workflow/gameskill/SKILL.md +11 -2
  158. package/skills/workflow/headless-scripting/SKILL.md +407 -0
  159. package/skills/workflow/investigating-bugs/SKILL.md +4 -2
  160. package/skills/workflow/mcpupdate/SKILL.md +9 -9
  161. package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
  162. package/skills/workflow/skill-create/SKILL.md +7 -7
  163. package/skills/workflow/skill-test/SKILL.md +3 -3
  164. package/skills/workflow/using-summer/SKILL.md +32 -3
  165. package/skills/workflow/verification-before-completion/SKILL.md +2 -0
  166. package/skills/workflow/writing-plans/SKILL.md +7 -7
  167. package/skills/workflow/writing-skills/SKILL.md +7 -5
  168. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
@@ -0,0 +1,308 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { hostname, platform, release } from "node:os";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { EngineApiClient } from "./api-client.js";
6
+ import { shapeEngineLogResponse } from "./log-filters.js";
7
+ import { tildeify } from "./format.js";
8
+ import { getMcpLogPath, readRecentMcpLogLines } from "./mcp-log.js";
9
+ import { runDoctor } from "../commands/doctor.js";
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require("../../package.json");
12
+ function sleep(ms) {
13
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
14
+ }
15
+ function errorMessage(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ function timestampForFile(date) {
19
+ return date.toISOString().replace(/[:.]/g, "-");
20
+ }
21
+ export function defaultDebugReportFilename(date = new Date()) {
22
+ return `summer-debug-report-${timestampForFile(date)}.md`;
23
+ }
24
+ function asRecord(value) {
25
+ return value && typeof value === "object" && !Array.isArray(value)
26
+ ? value
27
+ : null;
28
+ }
29
+ function pickString(value, keys) {
30
+ const record = asRecord(value);
31
+ if (!record)
32
+ return null;
33
+ for (const key of keys) {
34
+ const candidate = record[key];
35
+ if (typeof candidate === "string" && candidate.length > 0)
36
+ return candidate;
37
+ }
38
+ return null;
39
+ }
40
+ function projectPathFromReport(report) {
41
+ return pickString(report.engine.health, ["project_path", "projectPath", "projectRoot", "project_root"]);
42
+ }
43
+ function resolveOutputPath(report, options) {
44
+ if (options.outputPath) {
45
+ return isAbsolute(options.outputPath)
46
+ ? options.outputPath
47
+ : resolve(process.cwd(), options.outputPath);
48
+ }
49
+ const baseDir = options.baseDir ?? projectPathFromReport(report) ?? process.cwd();
50
+ return join(baseDir, defaultDebugReportFilename(new Date(report.generatedAt)));
51
+ }
52
+ async function writeMarkdown(path, markdown) {
53
+ await mkdir(dirname(path), { recursive: true });
54
+ await writeFile(path, markdown, "utf-8");
55
+ }
56
+ async function safeCollect(label, fn) {
57
+ try {
58
+ return await fn();
59
+ }
60
+ catch (error) {
61
+ return {
62
+ ok: false,
63
+ label,
64
+ error: errorMessage(error),
65
+ };
66
+ }
67
+ }
68
+ function firstOpsResult(value) {
69
+ const root = asRecord(value);
70
+ const results = root?.results;
71
+ if (Array.isArray(results) && results.length > 0) {
72
+ return asRecord(results[0]);
73
+ }
74
+ return root;
75
+ }
76
+ function isGamePlaying(value) {
77
+ return firstOpsResult(value)?.playing === true;
78
+ }
79
+ async function collectConsole(client, maxConsoleLines) {
80
+ const raw = await client.executeOps([
81
+ { op: "GetConsoleOutput", max_lines: maxConsoleLines },
82
+ ]);
83
+ return shapeEngineLogResponse(raw, {
84
+ errorsOnly: false,
85
+ maxEntries: maxConsoleLines,
86
+ }).result;
87
+ }
88
+ async function collectDebugger(client, type, maxEntries) {
89
+ const op = {
90
+ op: "GetDebuggerErrors",
91
+ max_errors: maxEntries,
92
+ include_stack: true,
93
+ };
94
+ if (type === "warning")
95
+ op.type = "warning";
96
+ const raw = await client.executeOps([op]);
97
+ return shapeEngineLogResponse(raw, { maxEntries }).result;
98
+ }
99
+ async function collectPlaySession(client, options) {
100
+ const session = {
101
+ requested: true,
102
+ waitMs: options.playWaitMs,
103
+ };
104
+ session.before = await safeCollect("is-running-before", () => client.executeOps([{ op: "IsGameRunning" }]));
105
+ const wasPlaying = isGamePlaying(session.before);
106
+ session.wasPlayingBefore = wasPlaying;
107
+ if (wasPlaying) {
108
+ session.note =
109
+ "Game was already running. Collected current runtime diagnostics without clearing console, launching, or stopping.";
110
+ }
111
+ else {
112
+ session.clearConsole = await safeCollect("clear-console", () => client.executeOps([{ op: "ClearConsoleOutput" }]));
113
+ session.play = await safeCollect("play", () => client.play());
114
+ }
115
+ await sleep(options.playWaitMs);
116
+ session.diagnostics = await safeCollect("diagnostics-after-play", () => client.getDiagnostics());
117
+ session.console = await safeCollect("console-after-play", () => collectConsole(client, options.maxConsoleLines));
118
+ session.debuggerErrors = await safeCollect("debugger-errors-after-play", () => collectDebugger(client, "error", options.maxDebuggerEntries));
119
+ session.debuggerWarnings = await safeCollect("debugger-warnings-after-play", () => collectDebugger(client, "warning", options.maxDebuggerEntries));
120
+ if (!wasPlaying) {
121
+ session.stop = await safeCollect("stop", () => client.stop());
122
+ }
123
+ return session;
124
+ }
125
+ function redact(value) {
126
+ if (Array.isArray(value))
127
+ return value.map((item) => redact(item));
128
+ if (!value || typeof value !== "object")
129
+ return value;
130
+ const out = {};
131
+ for (const [key, nested] of Object.entries(value)) {
132
+ if (/token|secret|password|authorization|cookie|api[-_]?key|hostname/i.test(key)) {
133
+ out[key] = "[REDACTED]";
134
+ }
135
+ else {
136
+ out[key] = redact(nested);
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+ export function redactDebugReport(report) {
142
+ return redact(report);
143
+ }
144
+ function jsonBlock(value) {
145
+ return `\`\`\`json\n${JSON.stringify(redact(value), null, 2)}\n\`\`\``;
146
+ }
147
+ function doctorSummary(doctor) {
148
+ if (!doctor)
149
+ return "Doctor was skipped.";
150
+ return `${doctor.summary.failures} failures, ${doctor.summary.warnings} warnings, ${doctor.summary.ok} OK.`;
151
+ }
152
+ function issueText(issue) {
153
+ return issue && issue.trim().length > 0 ? issue.trim() : "No issue text provided.";
154
+ }
155
+ export function renderDebugReportMarkdown(report) {
156
+ const health = asRecord(report.engine.health);
157
+ const projectName = pickString(health, ["project_name", "projectName"]) ?? "unknown";
158
+ const projectPath = pickString(health, ["project_path", "projectPath"]) ?? "unknown";
159
+ const scene = pickString(health, ["scene", "scenePath", "scene_path"]) ?? "unknown";
160
+ return `# Summer Debug Report
161
+
162
+ Generated: ${report.generatedAt}
163
+
164
+ ## Before Sending
165
+
166
+ This report is meant for Summer Engine support or an assisting AI agent. It does not include auth tokens or project file contents, but it may include local paths, project names, console output, and stack traces. Review it before sending.
167
+
168
+ ## User Issue
169
+
170
+ ${issueText(report.issue)}
171
+
172
+ ## Quick Summary
173
+
174
+ - Doctor: ${doctorSummary(report.doctor)}
175
+ - Engine connected: ${report.engine.connected ? "yes" : "no"}
176
+ - Project: ${projectName}
177
+ - Project path: ${tildeify(projectPath)}
178
+ - Scene: ${scene}
179
+
180
+ ## Environment
181
+
182
+ ${jsonBlock({
183
+ ...report.environment,
184
+ cwd: tildeify(report.environment.cwd),
185
+ hostname: report.environment.hostname ? "[local-hostname-present]" : "",
186
+ })}
187
+
188
+ ## Doctor Checks
189
+
190
+ ${report.doctor ? jsonBlock(report.doctor) : "Doctor was skipped."}
191
+
192
+ ## MCP Lifecycle Log
193
+
194
+ Log path: ${tildeify(report.mcp.logPath)}
195
+
196
+ ${report.mcp.recentLog.length > 0 ? `\`\`\`jsonl\n${report.mcp.recentLog.join("\n")}\n\`\`\`` : "No MCP lifecycle log entries found."}
197
+
198
+ ## Engine Health
199
+
200
+ ${report.engine.health ? jsonBlock(report.engine.health) : report.engine.error ?? "Engine was not reachable."}
201
+
202
+ ## Project State
203
+
204
+ ${report.engine.project ? jsonBlock(report.engine.project) : "No project state collected."}
205
+
206
+ ## Scene State
207
+
208
+ ${report.engine.scene ? jsonBlock(report.engine.scene) : "No scene state collected."}
209
+
210
+ ## Diagnostics
211
+
212
+ ${report.engine.diagnostics ? jsonBlock(report.engine.diagnostics) : "No diagnostics collected."}
213
+
214
+ ## Console
215
+
216
+ ${report.engine.console ? jsonBlock(report.engine.console) : "No console output collected."}
217
+
218
+ ## Debugger Errors
219
+
220
+ ${report.engine.debuggerErrors ? jsonBlock(report.engine.debuggerErrors) : "No debugger errors collected."}
221
+
222
+ ## Debugger Warnings
223
+
224
+ ${report.engine.debuggerWarnings ? jsonBlock(report.engine.debuggerWarnings) : "No debugger warnings collected."}
225
+
226
+ ## Play Session
227
+
228
+ ${report.engine.playSession ? jsonBlock(report.engine.playSession) : "No play session requested. Re-run with --play if the issue only appears while the game is running."}
229
+
230
+ ## Agent Handoff Prompt
231
+
232
+ \`\`\`text
233
+ You are debugging a Summer Engine project. Use the Summer debug skill and this report as the starting evidence.
234
+
235
+ User issue:
236
+ ${issueText(report.issue)}
237
+
238
+ Process:
239
+ 1. Start with the collected diagnostics, console output, and debugger errors in this report.
240
+ 2. Form one specific hypothesis before editing.
241
+ 3. Ask before making project changes.
242
+ 4. After a fix, re-run the diagnostic that found the issue and update the report or final answer with the exact evidence.
243
+ \`\`\`
244
+ `;
245
+ }
246
+ export async function collectDebugReport(options = {}) {
247
+ const generatedAt = new Date().toISOString();
248
+ const includeDoctor = options.includeDoctor ?? true;
249
+ const report = {
250
+ generatedAt,
251
+ issue: options.issue?.trim() || null,
252
+ environment: {
253
+ cliVersion: version,
254
+ nodeVersion: process.version,
255
+ platform: platform(),
256
+ release: release(),
257
+ cwd: process.cwd(),
258
+ hostname: hostname(),
259
+ },
260
+ doctor: null,
261
+ mcp: {
262
+ logPath: getMcpLogPath(),
263
+ recentLog: [],
264
+ },
265
+ engine: {
266
+ connected: false,
267
+ },
268
+ };
269
+ if (includeDoctor) {
270
+ report.doctor = await runDoctor({ quiet: true });
271
+ }
272
+ report.mcp.recentLog = await readRecentMcpLogLines();
273
+ let client;
274
+ try {
275
+ client = await EngineApiClient.connect();
276
+ report.engine.connected = true;
277
+ }
278
+ catch (error) {
279
+ report.engine.error = errorMessage(error);
280
+ report.mcp.recentLog = await readRecentMcpLogLines();
281
+ return report;
282
+ }
283
+ const maxConsoleLines = options.maxConsoleLines ?? 200;
284
+ const maxDebuggerEntries = options.maxDebuggerEntries ?? 100;
285
+ report.engine.health = await safeCollect("health", () => client.health());
286
+ report.engine.project = await safeCollect("project-state", () => client.getProjectState());
287
+ report.engine.scene = await safeCollect("scene-state", () => client.getSceneState());
288
+ report.engine.diagnostics = await safeCollect("diagnostics", () => client.getDiagnostics());
289
+ report.engine.console = await safeCollect("console", () => collectConsole(client, maxConsoleLines));
290
+ report.engine.debuggerErrors = await safeCollect("debugger-errors", () => collectDebugger(client, "error", maxDebuggerEntries));
291
+ report.engine.debuggerWarnings = await safeCollect("debugger-warnings", () => collectDebugger(client, "warning", maxDebuggerEntries));
292
+ if (options.includePlaySession) {
293
+ report.engine.playSession = await collectPlaySession(client, {
294
+ playWaitMs: options.playWaitMs ?? 2500,
295
+ maxConsoleLines,
296
+ maxDebuggerEntries,
297
+ });
298
+ }
299
+ report.mcp.recentLog = await readRecentMcpLogLines();
300
+ return report;
301
+ }
302
+ export async function createDebugReportArtifact(options = {}) {
303
+ const report = await collectDebugReport(options);
304
+ const markdown = renderDebugReportMarkdown(report);
305
+ const path = resolveOutputPath(report, options);
306
+ await writeMarkdown(path, markdown);
307
+ return { path, markdown, report };
308
+ }
@@ -1,5 +1,38 @@
1
- export declare function getApiToken(): Promise<string | null>;
2
- export declare function getApiPort(): Promise<number>;
1
+ export interface EngineSelection {
2
+ instanceId?: string;
3
+ projectPath?: string;
4
+ cwd?: string;
5
+ }
6
+ export interface EngineDiscoveryOptions {
7
+ summerDir?: string;
8
+ nowMs?: number;
9
+ }
10
+ export interface EngineInstance {
11
+ schemaVersion: 1;
12
+ instanceId: string;
13
+ pid: number;
14
+ port: number;
15
+ token: string;
16
+ projectId?: string;
17
+ projectIdHash?: string;
18
+ resourceRoot: string;
19
+ projectName?: string;
20
+ heartbeatAt: number;
21
+ engineVersion?: string;
22
+ }
23
+ export interface EngineConnection {
24
+ port: number;
25
+ token: string;
26
+ health: EngineHealth;
27
+ instance?: EngineInstance;
28
+ selection: EngineSelection | null;
29
+ source: "registry" | "legacy";
30
+ }
31
+ export declare function getApiToken(summerDir?: string): Promise<string | null>;
32
+ export declare function getApiPort(summerDir?: string): Promise<number>;
33
+ export declare function findProjectRoot(startPath: string): Promise<string | null>;
34
+ export declare function listEngineInstances(nowMs?: number, summerDir?: string): Promise<EngineInstance[]>;
35
+ export declare function resolveEngineConnection(selection?: EngineSelection, options?: EngineDiscoveryOptions): Promise<EngineConnection>;
3
36
  export interface EngineHealth {
4
37
  ok: boolean;
5
38
  engine: string;
@@ -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
- export async function getApiToken() {
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(getSummerDir(), "api-token"), "utf-8");
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(getSummerDir(), "api-port"), "utf-8");
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
@@ -249,7 +249,7 @@ function inferSkills(mode, target, goal) {
249
249
  addSkill(routes, "design-mechanic", "Implement the core shooting/combat loop.", 230);
250
250
  addSkill(routes, "design-level", "Shape the arena, spawn space, cover, and pacing.", 210);
251
251
  }
252
- addSkill(routes, "gdscript-patterns", "Use direct host file edits for scripts, then verify through MCP.", 90);
252
+ addSkill(routes, "gdscript-patterns", "Use guarded Summer file tools for scripts, then verify through MCP.", 90);
253
253
  addSkill(routes, "verification-before-completion", "Do not finish without a clean verification story.", 80);
254
254
  return routes
255
255
  .sort((a, b) => b.weight - a.weight)
@@ -350,7 +350,7 @@ export function buildGameTaskPlan(options) {
350
350
  verification,
351
351
  principles: [
352
352
  "The agent should build a playable game slice, not a static demo.",
353
- "Use host file edits for code/data/docs; use MCP for live scene state, imports, inspector values, play, and diagnostics.",
353
+ "Use identity-bound Summer file tools for code/data/docs and Summer scene tools for live hierarchy/inspector changes.",
354
354
  "Reuse/import assets before paid generation unless the user explicitly wants custom output.",
355
355
  "Prefer exact IDs and structured tool results over search guesses after asset creation.",
356
356
  "Verify through the engine before claiming completion.",
@@ -358,8 +358,8 @@ export function buildGameTaskPlan(options) {
358
358
  recommendedSkills: inferSkills(mode, target, goal),
359
359
  mcpToolPlan: buildMcpToolPlan(mode, target, assetPolicy, verification, goal),
360
360
  hostFileWork: [
361
- "Write and edit .gd/.cs scripts directly with host file tools.",
362
- "Use direct file edits for JSON, markdown, project notes, and simple resources.",
361
+ "Read and mutate project text through summer_read_file, summer_replace_text, and guarded summer_write_file.",
362
+ "Use create_only:true for new files and an engine sha256 receipt for overwrites.",
363
363
  "Read relevant .summer memory files when project context reports them.",
364
364
  ],
365
365
  userGates: buildUserGates(mode, assetPolicy),
@@ -0,0 +1,3 @@
1
+ import type { BootDriftNotice } from "./version-check.js";
2
+ export declare function getCachedBootDriftNotice(): BootDriftNotice | null;
3
+ export declare function setCachedBootDriftNotice(notice: BootDriftNotice | null): void;
@@ -0,0 +1,7 @@
1
+ let cachedBootDriftNotice = null;
2
+ export function getCachedBootDriftNotice() {
3
+ return cachedBootDriftNotice;
4
+ }
5
+ export function setCachedBootDriftNotice(notice) {
6
+ cachedBootDriftNotice = notice;
7
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getMcpLogPath(): string;
2
+ export declare function appendMcpLogEvent(event: string, details?: Record<string, unknown>): void;
3
+ export declare function readRecentMcpLogLines(maxLines?: number): Promise<string[]>;