summer-engine 2.2.0 → 2.5.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 (176) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +20 -5
  3. package/.codex-plugin/plugin.json +15 -5
  4. package/.cursor-plugin/plugin.json +15 -5
  5. package/.factory-plugin/plugin.json +2 -2
  6. package/.opencode/INSTALL.md +1 -1
  7. package/CHANGELOG.md +27 -6
  8. package/CLAUDE.md +2 -2
  9. package/GEMINI.md +2 -3
  10. package/README.md +82 -58
  11. package/_persona/summer/SKILL.md +5 -6
  12. package/commands/gameskill.md +103 -0
  13. package/dist/bin/summer.js +12 -4
  14. package/dist/commands/create.js +2 -0
  15. package/dist/commands/doctor.js +71 -1
  16. package/dist/commands/install.d.ts +17 -0
  17. package/dist/commands/install.js +63 -4
  18. package/dist/commands/memory.d.ts +14 -0
  19. package/dist/commands/memory.js +196 -0
  20. package/dist/commands/setup.js +12 -2
  21. package/dist/commands/skills.js +205 -8
  22. package/dist/commands/status.js +3 -0
  23. package/dist/lib/agent-config.d.ts +2 -2
  24. package/dist/lib/agent-config.js +298 -5
  25. package/dist/lib/api-client.d.ts +13 -0
  26. package/dist/lib/api-client.js +63 -26
  27. package/dist/lib/async-op-lifecycle.d.ts +60 -0
  28. package/dist/lib/async-op-lifecycle.js +83 -0
  29. package/dist/lib/project-memory.d.ts +33 -0
  30. package/dist/lib/project-memory.js +220 -0
  31. package/dist/lib/skills-registry.d.ts +134 -61
  32. package/dist/lib/skills-registry.js +106 -4
  33. package/dist/lib/version-check.d.ts +106 -0
  34. package/dist/lib/version-check.js +338 -0
  35. package/dist/mcp/server.d.ts +2 -0
  36. package/dist/mcp/server.js +81 -0
  37. package/dist/mcp/tools/debug-tools.js +32 -2
  38. package/dist/mcp/tools/project-tools.js +18 -6
  39. package/dist/mcp/tools/with-engine.d.ts +14 -0
  40. package/dist/mcp/tools/with-engine.js +52 -7
  41. package/gemini-extension.json +2 -2
  42. package/hooks/pre-commit-doctor.sh +0 -0
  43. package/hooks/run-hook.cmd +0 -0
  44. package/hooks/session-start.sh +0 -0
  45. package/package.json +6 -4
  46. package/skills/2d-assets/character-portrait/SKILL.md +1 -1
  47. package/skills/2d-assets/concept-art/SKILL.md +1 -1
  48. package/skills/2d-assets/create-asset-sheet/SKILL.md +163 -0
  49. package/skills/2d-assets/instantiate-asset-pack/SKILL.md +245 -0
  50. package/skills/2d-assets/pixel-art/SKILL.md +1 -1
  51. package/skills/2d-assets/skybox-panorama/SKILL.md +1 -1
  52. package/skills/2d-assets/sprite-sheet/SKILL.md +1 -1
  53. package/skills/2d-assets/tileable-texture/SKILL.md +1 -1
  54. package/skills/2d-assets/ui-graphics/SKILL.md +1 -1
  55. package/skills/2d-assets/use-widget-asset/SKILL.md +217 -0
  56. package/skills/3d-assets/character-model/SKILL.md +1 -1
  57. package/skills/3d-assets/organic-model/SKILL.md +1 -1
  58. package/skills/3d-assets/prop-model/SKILL.md +1 -1
  59. package/skills/3d-assets/vehicle-model/SKILL.md +1 -1
  60. package/skills/README.md +37 -30
  61. package/skills/ai-and-npcs/design-npc/SKILL.md +6 -6
  62. package/skills/animation/animation-tree/SKILL.md +2 -2
  63. package/skills/animation/facial-and-lipsync/SKILL.md +1 -1
  64. package/skills/animation/generate-motion/SKILL.md +1 -1
  65. package/skills/animation/procedural-animation/SKILL.md +2 -2
  66. package/skills/audio/adaptive-music/SKILL.md +1 -1
  67. package/skills/audio/audio-direction/SKILL.md +5 -5
  68. package/skills/audio/sound-effect/SKILL.md +2 -2
  69. package/skills/audio/voice-line/SKILL.md +25 -3
  70. package/skills/character-controllers/fps-controller/SKILL.md +5 -5
  71. package/skills/debugging/debug/SKILL.md +39 -1
  72. package/skills/deployment/export-and-ship/SKILL.md +4 -4
  73. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +170 -0
  74. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +8 -8
  75. package/skills/level-design/design-level/SKILL.md +5 -5
  76. package/skills/level-design/scene-to-level/SKILL.md +125 -0
  77. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +4 -4
  78. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +4 -4
  79. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +7 -7
  80. package/skills/performance/tune-performance/SKILL.md +5 -5
  81. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
  82. package/skills/rendering-and-lighting/art-direction/SKILL.md +5 -5
  83. package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
  84. package/skills/scene-and-project/play/SKILL.md +1 -1
  85. package/skills/scene-and-project/scene-composition/SKILL.md +30 -1
  86. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +53 -0
  87. package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
  88. package/skills/video/animated-loop/SKILL.md +1 -1
  89. package/skills/video/cinematic-cutscene/SKILL.md +1 -1
  90. package/skills/video/trailer-shot/SKILL.md +1 -1
  91. package/skills/visual-effects/game-feel/SKILL.md +6 -6
  92. package/skills/workflow/brainstorming/SKILL.md +164 -0
  93. package/skills/workflow/brainstorming/scripts/frame-template.html +214 -0
  94. package/skills/workflow/brainstorming/scripts/helper.js +88 -0
  95. package/skills/workflow/brainstorming/scripts/server.cjs +354 -0
  96. package/skills/workflow/brainstorming/scripts/start-server.sh +148 -0
  97. package/skills/workflow/brainstorming/scripts/stop-server.sh +56 -0
  98. package/skills/workflow/brainstorming/spec-document-reviewer-prompt.md +49 -0
  99. package/skills/workflow/brainstorming/visual-companion.md +287 -0
  100. package/skills/workflow/debugging-game-feel/SKILL.md +174 -0
  101. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +190 -0
  102. package/skills/workflow/dispatching-parallel-agents/SKILL.md +182 -0
  103. package/skills/workflow/gameskill/SKILL.md +111 -0
  104. package/skills/workflow/investigating-bugs/CREATION-LOG.md +119 -0
  105. package/skills/workflow/investigating-bugs/SKILL.md +296 -0
  106. package/skills/workflow/investigating-bugs/condition-based-waiting-example.ts +158 -0
  107. package/skills/workflow/investigating-bugs/condition-based-waiting.md +115 -0
  108. package/skills/workflow/investigating-bugs/defense-in-depth.md +122 -0
  109. package/skills/workflow/investigating-bugs/find-polluter.sh +63 -0
  110. package/skills/workflow/investigating-bugs/root-cause-tracing.md +169 -0
  111. package/skills/workflow/investigating-bugs/test-academic.md +14 -0
  112. package/skills/workflow/investigating-bugs/test-pressure-1.md +58 -0
  113. package/skills/workflow/investigating-bugs/test-pressure-2.md +68 -0
  114. package/skills/workflow/investigating-bugs/test-pressure-3.md +69 -0
  115. package/skills/workflow/playtesting-a-feature/SKILL.md +173 -0
  116. package/skills/{_meta → workflow}/skill-create/SKILL.md +32 -22
  117. package/skills/{_meta → workflow}/skill-improve/SKILL.md +9 -10
  118. package/skills/{_meta → workflow}/skill-test/SKILL.md +12 -12
  119. package/skills/{_meta → workflow}/using-summer/SKILL.md +5 -2
  120. package/skills/workflow/verification-before-completion/SKILL.md +139 -0
  121. package/skills/workflow/writing-plans/SKILL.md +152 -0
  122. package/skills/workflow/writing-plans/plan-document-reviewer-prompt.md +49 -0
  123. package/skills/workflow/writing-skills/SKILL.md +655 -0
  124. package/skills/workflow/writing-skills/anthropic-best-practices.md +1150 -0
  125. package/skills/workflow/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  126. package/skills/workflow/writing-skills/graphviz-conventions.dot +172 -0
  127. package/skills/workflow/writing-skills/persuasion-principles.md +187 -0
  128. package/skills/workflow/writing-skills/render-graphs.js +168 -0
  129. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +384 -0
  130. package/dist/commands/assets.d.ts +0 -2
  131. package/dist/commands/assets.js +0 -189
  132. package/dist/commands/debug.d.ts +0 -2
  133. package/dist/commands/debug.js +0 -127
  134. package/dist/commands/doctor.test.d.ts +0 -1
  135. package/dist/commands/doctor.test.js +0 -29
  136. package/dist/commands/project.d.ts +0 -2
  137. package/dist/commands/project.js +0 -170
  138. package/dist/commands/scene.d.ts +0 -2
  139. package/dist/commands/scene.js +0 -290
  140. package/dist/lib/agent-config.test.d.ts +0 -1
  141. package/dist/lib/agent-config.test.js +0 -80
  142. package/dist/lib/format.test.d.ts +0 -1
  143. package/dist/lib/format.test.js +0 -36
  144. package/dist/lib/plugin-manifests.test.d.ts +0 -1
  145. package/dist/lib/plugin-manifests.test.js +0 -134
  146. package/dist/lib/with-engine.d.ts +0 -14
  147. package/dist/lib/with-engine.js +0 -109
  148. package/skills/_shared/collaborative-protocol.md +0 -50
  149. package/skills/_shared/gd-style.md +0 -121
  150. package/skills/_shared/godot-version.md +0 -32
  151. package/skills/_shared/mcp-tools-reference.md +0 -133
  152. package/skills/_shared/summer-folder.md +0 -44
  153. package/skills/_shared/template-registry.md +0 -40
  154. package/skills/_tests/runner.md +0 -90
  155. package/skills/_tests/specs/3d-lighting.md +0 -19
  156. package/skills/_tests/specs/art-direction.md +0 -115
  157. package/skills/_tests/specs/asset-strategy.md +0 -19
  158. package/skills/_tests/specs/audio-direction.md +0 -123
  159. package/skills/_tests/specs/brainstorm-game.md +0 -99
  160. package/skills/_tests/specs/debug.md +0 -94
  161. package/skills/_tests/specs/design-level.md +0 -113
  162. package/skills/_tests/specs/design-mechanic.md +0 -112
  163. package/skills/_tests/specs/design-npc.md +0 -143
  164. package/skills/_tests/specs/export-and-ship.md +0 -114
  165. package/skills/_tests/specs/fps-controller.md +0 -113
  166. package/skills/_tests/specs/gdscript-patterns.md +0 -19
  167. package/skills/_tests/specs/host-authoritative-state.md +0 -104
  168. package/skills/_tests/specs/make-game.md +0 -19
  169. package/skills/_tests/specs/peer-to-peer-multiplayer.md +0 -99
  170. package/skills/_tests/specs/play.md +0 -88
  171. package/skills/_tests/specs/scene-composition.md +0 -19
  172. package/skills/_tests/specs/setup-multiplayer.md +0 -106
  173. package/skills/_tests/specs/tune-performance.md +0 -104
  174. package/skills/_tests/specs/ui-basics.md +0 -19
  175. package/skills/_tests/specs/vfx.md +0 -116
  176. package/skills/catalog.yaml +0 -991
