summer-engine 2.3.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 +51 -30
  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 +8 -2
  21. package/dist/commands/skills.js +90 -6
  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 +134 -7
  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 +102 -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
@@ -1,13 +1,21 @@
1
1
  import { Command } from "commander";
2
- import { existsSync, mkdirSync, readFileSync, cpSync, writeFileSync, } from "fs";
2
+ import { existsSync, mkdirSync, readFileSync, cpSync, rmSync, writeFileSync, readdirSync, copyFileSync, } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { homedir, platform } from "os";
6
+ import { createRequire } from "node:module";
6
7
  import { AGENT_CLIENTS, SKILL_REGISTRY, } from "../lib/skills-registry.js";
7
8
  import { tildeify } from "../lib/format.js";
9
+ import { writeSkillMarker } from "../lib/version-check.js";
10
+ const requireFromHere = createRequire(import.meta.url);
11
+ const { version: cliVersion } = requireFromHere("../../package.json");
8
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
13
  // Resolve skills dir: from dist/commands/skills.js -> ../../skills
10
14
  const skillsDir = join(__dirname, "..", "..", "skills");
15
+ // Resolve commands dir: same parent. Used by Claude Code installs to also copy
16
+ // slash commands (e.g. /gameskill) to ~/.claude/commands/. Other agents don't
17
+ // have an equivalent today, so the copy is gated to claude-code.
18
+ const commandsDir = join(__dirname, "..", "..", "commands");
11
19
  const SKILL_SCOPES = ["user", "project"];