@@ -2,13 +2,19 @@ import { existsSync } from "fs";
2
2
  import { mkdir, readFile, writeFile } from "fs/promises";
3
3
  import { dirname, join, resolve } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { homedir } from "os";
5
+ import { homedir, platform } from "os";
6
6
  export const SUMMER_MCP_SERVER_NAME = "summer-engine";
7
7
  export const supportedAgents = [
8
8
  "codex",
9
9
  "claude-code",
10
10
  "cursor",
11
11
  "windsurf",
12
+ "cline",
13
+ "roo-code",
14
+ "gemini",
15
+ "github-copilot",
16
+ "vscode-copilot",
17
+ "opencode",
12
18
  ];
13
19
  const agentAliases = {
14
20
  claude: "claude-code",
@@ -16,6 +22,23 @@ const agentAliases = {
16
22
  codex: "codex",
17
23
  cursor: "cursor",
18
24
  windsurf: "windsurf",
25
+ cline: "cline",
26
+ "roo-code": "roo-code",
27
+ roo: "roo-code",
28
+ roocode: "roo-code",
29
+ gemini: "gemini",
30
+ "gemini-cli": "gemini",
31
+ copilot: "github-copilot",
32
+ "copilot-cli": "github-copilot",
33
+ "github-copilot": "github-copilot",
34
+ "github-copilot-cli": "github-copilot",
35
+ vscode: "vscode-copilot",
36
+ "vs-code": "vscode-copilot",
37
+ "vscode-copilot": "vscode-copilot",
38
+ "vs-code-copilot": "vscode-copilot",
39
+ "github-copilot-vscode": "vscode-copilot",
40
+ opencode: "opencode",
41
+ "open-code": "opencode",
19
42
  };
20
43
  export function parseAgent(value) {
21
44
  if (!value)
@@ -43,7 +66,15 @@ export async function configureAgentMcp(options) {
43
66
  ? { changed: true }
44
67
  : target.format === "toml"
45
68
  ? await upsertCodexConfig(target.path, server, shouldWrite)
46
- : await upsertJsonMcpConfig(target.path, server, shouldWrite);
69
+ : target.format === "json-opencode"
70
+ ? await upsertOpencodeConfig(target.path, server, shouldWrite)
71
+ : target.format === "json-copilot"
72
+ ? await upsertCopilotConfig(target.path, server, shouldWrite)
73
+ : target.format === "json-vscode"
74
+ ? await upsertVsCodeMcpConfig(target.path, server, shouldWrite)
75
+ : options.agent === "gemini"
76
+ ? await upsertGeminiExtension(target.path, server, shouldWrite)
77
+ : await upsertJsonMcpConfig(target.path, server, shouldWrite);
47
78
  return {
48
79
  agent: options.agent,
49
80
  scope: options.scope,
@@ -70,13 +101,38 @@ export function createSummerMcpServerConfig(localDev) {
70
101
  }
71
102
  return {
72
103
  command: "npx",
73
- args: ["summer-engine", "mcp"],
104
+ args: ["-y", "summer-engine@latest", "mcp"],
74
105
  };
75
106
  }
76
107
  export function renderConfigSnippet(agent, server) {
77
108
  if (agent === "codex") {
78
109
  return renderCodexServerTable(server);
79
110
  }
111
+ if (agent === "opencode") {
112
+ return (JSON.stringify({
113
+ $schema: "https://opencode.ai/config.json",
114
+ mcp: {
115
+ [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server),
116
+ },
117
+ }, null, 2) + "\n");
118
+ }
119
+ if (agent === "github-copilot") {
120
+ return (JSON.stringify({
121
+ mcpServers: {
122
+ [SUMMER_MCP_SERVER_NAME]: copilotServerEntry(server),
123
+ },
124
+ }, null, 2) + "\n");
125
+ }
126
+ if (agent === "vscode-copilot") {
127
+ return (JSON.stringify({
128
+ servers: {
129
+ [SUMMER_MCP_SERVER_NAME]: vsCodeServerEntry(server),
130
+ },
131
+ }, null, 2) + "\n");
132
+ }
133
+ if (agent === "gemini") {
134
+ return (JSON.stringify(geminiExtensionManifest(server), null, 2) + "\n");
135
+ }
80
136
  return (JSON.stringify({
81
137
  mcpServers: {
82
138
  [SUMMER_MCP_SERVER_NAME]: server,
@@ -91,9 +147,21 @@ function resolveConfigTarget(agent, scope, cwd, env) {
91
147
  const override = getConfigPathOverride(agent, env);
92
148
  const warnings = [];
93
149
  if (override) {
150
+ if (scope === "project" &&
151
+ (agent === "cline" || agent === "roo-code" || agent === "gemini")) {
152
+ warnings.push(`${agent} MCP config has no project scope today; treating as user scope.`);
153
+ }
94
154
  return {
95
155
  path: resolve(override),
96
- format: agent === "codex" ? "toml" : "json",
156
+ format: agent === "codex"
157
+ ? "toml"
158
+ : agent === "opencode"
159
+ ? "json-opencode"
160
+ : agent === "github-copilot"
161
+ ? "json-copilot"
162
+ : agent === "vscode-copilot"
163
+ ? "json-vscode"
164
+ : "json",
97
165
  warnings,
98
166
  };
99
167
  }
@@ -124,6 +192,63 @@ function resolveConfigTarget(agent, scope, cwd, env) {
124
192
  warnings,
125
193
  };
126
194
  }
195
+ if (agent === "cline") {
196
+ if (scope === "project") {
197
+ warnings.push("Cline MCP config has no project scope today; writing to user-scope global VS Code storage instead.");
198
+ }
199
+ return {
200
+ path: vsCodeGlobalStoragePath(env, "saoudrizwan.claude-dev"),
201
+ format: "json",
202
+ warnings,
203
+ };
204
+ }
205
+ if (agent === "roo-code") {
206
+ if (scope === "project") {
207
+ warnings.push("Roo Code MCP config has no project scope today; writing to user-scope global VS Code storage instead.");
208
+ }
209
+ return {
210
+ path: vsCodeGlobalStoragePath(env, "rooveterinaryinc.roo-cline"),
211
+ format: "json",
212
+ warnings,
213
+ };
214
+ }
215
+ if (agent === "gemini") {
216
+ if (scope === "project") {
217
+ warnings.push("Gemini extensions are user-scoped today; writing to ~/.gemini/extensions/summer-engine/ instead of project.");
218
+ }
219
+ return {
220
+ path: join(homedir(), ".gemini", "extensions", "summer-engine", "gemini-extension.json"),
221
+ format: "json",
222
+ warnings,
223
+ };
224
+ }
225
+ if (agent === "github-copilot") {
226
+ return {
227
+ path: scope === "user"
228
+ ? join(homedir(), ".copilot", "mcp-config.json")
229
+ : join(cwd, ".mcp.json"),
230
+ format: "json-copilot",
231
+ warnings,
232
+ };
233
+ }
234
+ if (agent === "vscode-copilot") {
235
+ return {
236
+ path: scope === "user"
237
+ ? vsCodeUserMcpPath(env)
238
+ : join(cwd, ".vscode", "mcp.json"),
239
+ format: "json-vscode",
240
+ warnings,
241
+ };
242
+ }
243
+ if (agent === "opencode") {
244
+ return {
245
+ path: scope === "user"
246
+ ? opencodeUserConfigPath(env)
247
+ : join(cwd, "opencode.json"),
248
+ format: "json-opencode",
249
+ warnings,
250
+ };
251
+ }
127
252
  if (scope === "project") {
128
253
  warnings.push("Windsurf documents MCP configuration as user-scoped; project scope writes .windsurf/mcp_config.json for teams that load workspace config.");
129
254
  }
@@ -135,6 +260,40 @@ function resolveConfigTarget(agent, scope, cwd, env) {
135
260
  warnings,
136
261
  };
137
262
  }
263
+ function vsCodeGlobalStoragePath(env, extensionId) {
264
+ const os = platform();
265
+ if (os === "win32") {
266
+ const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
267
+ return join(appData, "Code", "User", "globalStorage", extensionId, "settings", "cline_mcp_settings.json");
268
+ }
269
+ if (os === "darwin") {
270
+ return join(homedir(), "Library", "Application Support", "Code", "User", "globalStorage", extensionId, "settings", "cline_mcp_settings.json");
271
+ }
272
+ // Linux and other Unix
273
+ const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
274
+ return join(xdg, "Code", "User", "globalStorage", extensionId, "settings", "cline_mcp_settings.json");
275
+ }
276
+ function opencodeUserConfigPath(env) {
277
+ const os = platform();
278
+ if (os === "win32") {
279
+ const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
280
+ return join(appData, "opencode", "opencode.json");
281
+ }
282
+ const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
283
+ return join(xdg, "opencode", "opencode.json");
284
+ }
285
+ function vsCodeUserMcpPath(env) {
286
+ const os = platform();
287
+ if (os === "win32") {
288
+ const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
289
+ return join(appData, "Code", "User", "mcp.json");
290
+ }
291
+ if (os === "darwin") {
292
+ return join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json");
293
+ }
294
+ const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
295
+ return join(xdg, "Code", "User", "mcp.json");
296
+ }
138
297
  function getConfigPathOverride(agent, env) {
139
298
  if (agent === "codex")
140
299
  return env.SUMMER_CODEX_CONFIG_FILE;
@@ -142,6 +301,18 @@ function getConfigPathOverride(agent, env) {
142
301
  return env.SUMMER_CLAUDE_CONFIG_FILE;
143
302
  if (agent === "cursor")
144
303
  return env.SUMMER_CURSOR_MCP_CONFIG_FILE;
304
+ if (agent === "cline")
305
+ return env.SUMMER_CLINE_CONFIG_FILE;
306
+ if (agent === "roo-code")
307
+ return env.SUMMER_ROO_CODE_CONFIG_FILE;
308
+ if (agent === "gemini")
309
+ return env.SUMMER_GEMINI_CONFIG_FILE;
310
+ if (agent === "github-copilot")
311
+ return env.SUMMER_GITHUB_COPILOT_CONFIG_FILE;
312
+ if (agent === "vscode-copilot")
313
+ return env.SUMMER_VSCODE_COPILOT_CONFIG_FILE;
314
+ if (agent === "opencode")
315
+ return env.SUMMER_OPENCODE_CONFIG_FILE;
145
316
  return env.SUMMER_WINDSURF_MCP_CONFIG_FILE;
146
317
  }
147
318
  async function upsertJsonMcpConfig(path, server, write) {
@@ -198,6 +369,107 @@ async function upsertCodexConfig(path, server, write) {
198
369
  }
199
370
  return { changed };
200
371
  }
372
+ async function upsertOpencodeConfig(path, server, write) {
373
+ const current = await readJsonConfig(path);
374
+ const next = copyJsonObject(current);
375
+ if (typeof next.$schema !== "string") {
376
+ next.$schema = "https://opencode.ai/config.json";
377
+ }
378
+ const existingMcp = isJsonObject(next.mcp) ? next.mcp : {};
379
+ next.mcp = {
380
+ ...existingMcp,
381
+ [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server),
382
+ };
383
+ const currentRendered = renderJsonFile(current);
384
+ const nextRendered = renderJsonFile(next);
385
+ const changed = currentRendered !== nextRendered;
386
+ if (write && changed) {
387
+ await writeTextFile(path, nextRendered);
388
+ }
389
+ return { changed };
390
+ }
391
+ async function upsertCopilotConfig(path, server, write) {
392
+ const current = await readJsonConfig(path);
393
+ const next = copyJsonObject(current);
394
+ next.mcpServers = mergeNamedObject(next.mcpServers, SUMMER_MCP_SERVER_NAME, copilotServerEntry(server), "mcpServers");
395
+ const currentRendered = renderJsonFile(current);
396
+ const nextRendered = renderJsonFile(next);
397
+ const changed = currentRendered !== nextRendered;
398
+ if (write && changed) {
399
+ await writeTextFile(path, nextRendered);
400
+ }
401
+ return { changed };
402
+ }
403
+ async function upsertVsCodeMcpConfig(path, server, write) {
404
+ const current = await readJsonConfig(path);
405
+ const next = copyJsonObject(current);
406
+ next.servers = mergeNamedObject(next.servers, SUMMER_MCP_SERVER_NAME, vsCodeServerEntry(server), "servers");
407
+ const currentRendered = renderJsonFile(current);
408
+ const nextRendered = renderJsonFile(next);
409
+ const changed = currentRendered !== nextRendered;
410
+ if (write && changed) {
411
+ await writeTextFile(path, nextRendered);
412
+ }
413
+ return { changed };
414
+ }
415
+ async function upsertGeminiExtension(path, server, write) {
416
+ const current = await readJsonConfig(path);
417
+ const next = geminiExtensionManifest(server, current);
418
+ const currentRendered = renderJsonFile(current);
419
+ const nextRendered = renderJsonFile(next);
420
+ const changed = currentRendered !== nextRendered;
421
+ if (write && changed) {
422
+ await writeTextFile(path, nextRendered);
423
+ }
424
+ return { changed };
425
+ }
426
+ function opencodeServerEntry(server) {
427
+ const entry = {
428
+ type: "local",
429
+ command: [server.command, ...server.args],
430
+ };
431
+ if (server.env && Object.keys(server.env).length > 0) {
432
+ entry.environment = { ...server.env };
433
+ }
434
+ return entry;
435
+ }
436
+ function copilotServerEntry(server) {
437
+ const entry = {
438
+ type: "local",
439
+ command: server.command,
440
+ args: server.args,
441
+ tools: ["*"],
442
+ };
443
+ if (server.env && Object.keys(server.env).length > 0) {
444
+ entry.env = { ...server.env };
445
+ }
446
+ return entry;
447
+ }
448
+ function vsCodeServerEntry(server) {
449
+ const entry = {
450
+ type: "stdio",
451
+ command: server.command,
452
+ args: server.args,
453
+ };
454
+ if (server.env && Object.keys(server.env).length > 0) {
455
+ entry.env = { ...server.env };
456
+ }
457
+ return entry;
458
+ }
459
+ function geminiExtensionManifest(server, base = {}) {
460
+ const manifest = { ...base };
461
+ manifest.name = "summer";
462
+ manifest.description =
463
+ "Superpowers for AI game dev. MCP bridge to the local Summer Engine plus a context primer that teaches Gemini the Summer skills.";
464
+ manifest.contextFileName = "GEMINI.md";
465
+ manifest.mcpServers = {
466
+ [SUMMER_MCP_SERVER_NAME]: {
467
+ command: server.command,
468
+ args: server.args,
469
+ },
470
+ };
471
+ return manifest;
472
+ }
201
473
  async function readTextFileIfExists(path) {
202
474
  if (!existsSync(path))
203
475
  return "";
@@ -256,6 +528,15 @@ function renderJsonFile(value) {
256
528
  function copyJsonObject(value) {
257
529
  return JSON.parse(JSON.stringify(value));
258
530
  }
531
+ function mergeNamedObject(value, key, entry, label) {
532
+ if (value !== undefined && !isJsonObject(value)) {
533
+ throw new Error(`Existing ${label} value must be a JSON object.`);
534
+ }
535
+ return {
536
+ ...(isJsonObject(value) ? value : {}),
537
+ [key]: entry,
538
+ };
539
+ }
259
540
  function isJsonObject(value) {
260
541
  return typeof value === "object" && value !== null && !Array.isArray(value);
261
542
  }
@@ -273,7 +554,19 @@ function createNextSteps(agent, scope, path) {
273
554
  ? "Restart Codex or run /mcp in a new session."
274
555
  : agent === "cursor"
275
556
  ? "Restart Cursor and enable the summer-engine MCP server if prompted."
276
- : "Restart Windsurf and refresh MCP servers from Cascade settings.";
557
+ : agent === "cline"
558
+ ? "Restart VS Code so Cline reloads its MCP config."
559
+ : agent === "roo-code"
560
+ ? "Restart VS Code so Roo Code reloads its MCP config."
561
+ : agent === "gemini"
562
+ ? "Run `gemini extensions enable summer-engine` (if not already enabled), then restart Gemini CLI."
563
+ : agent === "github-copilot"
564
+ ? "Restart Copilot CLI, or run /mcp reload and /skills reload in the active session."
565
+ : agent === "vscode-copilot"
566
+ ? "Restart VS Code or run MCP: List Servers, then start summer-engine in Copilot Agent mode."
567
+ : agent === "opencode"
568
+ ? "Restart OpenCode so it reloads opencode.json."
569
+ : "Restart Windsurf and refresh MCP servers from Cascade settings.";
277
570
  const projectTrust = scope === "project" && agent === "codex"
278
571
  ? "Codex only loads project .codex/config.toml from trusted projects."
279
572
  : null;
@@ -19,6 +19,19 @@ export declare class EngineApiClient {
19
19
  constructor(port: number, token: string);
20
20
  static connect(): Promise<EngineApiClient>;
21
21
  private request;
22
+ /** Raw fetch with auth + timeout — returns the Response so callers can inspect
23
+ * the status (202 queued vs 200 legacy). */
24
+ private _fetchRaw;
25
+ /** One long-poll of GET /api/ops/result (up to waitMs). */
26
+ private _pollResult;
27
+ /**
28
+ * Block E: send a mutating request, then resolve EITHER the legacy synchronous
29
+ * result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
30
+ * {requestId} -> long-poll /api/ops/result until terminal). Compatible with
31
+ * both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
32
+ * total budget; a long op that keeps advancing is not falsely timed out.
33
+ */
34
+ private _requestQueued;
22
35
  health(): Promise<unknown>;
23
36
  executeOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
24
37
  getSceneState(): Promise<unknown>;
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "fs/promises";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { getApiToken, getApiPort, checkEngineHealth } from "./engine.js";
5
+ import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
5
6
  function asRecord(value) {
6
7
  return value && typeof value === "object" && !Array.isArray(value)
7
8
  ? value
@@ -96,11 +97,60 @@ export class EngineApiClient {
96
97
  }
97
98
  return res.json();
98
99
  }
100
+ /** Raw fetch with auth + timeout — returns the Response so callers can inspect
101
+ * the status (202 queued vs 200 legacy). */
102
+ async _fetchRaw(method, path, body, timeoutMs) {
103
+ return fetch(`http://127.0.0.1:${this.port}${path}`, {
104
+ method,
105
+ headers: {
106
+ Authorization: `Bearer ${this.token}`,
107
+ "Content-Type": "application/json",
108
+ },
109
+ body: body !== undefined ? JSON.stringify(body) : undefined,
110
+ signal: AbortSignal.timeout(timeoutMs),
111
+ });
112
+ }
113
+ /** One long-poll of GET /api/ops/result (up to waitMs). */
114
+ async _pollResult(requestId, waitMs) {
115
+ const res = await this._fetchRaw("GET", `/api/ops/result?requestId=${encodeURIComponent(requestId)}&wait=${waitMs}`, undefined, waitMs + 5000);
116
+ if (!res.ok) {
117
+ const text = await res.text().catch(() => "");
118
+ throw new Error(`Engine API error ${res.status}: ${text.slice(0, 200)}`);
119
+ }
120
+ return (await res.json());
121
+ }
122
+ /**
123
+ * Block E: send a mutating request, then resolve EITHER the legacy synchronous
124
+ * result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
125
+ * {requestId} -> long-poll /api/ops/result until terminal). Compatible with
126
+ * both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
127
+ * total budget; a long op that keeps advancing is not falsely timed out.
128
+ */
129
+ async _requestQueued(method, path, body, timeoutMs) {
130
+ const res = await this._fetchRaw(method, path, body, timeoutMs);
131
+ // 202 (queued) + 429 (backpressure, errorClass in body) are handled below;
132
+ // any other non-2xx is a real transport error.
133
+ if (!res.ok && res.status !== 202 && res.status !== 429) {
134
+ const text = await res.text().catch(() => "");
135
+ throw new Error(`Engine API error ${res.status}: ${text.slice(0, 200)}`);
136
+ }
137
+ const respBody = await res.json().catch(() => ({}));
138
+ const classified = classifyOpsResponse(res.status, respBody);
139
+ if (classified.mode === "legacy") {
140
+ return respBody;
141
+ }
142
+ return pollOpToTerminal((waitMs) => this._pollResult(classified.requestId, waitMs), {
143
+ totalTimeoutMs: timeoutMs,
144
+ });
145
+ }
99
146
  async health() {
100
147
  return this.request("GET", "/api/health");
101
148
  }
102
149
  async executeOps(ops, options) {
103
- return this.request("POST", "/api/ops", { ops, options });
150
+ // Ops may include long-running work (ImportFromUrlBatch, GitCommit); the
151
+ // engine keeps it "running" and we poll. 120s budget. Resolves legacy-200 or
152
+ // async-202 (Block E).
153
+ return this._requestQueued("POST", "/api/ops", { ops, options }, 120_000);
104
154
  }
105
155
  async getSceneState() {
106
156
  return this.request("GET", "/api/state/scene");
@@ -121,10 +171,11 @@ export class EngineApiClient {
121
171
  return this.request("GET", `/api/state/script-errors?path=${encodeURIComponent(path)}`);
122
172
  }
123
173
  async play(scene) {
124
- return this.request("POST", "/api/play", scene ? { scene } : {});
174
+ // Cold-load on large projects can take 25-40s. 60s budget.
175
+ return this._requestQueued("POST", "/api/play", scene ? { scene } : {}, 60_000);
125
176
  }
126
177
  async stop() {
127
- return this.request("POST", "/api/stop");
178
+ return this._requestQueued("POST", "/api/stop", undefined, 15_000);
128
179
  }
129
180
  async readFile(path, maxBytes) {
130
181
  const params = new URLSearchParams({ path });
@@ -153,31 +204,17 @@ export class EngineApiClient {
153
204
  return localPath;
154
205
  }
155
206
  async snapshot(kind) {
156
- const url = `http://127.0.0.1:${this.port}/api/snapshot/${kind}`;
157
- const res = await fetch(url, {
158
- headers: { Authorization: `Bearer ${this.token}` },
159
- signal: AbortSignal.timeout(30000),
160
- });
161
- if (!res.ok) {
162
- const text = await res.text().catch(() => "");
163
- throw new Error(`Snapshot failed ${res.status}: ${text.slice(0, 200)}`);
207
+ // Block E: /api/snapshot/* is queued (GET -> 202 -> poll /api/ops/result).
208
+ // The terminal result is the apply dict; the image rides as base64 inside it
209
+ // (no raw-binary channel). Legacy/dormant engines answer 200 synchronously
210
+ // with the same payload shape — _requestQueued resolves both.
211
+ let response;
212
+ try {
213
+ response = await this._requestQueued("GET", `/api/snapshot/${kind}`, undefined, 60_000);
164
214
  }
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
- };
215
+ catch (err) {
216
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
179
217
  }
180
- const response = await res.json();
181
218
  const payload = findSnapshotPayload(response);
182
219
  if (!payload) {
183
220
  return {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 0.5.34 Block E — async tool-lifecycle consumer for the CLI/MCP.
3
+ *
4
+ * Ported from the web orchestrator (publicsummerengine
5
+ * src/lib/bridge/async-op-lifecycle.ts) so the CLI speaks the SAME contract as
6
+ * the engine's responsive tool server (Block E):
7
+ * - NEW path: `202 {requestId, status:"queued"}` -> long-poll
8
+ * GET /api/ops/result?requestId=&wait=ms until terminal (done|failed|canceled).
9
+ * - LEGACY path (dormant/older engine): a synchronous `200` with the full apply
10
+ * result, exactly as before.
11
+ *
12
+ * It inspects the RESPONSE (status/body), not a flag, so it stays compatible with
13
+ * both an async and a synchronous engine. Pure + dependency-injected (now/sleep/
14
+ * pollOnce) so it is unit-tested without HTTP.
15
+ */
16
+ export declare function isTerminalStatus(status: string | undefined): boolean;
17
+ /** The envelope returned by GET /api/ops/result. */
18
+ export interface OpResultEnvelope {
19
+ requestId?: string;
20
+ status: string;
21
+ terminalState?: string;
22
+ errorClass?: string;
23
+ result?: Record<string, unknown>;
24
+ progress?: unknown;
25
+ appliedSeq?: number;
26
+ }
27
+ export type OpsResponseClassification = {
28
+ mode: "queued";
29
+ requestId: string;
30
+ } | {
31
+ mode: "legacy";
32
+ legacyResult: unknown;
33
+ };
34
+ /**
35
+ * Decide whether a POST /api/ops (or GET /api/snapshot/*) response is the new
36
+ * async ack (`202`) or the legacy synchronous result (`200`). Inspecting the HTTP
37
+ * status (not a flag) is what keeps this compatible with both engines.
38
+ */
39
+ export declare function classifyOpsResponse(httpStatus: number, body: unknown): OpsResponseClassification;
40
+ export interface PollOpts {
41
+ /** Hard wall-clock cap on the whole poll loop (ms). The per-tool budget. */
42
+ totalTimeoutMs: number;
43
+ /** Give up if the op hasn't advanced for this long (ms). Default = totalTimeoutMs. */
44
+ noProgressTimeoutMs?: number;
45
+ /** Server long-poll wait per GET (ms). Default 5000 — the engine holds the
46
+ * connection up to this long, which paces the loop in production. */
47
+ pollWaitMs?: number;
48
+ /** Pacing sleep between non-terminal polls (ms). Negligible in production
49
+ * (the long-poll dominates); keeps the loop from hot-spinning. Default 250. */
50
+ pacingMs?: number;
51
+ now?: () => number;
52
+ sleep?: (ms: number) => Promise<void>;
53
+ }
54
+ /**
55
+ * Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
56
+ * one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
57
+ * dict with terminalState/errorClass/appliedSeq merged on top, or a synthetic
58
+ * `timed_out` result if the budget is exhausted.
59
+ */
60
+ export declare function pollOpToTerminal(pollOnce: (waitMs: number) => Promise<OpResultEnvelope>, opts: PollOpts): Promise<Record<string, unknown>>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * 0.5.34 Block E — async tool-lifecycle consumer for the CLI/MCP.
3
+ *
4
+ * Ported from the web orchestrator (publicsummerengine
5
+ * src/lib/bridge/async-op-lifecycle.ts) so the CLI speaks the SAME contract as
6
+ * the engine's responsive tool server (Block E):
7
+ * - NEW path: `202 {requestId, status:"queued"}` -> long-poll
8
+ * GET /api/ops/result?requestId=&wait=ms until terminal (done|failed|canceled).
9
+ * - LEGACY path (dormant/older engine): a synchronous `200` with the full apply
10
+ * result, exactly as before.
11
+ *
12
+ * It inspects the RESPONSE (status/body), not a flag, so it stays compatible with
13
+ * both an async and a synchronous engine. Pure + dependency-injected (now/sleep/
14
+ * pollOnce) so it is unit-tested without HTTP.
15
+ */
16
+ const TERMINAL_STATUSES = new Set(["done", "failed", "canceled"]);
17
+ export function isTerminalStatus(status) {
18
+ return status != null && TERMINAL_STATUSES.has(status);
19
+ }
20
+ /**
21
+ * Decide whether a POST /api/ops (or GET /api/snapshot/*) response is the new
22
+ * async ack (`202`) or the legacy synchronous result (`200`). Inspecting the HTTP
23
+ * status (not a flag) is what keeps this compatible with both engines.
24
+ */
25
+ export function classifyOpsResponse(httpStatus, body) {
26
+ const b = (body ?? {});
27
+ const looksQueued = b.accepted === true && b.status === "queued" && typeof b.requestId === "string";
28
+ if (httpStatus === 202 || looksQueued) {
29
+ return { mode: "queued", requestId: String(b.requestId ?? "") };
30
+ }
31
+ return { mode: "legacy", legacyResult: body };
32
+ }
33
+ const TIMED_OUT_RESULT = {
34
+ status: "error",
35
+ error: "Tool timeout - Summer Engine may be unresponsive",
36
+ terminalState: "timed_out",
37
+ errorClass: "transient",
38
+ };
39
+ /**
40
+ * Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
41
+ * one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
42
+ * dict with terminalState/errorClass/appliedSeq merged on top, or a synthetic
43
+ * `timed_out` result if the budget is exhausted.
44
+ */
45
+ export async function pollOpToTerminal(pollOnce, opts) {
46
+ const now = opts.now ?? (() => Date.now());
47
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
48
+ const pollWaitMs = opts.pollWaitMs ?? 5000;
49
+ const pacingMs = opts.pacingMs ?? 250;
50
+ const noProgressMs = opts.noProgressTimeoutMs ?? opts.totalTimeoutMs;
51
+ const start = now();
52
+ let lastAdvanceAt = start;
53
+ let lastStatus;
54
+ let lastSeq = -1;
55
+ let lastProgress = "";
56
+ for (;;) {
57
+ const env = await pollOnce(pollWaitMs);
58
+ if (isTerminalStatus(env.status)) {
59
+ const out = { ...(env.result ?? {}) };
60
+ if (env.terminalState)
61
+ out.terminalState = env.terminalState;
62
+ if (env.errorClass)
63
+ out.errorClass = env.errorClass;
64
+ if (env.appliedSeq != null)
65
+ out.appliedSeq = env.appliedSeq;
66
+ return out;
67
+ }
68
+ const progressJson = env.progress != null ? JSON.stringify(env.progress) : "";
69
+ const seq = env.appliedSeq ?? -1;
70
+ const advanced = env.status !== lastStatus || seq !== lastSeq || progressJson !== lastProgress;
71
+ const t = now();
72
+ if (advanced) {
73
+ lastAdvanceAt = t;
74
+ lastStatus = env.status;
75
+ lastSeq = seq;
76
+ lastProgress = progressJson;
77
+ }
78
+ if (t - start >= opts.totalTimeoutMs || t - lastAdvanceAt >= noProgressMs) {
79
+ return { ...TIMED_OUT_RESULT };
80
+ }
81
+ await sleep(pacingMs);
82
+ }
83
+ }