12
20
  function getBuiltinSkills() {
13
21
  const skills = [];
@@ -126,6 +134,10 @@ function agentLabel(agent) {
126
134
  return "Roo Code";
127
135
  case "gemini":
128
136
  return "Gemini CLI";
137
+ case "github-copilot":
138
+ return "GitHub Copilot CLI";
139
+ case "vscode-copilot":
140
+ return "GitHub Copilot in VS Code";
129
141
  case "opencode":
130
142
  return "OpenCode";
131
143
  case "summer":
@@ -180,6 +192,14 @@ function resolveInstallLocation(agent, scope) {
180
192
  kind: "gemini-skill-dir",
181
193
  path: join(homedir(), ".gemini", "extensions", "summer-engine", "skills"),
182
194
  };
195
+ case "github-copilot":
196
+ case "vscode-copilot":
197
+ return {
198
+ kind: "skill-dir",
199
+ path: scope === "user"
200
+ ? join(homedir(), ".copilot", "skills")
201
+ : join(process.cwd(), ".github", "skills"),
202
+ };
183
203
  case "opencode":
184
204
  return {
185
205
  kind: "opencode-skill-dir",
@@ -244,13 +264,13 @@ function printAvailableSkillNames() {
244
264
  console.log(` ${skill.name}`);
245
265
  }
246
266
  }
247
- function installSkill(skill, agent, location) {
267
+ function installSkill(skill, agent, location, options) {
248
268
  if (!skill.clients.includes(agent)) {
249
269
  die(`${skill.name} does not support ${agentLabel(agent)}.`);
250
270
  }
251
271
  switch (location.kind) {
252
272
  case "skill-dir":
253
- return copySkillDirectory(skill, location.path);
273
+ return copySkillDirectory(skill, location.path, options);
254
274
  case "cursor-rule-dir":
255
275
  return writeCursorRule(skill, location.path);
256
276
  case "windsurf-rule-file":
@@ -263,13 +283,18 @@ function installSkill(skill, agent, location) {
263
283
  return writeOpencodeSkill(skill, location.path);
264
284
  }
265
285
  }
266
- function copySkillDirectory(skill, targetDir) {
286
+ function copySkillDirectory(skill, targetDir, options) {
267
287
  const src = getSkillPath(skill.name);
268
288
  if (!src)
269
289
  die(`Skill files missing: ${skill.name}`);
270
290
  mkdirSync(targetDir, { recursive: true });
271
291
  const dest = join(targetDir, skill.name);
272
- cpSync(src, dest, { recursive: true });
292
+ if (options.force && existsSync(dest)) {
293
+ // Wipe stale skill content so re-installs overwrite cleanly even if files
294
+ // were renamed or removed upstream.
295
+ rmSync(dest, { recursive: true, force: true });
296
+ }
297
+ cpSync(src, dest, { recursive: true, force: true });
273
298
  return { action: "Installed", path: dest };
274
299
  }
275
300
  function writeCursorRule(skill, rulesDir) {
@@ -400,6 +425,7 @@ skillsCommand
400
425
  .option("--scope <scope>", "Install scope: user or project")
401
426
  .option("--as-claude-skill", "Legacy alias for --agent claude-code")
402
427
  .option("--as-cursor-skill", "Legacy alias for --agent cursor")
428
+ .option("--force", "Overwrite existing skill files (wipe stale skill dirs before copying)")
403
429
  .action((name, opts) => {
404
430
  const agent = resolveAgent(opts);
405
431
  const scope = resolveScope(agent, opts);
@@ -410,11 +436,69 @@ skillsCommand
410
436
  return;
411
437
  }
412
438
  for (const skill of skills) {
413
- const result = installSkill(skill, agent, location);
439
+ const result = installSkill(skill, agent, location, {
440
+ force: Boolean(opts.force),
441
+ });
414
442
  console.log(` ${result.action} ${skill.name} -> ${result.path}`);
415
443
  }
444
+ // Claude Code: also install slash commands (`tools/summer-cli/commands/*.md`)
445
+ // into `~/.claude/commands/`. Other agents don't have an equivalent today.
446
+ if (agent === "claude-code") {
447
+ const commandResults = installClaudeCommands(scope, Boolean(opts.force));
448
+ for (const r of commandResults) {
449
+ console.log(` ${r.action} command ${r.name} -> ${r.path}`);
450
+ }
451
+ }
452
+ writeMarkerForLocation(location);
416
453
  printInstallSummary(skills.length, agent, scope, location);
417
454
  });
455
+ /**
456
+ * Copy every `.md` file in `tools/summer-cli/commands/` to the user's
457
+ * `~/.claude/commands/` directory so the slash commands (e.g. `/gameskill`)
458
+ * are available on every machine that runs `summer skills install`.
459
+ *
460
+ * Project-scope installs go to `<cwd>/.claude/commands/`. Returns one result
461
+ * per command file copied. Silent no-op if no commands directory exists in
462
+ * the package (older summer-cli releases didn't ship one).
463
+ */
464
+ function installClaudeCommands(scope, force) {
465
+ if (!existsSync(commandsDir))
466
+ return [];
467
+ const root = scope === "user" ? homedir() : process.cwd();
468
+ const targetDir = join(root, ".claude", "commands");
469
+ mkdirSync(targetDir, { recursive: true });
470
+ const results = [];
471
+ for (const entry of readdirSync(commandsDir)) {
472
+ if (!entry.endsWith(".md"))
473
+ continue;
474
+ const src = join(commandsDir, entry);
475
+ const dest = join(targetDir, entry);
476
+ if (existsSync(dest) && !force) {
477
+ results.push({ action: "Kept", name: entry, path: dest });
478
+ continue;
479
+ }
480
+ copyFileSync(src, dest);
481
+ results.push({ action: "Installed", name: entry, path: dest });
482
+ }
483
+ return results;
484
+ }
485
+ /**
486
+ * Drop a `.summer-version` marker into the install dir so doctor's
487
+ * `skills-version-stale` check can detect drift later. Windsurf writes a
488
+ * single rules file with no surrounding dir, so we skip it there.
489
+ */
490
+ function writeMarkerForLocation(location) {
491
+ if (location.kind === "windsurf-rule-file")
492
+ return;
493
+ try {
494
+ mkdirSync(location.path, { recursive: true });
495
+ writeSkillMarker(location.path, cliVersion);
496
+ }
497
+ catch {
498
+ // Marker is purely informational. If the FS rejects it (e.g. permission
499
+ // surprises on locked-down corp machines) we don't want to fail the install.
500
+ }
501
+ }
418
502
  skillsCommand
419
503
  .command("info <name>")
420
504
  .description("Show skill description and preview")
@@ -1,6 +1,8 @@
1
1
  import { Command } from "commander";
2
2
  import { getAuthToken, getUserInfo } from "../lib/auth.js";
3
3
  import { getApiToken, getApiPort, checkEngineHealth } from "../lib/engine.js";
4
+ import { getProjectMemorySummary } from "../lib/project-memory.js";
5
+ import { formatStatusMemoryLine } from "./memory.js";
4
6
  export const statusCommand = new Command("status")
5
7
  .description("Check Summer Engine status, connection, and auth state")
6
8
  .action(async () => {
@@ -36,6 +38,7 @@ export const statusCommand = new Command("status")
36
38
  }
37
39
  if (health.project_path) {
38
40
  console.log(` Path: ${health.project_path}`);
41
+ console.log(` ${formatStatusMemoryLine(getProjectMemorySummary(health.project_path))}`);
39
42
  }
40
43
  if (health.scene) {
41
44
  console.log(` Scene: ${health.scene}`);
@@ -1,5 +1,5 @@
1
1
  export declare const SUMMER_MCP_SERVER_NAME = "summer-engine";
2
- export declare const supportedAgents: readonly ["codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "gemini", "opencode"];
2
+ export declare const supportedAgents: readonly ["codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "gemini", "github-copilot", "vscode-copilot", "opencode"];
3
3
  export type SupportedAgent = (typeof supportedAgents)[number];
4
4
  export type ConfigScope = "user" | "project";
5
5
  export interface StdioMcpServerConfig {
@@ -22,7 +22,7 @@ export interface AgentConfigResult {
22
22
  path: string;
23
23
  serverName: string;
24
24
  server: StdioMcpServerConfig;
25
- format: "json" | "toml" | "json-opencode";
25
+ format: "json" | "toml" | "json-opencode" | "json-copilot" | "json-vscode";
26
26
  snippet: string;
27
27
  changed: boolean;
28
28
  wrote: boolean;
@@ -12,6 +12,8 @@ export const supportedAgents = [
12
12
  "cline",
13
13
  "roo-code",
14
14
  "gemini",
15
+ "github-copilot",
16
+ "vscode-copilot",
15
17
  "opencode",
16
18
  ];
17
19
  const agentAliases = {
@@ -26,6 +28,15 @@ const agentAliases = {
26
28
  roocode: "roo-code",
27
29
  gemini: "gemini",
28
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",
29
40
  opencode: "opencode",
30
41
  "open-code": "opencode",
31
42
  };
@@ -57,9 +68,13 @@ export async function configureAgentMcp(options) {
57
68
  ? await upsertCodexConfig(target.path, server, shouldWrite)
58
69
  : target.format === "json-opencode"
59
70
  ? await upsertOpencodeConfig(target.path, server, shouldWrite)
60
- : options.agent === "gemini"
61
- ? await upsertGeminiExtension(target.path, server, shouldWrite)
62
- : await upsertJsonMcpConfig(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);
63
78
  return {
64
79
  agent: options.agent,
65
80
  scope: options.scope,
@@ -101,6 +116,20 @@ export function renderConfigSnippet(agent, server) {
101
116
  },
102
117
  }, null, 2) + "\n");
103
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
+ }
104
133
  if (agent === "gemini") {
105
134
  return (JSON.stringify(geminiExtensionManifest(server), null, 2) + "\n");
106
135
  }
@@ -128,7 +157,11 @@ function resolveConfigTarget(agent, scope, cwd, env) {
128
157
  ? "toml"
129
158
  : agent === "opencode"
130
159
  ? "json-opencode"
131
- : "json",
160
+ : agent === "github-copilot"
161
+ ? "json-copilot"
162
+ : agent === "vscode-copilot"
163
+ ? "json-vscode"
164
+ : "json",
132
165
  warnings,
133
166
  };
134
167
  }
@@ -189,6 +222,24 @@ function resolveConfigTarget(agent, scope, cwd, env) {
189
222
  warnings,
190
223
  };
191
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
+ }
192
243
  if (agent === "opencode") {
193
244
  return {
194
245
  path: scope === "user"
@@ -231,6 +282,18 @@ function opencodeUserConfigPath(env) {
231
282
  const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
232
283
  return join(xdg, "opencode", "opencode.json");
233
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
+ }
234
297
  function getConfigPathOverride(agent, env) {
235
298
  if (agent === "codex")
236
299
  return env.SUMMER_CODEX_CONFIG_FILE;
@@ -244,6 +307,10 @@ function getConfigPathOverride(agent, env) {
244
307
  return env.SUMMER_ROO_CODE_CONFIG_FILE;
245
308
  if (agent === "gemini")
246
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;
247
314
  if (agent === "opencode")
248
315
  return env.SUMMER_OPENCODE_CONFIG_FILE;
249
316
  return env.SUMMER_WINDSURF_MCP_CONFIG_FILE;
@@ -321,6 +388,30 @@ async function upsertOpencodeConfig(path, server, write) {
321
388
  }
322
389
  return { changed };
323
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
+ }
324
415
  async function upsertGeminiExtension(path, server, write) {
325
416
  const current = await readJsonConfig(path);
326
417
  const next = geminiExtensionManifest(server, current);
@@ -342,6 +433,29 @@ function opencodeServerEntry(server) {
342
433
  }
343
434
  return entry;
344
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
+ }
345
459
  function geminiExtensionManifest(server, base = {}) {
346
460
  const manifest = { ...base };
347
461
  manifest.name = "summer";
@@ -414,6 +528,15 @@ function renderJsonFile(value) {
414
528
  function copyJsonObject(value) {
415
529
  return JSON.parse(JSON.stringify(value));
416
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
+ }
417
540
  function isJsonObject(value) {
418
541
  return typeof value === "object" && value !== null && !Array.isArray(value);
419
542
  }
@@ -437,9 +560,13 @@ function createNextSteps(agent, scope, path) {
437
560
  ? "Restart VS Code so Roo Code reloads its MCP config."
438
561
  : agent === "gemini"
439
562
  ? "Run `gemini extensions enable summer-engine` (if not already enabled), then restart Gemini CLI."
440
- : agent === "opencode"
441
- ? "Restart OpenCode so it reloads opencode.json."
442
- : "Restart Windsurf and refresh MCP servers from Cascade settings.";
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.";
443
570
  const projectTrust = scope === "project" && agent === "codex"
444
571
  ? "Codex only loads project .codex/config.toml from trusted projects."
445
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>>;