summer-engine 3.5.0 → 3.5.2

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 (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/plugin.json +2 -1
  4. package/.cursor-plugin/plugin.json +2 -1
  5. package/.factory-plugin/plugin.json +2 -1
  6. package/CHANGELOG.md +23 -0
  7. package/README.md +3 -3
  8. package/commands/summer.md +1 -0
  9. package/dist/cli/commands/setup.js +2 -2
  10. package/dist/core/capabilities/publish-build.d.ts +4 -0
  11. package/dist/core/capabilities/publish-build.js +67 -34
  12. package/dist/core/capabilities/runtime-control.d.ts +2 -2
  13. package/dist/installer/agent-config.d.ts +5 -5
  14. package/dist/installer/agent-config.js +96 -32
  15. package/dist/installer/agent-table.d.ts +7 -6
  16. package/dist/installer/agent-table.js +117 -6
  17. package/dist/mcp/hosted-mount.js +2 -1
  18. package/dist/mcp/provider-safe-schema.d.ts +10 -0
  19. package/dist/mcp/provider-safe-schema.js +96 -0
  20. package/dist/mcp/server.js +3 -0
  21. package/gemini-extension.json +2 -1
  22. package/integrations/README.md +6 -0
  23. package/library/skills/README.md +3 -2
  24. package/library/skills/export-and-ship/SKILL.md +2 -0
  25. package/library/skills/make-game/SKILL.md +21 -2
  26. package/library/skills/make-game/resource.yaml +1 -0
  27. package/library/skills/multiplayer/SKILL.md +3 -1
  28. package/library/skills/multiplayer/resource.yaml +1 -0
  29. package/library/skills/multiplayer-movement/SKILL.md +1 -1
  30. package/library/skills/multiplayer-movement/files.md +2 -3
  31. package/library/skills/multiplayer-project/SKILL.md +20 -4
  32. package/library/skills/multiplayer-project/files.md +2 -3
  33. package/library/skills/multiplayer-publish/SKILL.md +86 -5
  34. package/library/skills/multiplayer-publish/resource.yaml +1 -0
  35. package/library/skills/multiplayer-state/SKILL.md +1 -1
  36. package/library/skills/multiplayer-state/files.md +2 -3
  37. package/library/skills/multiplayer-testing/SKILL.md +13 -0
  38. package/library/skills/multiplayer-testing/resource.yaml +1 -0
  39. package/library/skills/publish-your-game/SKILL.md +2 -0
  40. package/library/skills/release-readiness/SKILL.md +298 -0
  41. package/library/skills/release-readiness/resource.yaml +41 -0
  42. package/library/skills/summer-parties/SKILL.md +1 -1
  43. package/package.json +1 -1
  44. package/registry/generated/counts.json +2 -2
  45. package/registry/generated/gemini-extension.json +2 -1
  46. package/registry/generated/index.json +63 -11
  47. package/registry/generated/marketplace.claude.json +1 -1
  48. package/registry/generated/plugin.claude.json +2 -1
  49. package/registry/generated/plugin.codex.json +2 -1
  50. package/registry/generated/plugin.cursor.json +2 -1
  51. package/registry/generated/plugin.factory.json +2 -1
  52. package/registry/generated/skills-index.md +3 -2
  53. package/registry/generated/skills-registry.json +14 -0
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "name": "summer",
13
13
  "source": "./",
14
- "version": "3.5.0",
14
+ "version": "3.5.2",
15
15
  "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
16
16
  "author": {
17
17
  "name": "Summer Engine",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "_generated": "GENERATED from integrations/claude — do not edit; npm run generate:registry",
4
4
  "name": "summer",
5
- "version": "3.5.0",
5
+ "version": "3.5.2",
6
6
  "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
7
7
  "author": {
8
8
  "name": "Summer Engine",
@@ -96,6 +96,7 @@
96
96
  "./library/skills/psx-retro-rendering/",
97
97
  "./library/skills/publish-your-game/",
98
98
  "./library/skills/realtime-wet-surfaces/",
99
+ "./library/skills/release-readiness/",
99
100
  "./library/skills/remote-deploy/",
100
101
  "./library/skills/retarget/",
101
102
  "./library/skills/running-in-the-cloud/",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_generated": "GENERATED from integrations/codex — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
- "version": "3.5.0",
4
+ "version": "3.5.2",
5
5
  "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge. Build games by talking.",
6
6
  "author": {
7
7
  "name": "Summer Engine",
@@ -91,6 +91,7 @@
91
91
  "./library/skills/psx-retro-rendering/",
92
92
  "./library/skills/publish-your-game/",
93
93
  "./library/skills/realtime-wet-surfaces/",
94
+ "./library/skills/release-readiness/",
94
95
  "./library/skills/remote-deploy/",
95
96
  "./library/skills/retarget/",
96
97
  "./library/skills/running-in-the-cloud/",
@@ -3,7 +3,7 @@
3
3
  "_generated": "GENERATED from integrations/cursor — do not edit; npm run generate:registry",
4
4
  "name": "summer",
5
5
  "displayName": "Summer",
6
- "version": "3.5.0",
6
+ "version": "3.5.2",
7
7
  "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
8
8
  "author": {
9
9
  "name": "Summer Engine",
@@ -103,6 +103,7 @@
103
103
  "./library/skills/psx-retro-rendering/",
104
104
  "./library/skills/publish-your-game/",
105
105
  "./library/skills/realtime-wet-surfaces/",
106
+ "./library/skills/release-readiness/",
106
107
  "./library/skills/remote-deploy/",
107
108
  "./library/skills/retarget/",
108
109
  "./library/skills/running-in-the-cloud/",
@@ -2,7 +2,7 @@
2
2
  "_generated": "GENERATED from integrations/factory — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
4
  "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
5
- "version": "3.5.0",
5
+ "version": "3.5.2",
6
6
  "author": {
7
7
  "name": "Summer Engine",
8
8
  "email": "founders@summerengine.com",
@@ -88,6 +88,7 @@
88
88
  "./library/skills/psx-retro-rendering/",
89
89
  "./library/skills/publish-your-game/",
90
90
  "./library/skills/realtime-wet-surfaces/",
91
+ "./library/skills/release-readiness/",
91
92
  "./library/skills/remote-deploy/",
92
93
  "./library/skills/retarget/",
93
94
  "./library/skills/running-in-the-cloud/",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to summer-engine will be documented here. Following [Keep a
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ Hosted games: agents check that players can actually join before a release.
8
+
9
+ - **`release-readiness` check 7: export with a released Summer, and prove the scripts compile on the runtime players get.** Players' apps run a bundle on the released Summer of the version its export stamp names. A development build that still stamps an older released version can use APIs that release lacks; its scripts then fail to compile on every player's device (`Parse Error: Static function "…" not found`, `Failed to load script`) and the game opens to an empty window, while every upload check passes. The check reads the stamp from `summer-bundle.json`, compares `engineSha` with the release's, and runs `client.pck` on that released Summer with `--headless --verbose --main-pack`, failing on `SCRIPT ERROR`, `Parse Error` or `Failed to load script`. `multiplayer-publish` adds the run as step 4e, and `export-and-ship` says to export summer.games bundles with the released editor only.
10
+
11
+ - **New `release-readiness` skill.** Before a version reaches players, it runs six checks on the summer.games `.zip` you upload: the bundle's `server.pck`, started with no scene, loads the authority scene; a real online join with the released version reaches the game; online play is reachable by default; the server saves online progress through player data; players can get back to the title; and `Summer.initialize` passes no hard-coded Game ID. It reports pass, fail, not applicable or not run for each, and grows by one check after every release that fails in a way none of them caught. The same checklist is in the docs at `/publishing/release-readiness`.
12
+ - **The multiplayer skills call `Summer.initialize()` with no Game ID.** The summer.games bundle leaves `gameId` out, so an ID in code (or a fallback when it is missing) fails on the hosted server with `game_identity_mismatch` and every online join ends in `world_start_timeout`.
13
+ - `multiplayer-publish` explains why the server preset needs the `summer_authority` custom feature, adds a pre-upload step that runs the bundle's `server.pck` with no scene, a real online join after release, and what an empty server log does and doesn't mean. `multiplayer-testing` lists what Local Play can't catch; `make-game` plans online play reachable by default, server-saved progress and a way back to the title, and runs `release-readiness` before shipping.
14
+
15
+ ## [3.5.2] - 2026-10-10
16
+
17
+ Summer works with Kimi Code and other agents whose model checks tool schemas strictly.
18
+
19
+ - Kimi Code no longer fails every prompt with `400 tools.function.parameters is not a valid moonshot flavored json schema ... items must be an object` when Summer is connected. Ten tools (`summer_align_distribute_3d`, `summer_raycast`, `summer_instantiate_scene`, `summer_capture_gameplay`, `summer_test_placement`, `summer_snap_to_surface`, `summer_navigation_probe`, `summer_attach_to_surface`, `summer_repeat_along`, `summer_game_input`) described their `[x, y, z]` and `[x, y]` vectors in a form Moonshot rejects. Every tool now lists its parameters in the JSON Schema subset strict providers accept: a vector is an array of numbers with an exact length, and no schema points into another with `$ref`. The hosted store tools get the same treatment. The tools still reject a vector of the wrong length.
20
+ - If you added Summer tools to Kimi's `disabledTools` to work around this, remove them after updating, then restart Kimi Code.
21
+
22
+ ## [3.5.1] - 2026-10-10
23
+
24
+ Summer Games now connects from almost every agent setup supports, and large store uploads finish.
25
+
26
+ - **Summer Games in more agents.** `summer setup` now adds `summer-games` (`https://mcp.summer.games/mcp`: friends, parties and messages as you) for Cursor, GitHub Copilot in VS Code, the Copilot CLI and Copilot in JetBrains, Zed, Cline and Cline CLI, Kilo Code, OpenCode, Devin Desktop (Windsurf), Antigravity, Kiro, Trae, Warp, Goose, Hermes, Crush, Amp, Factory Droid, Junie, Qwen Code, Kimi Code, Grok Build and Mistral Vibe, as well as Claude Code and Codex. Each gets the entry in its own format, and setup prints the one sign-in step for that agent. As in 3.5.0, setup adds no second entry when the same address is already listed under another name, and `--no-games` skips it.
27
+ - Not included, because they cannot sign in to it from their config file: Claude Desktop (add Summer Games as a custom connector in its settings), Roo Code and Rovo Dev (no sign-in for remote servers), and Visual Studio, Qoder and LM Studio (sign-in not yet checked).
28
+ - `summer_publish_build` and `summer_publish_download` no longer fail an upload of 3 or more parts with `store_sign_failed: The store did not sign part N` when the store signed every part. Each sign now asks for every part still to upload, a part counts as unsigned only when the store was asked for it and left it out, a signing error is retried like a failed part upload, and the first failure stops the other parts' uploads.
29
+
7
30
  ## [3.5.0] - 2026-10-10
8
31
 
9
32
  Setup now connects Summer Games too, and agents that publish a game get further on their own. 3.4.2 was never published; its changes ship here.
package/README.md CHANGED
@@ -57,7 +57,7 @@ The first run will:
57
57
  4. Open your browser for one-click sign-in.
58
58
  5. Scaffold a starter project and open the engine.
59
59
 
60
- In Claude Code and Codex, setup also connects Summer Games (`summer-games`), so the agent can see your friends, start parties and send messages as you. You sign in once: Claude Code `/mcp`, choose `summer-games`; Codex `/mcp login summer-games`. A summer.games page opens in your browser; sign in and choose Allow.
60
+ In most agents (Claude Code, Codex, Cursor, GitHub Copilot (VS Code, CLI, JetBrains), Zed, Cline, Kilo Code, OpenCode, Devin Desktop, Antigravity, Kiro, Trae, Warp, Goose, Hermes, Crush, Amp, Factory, Junie, Qwen Code, Kimi Code, Grok Build and Mistral Vibe), setup also connects Summer Games (`summer-games`), so the agent can see your friends, start parties and send messages as you. You sign in once; setup prints the step for your agent (Claude Code: `/mcp`, choose `summer-games`; Codex: `/mcp login summer-games`; Cursor: Connect next to `summer-games` in its MCP settings). A summer.games page opens in your browser; sign in and choose Allow.
61
61
 
62
62
  Then ask the agent to build something.
63
63
 
@@ -125,7 +125,7 @@ npx -y summer-engine@latest setup claude-code --yes --force
125
125
 
126
126
  Replace `claude-code` with the user's actual agent: `claude-desktop`, `codex`, `cursor`, `windsurf`, `antigravity`, `cline`, `cline-cli`, `kilo-code`, `github-copilot`, `vscode-copilot`, `visual-studio`, `copilot-jetbrains`, `opencode`, `zed`, `kiro`, `goose`, `hermes`, `trae`, `qwen-code`, `kimi-code`, `crush`, `amp`, `factory`, `junie`, `warp`, `rovo-dev`, `qoder`, `grok-build`, `mistral-vibe`, `lm-studio`. Use `github-copilot` for Copilot CLI, `vscode-copilot` for GitHub Copilot Chat/Agent in VS Code, `visual-studio` for Copilot in Visual Studio, and `copilot-jetbrains` for Copilot in JetBrains IDEs. Legacy targets `gemini` and `roo-code` still work; for Google's agent use `antigravity`. This installs **every skill in the library** to `~/.claude/skills/<slug>/SKILL.md` (or the agent's own skill directory), writes the MCP server config, runs doctor, and prints what it installed and where. Preview skills are included and labelled; `--stable-only` skips them. `--force` wipes stale skill copies first. Don't use `--recommended` from this playbook; it installs only a subset.
127
127
 
128
- **Summer Games:** for `claude-code` and `codex`, setup also adds the remote `summer-games` MCP (`https://mcp.summer.games/mcp`) and prints a `Sign in once:` line. Tell the user that line once, in plain words: in Claude Code, run `/mcp`, choose `summer-games` and sign in; in Codex, run `/mcp login summer-games`. Their browser opens summer.games; they sign in and choose Allow. Do not wait for it; continue the playbook. If the user already added the same URL under another name (for example `summer`), setup keeps theirs and adds no second entry.
128
+ **Summer Games:** for every agent whose sign-in summer.games accepts (all targets except `claude-desktop`, `visual-studio`, `qoder`, `rovo-dev`, `lm-studio` and the legacy ones), setup also adds the remote `summer-games` MCP (`https://mcp.summer.games/mcp`) and prints a `Sign in once:` line. Tell the user that line once, in plain words. Their browser opens summer.games; they sign in and choose Allow. Do not wait for it; continue the playbook. If the user already added the same URL under another name (for example `summer`), setup keeps theirs and adds no second entry.
129
129
 
130
130
  **First-install detection:** if `~/.claude/skills/` didn't exist before this command, Claude Code wasn't watching it and won't auto-detect the new files this session. Tell the user **once**: "Skills installed - restart your agent so they load." On subsequent installs (directory already existed), skills auto-detect mid-session and no restart is needed. **You can detect first-install vs upgrade by checking if `~/.claude/skills/` existed before Step 1; record the result before running setup.**
131
131
 
@@ -374,7 +374,7 @@ matchmaking are not promised by this package.
374
374
  | `summer tool <name> --args '<json>'` | Run any Summer tool from the shell — the same implementation the MCP tool uses. `summer tool --list` prints them all. |
375
375
  | `summer mcp [--project <path> \| --instance <id>]` | Start MCP; normally auto-binds from the agent's project directory. |
376
376
  | `summer mcp setup <agent>` | Deprecated alias of `summer setup <agent>`. |
377
- | `summer setup <agent> [--yes] [--force] [--recommended] [--stable-only] [--no-games]` | One shot: MCP config + all skills, preview included (`--recommended` for the subset, `--stable-only` to skip preview) + doctor. Idempotent. For Claude Code and Codex it also adds `summer-games` (`https://mcp.summer.games/mcp`: friends, parties and messages as you) unless the same URL is already listed; you sign in once through summer.games. `--no-games` skips it. |
377
+ | `summer setup <agent> [--yes] [--force] [--recommended] [--stable-only] [--no-games]` | One shot: MCP config + all skills, preview included (`--recommended` for the subset, `--stable-only` to skip preview) + doctor. Idempotent. For most agents (not Claude Desktop, Visual Studio, Qoder, Rovo Dev, LM Studio or the legacy targets) it also adds `summer-games` (`https://mcp.summer.games/mcp`: friends, parties and messages as you) unless the same URL is already listed; you sign in once through summer.games. `--no-games` skips it. |
378
378
 
379
379
  Agents: `claude-code`, `claude-desktop`, `codex`, `cursor`, `windsurf`, `antigravity`, `cline`, `cline-cli`, `kilo-code`, `github-copilot`, `vscode-copilot`, `visual-studio`, `copilot-jetbrains`, `opencode`, `zed`, `kiro`, `goose`, `hermes`, `trae`, `qwen-code`, `kimi-code`, `crush`, `amp`, `factory`, `junie`, `warp`, `rovo-dev`, `qoder`, `grok-build`, `mistral-vibe`, `lm-studio`. Legacy (still accepted, warn on use): `gemini`, `roo-code`. Aliases: `devin`/`devin-desktop` → `windsurf`, `agy` → `antigravity`, `droid` → `factory`, `copilot` → `github-copilot`, `vscode` → `vscode-copilot`, `kilo` → `kilo-code`, `qwen` → `qwen-code`, `kimi` → `kimi-code`. Scopes: `--scope user` (default), `--scope project`.
380
380
 
@@ -47,6 +47,7 @@ Skills are invoked by their bare slug, such as `brainstorm-game`. There is no `s
47
47
  | add an animation to [character] | `generate-motion` |
48
48
  | wire up a state machine for [character] | `animation-tree` |
49
49
  | the game feels mushy / add juice | `game-feel` |
50
+ | is it ready to release / check before publishing / online joins fail with `world_start_timeout` | `release-readiness` |
50
51
  | ship a trailer | `trailer-shot` |
51
52
  | cutscene for [X] | `cinematic-cutscene` |
52
53
  | let me play it | `play` |
@@ -7,7 +7,7 @@ import { TOOLKIT_VERSION as cliVersion } from "../../core/version.js";
7
7
  import { resolveDefaultChannel } from "../../installer/version-check.js";
8
8
  import { agentLabel } from "../../installer/agent-table.js";
9
9
  export const setupCommand = new Command("setup")
10
- .description("Configure Summer Engine for an AI agent and run diagnostics. For Claude Code and Codex it also adds Summer Games (summer-games).")
10
+ .description("Configure Summer Engine for an AI agent and run diagnostics. For agents that can sign in to it, it also adds Summer Games (summer-games).")
11
11
  .argument("[agent]", `Agent to configure: ${supportedAgents.join(", ")}`)
12
12
  .option("--agent <agent>", `Agent to configure: ${supportedAgents.join(", ")}`)
13
13
  .option("--scope <scope>", "Configuration scope: user or project", "user")
@@ -16,7 +16,7 @@ export const setupCommand = new Command("setup")
16
16
  .option("--local-dev", "Point the agent at this checkout's built CLI (node <repo>/dist/bin/summer.js mcp) instead of npx summer-engine@latest — for testing unpublished builds. SUMMER_DEV=1 does the same.")
17
17
  .option("--channel <dist-tag>", "npm dist-tag the agent's MCP entry runs: npx -y summer-engine@<dist-tag> mcp (default latest; use next while a release soaks on the next tag). SUMMER_CHANNEL does the same.")
18
18
  .option("--yes", "Apply practical setup steps without prompting")
19
- .option("--no-games", "Do not add the Summer Games MCP (summer-games: friends, parties and messages as you, signed in through summer.games; Claude Code and Codex)")
19
+ .option("--no-games", "Do not add the Summer Games MCP (summer-games: friends, parties and messages as you, signed in through summer.games)")
20
20
  .option("--json", "Print setup result as JSON")
21
21
  .option("--force", "Overwrite existing skill content (passes --force through to skills install)")
22
22
  .option("--recommended", "Install only the recommended skill subset instead of the whole library")
@@ -51,6 +51,10 @@ export declare function readGrants(body: Record<string, any>): Grant[];
51
51
  * Upload the given parts: signed in batches, PUT from disk, re-signed when
52
52
  * refused or about to expire. `sign` asks the store for URLs (build
53
53
  * publications bind each to its part SHA-256; store versions to its length).
54
+ * Each sign asks for every part not yet uploaded, including the parts other
55
+ * workers hold, so one sign usually covers them all. A part fails to sign only
56
+ * when the store was asked for it and did not sign it; the first fatal error
57
+ * stops the other workers and aborts their uploads.
54
58
  */
55
59
  export declare function uploadParts(sign: (partNumbers: number[]) => Promise<Grant[]>, file: string, sizeBytes: number, plan: {
56
60
  partSizeBytes: number;
@@ -185,19 +185,28 @@ async function readPart(path, partNumber, partSize, sizeBytes) {
185
185
  * Upload the given parts: signed in batches, PUT from disk, re-signed when
186
186
  * refused or about to expire. `sign` asks the store for URLs (build
187
187
  * publications bind each to its part SHA-256; store versions to its length).
188
+ * Each sign asks for every part not yet uploaded, including the parts other
189
+ * workers hold, so one sign usually covers them all. A part fails to sign only
190
+ * when the store was asked for it and did not sign it; the first fatal error
191
+ * stops the other workers and aborts their uploads.
188
192
  */
189
193
  export async function uploadParts(sign, file, sizeBytes, plan, deps, parts = Array.from({ length: plan.partCount }, (_, index) => index + 1)) {
190
194
  const queue = [...parts];
195
+ const unfinished = new Set(parts);
191
196
  const grants = new Map();
197
+ const stop = new AbortController();
198
+ let failure;
192
199
  let signing = null;
193
200
  const fresh = (grant) => grant !== undefined && grant.expiresAt - deps.now() > 60_000;
194
201
  const grantFor = async (partNumber) => {
195
- for (let attempt = 0; attempt < 3; attempt++) {
196
- if (fresh(grants.get(partNumber)))
197
- return grants.get(partNumber);
202
+ for (;;) {
203
+ const grant = grants.get(partNumber);
204
+ if (fresh(grant))
205
+ return grant;
198
206
  if (!signing) {
199
- const batch = [...new Set([partNumber, ...queue.filter((n) => !fresh(grants.get(n)))])].slice(0, MAX_SIGNED_PARTS);
200
- signing = sign(batch)
207
+ const others = [...unfinished].filter((n) => n !== partNumber && !fresh(grants.get(n)));
208
+ const batch = [partNumber, ...others].slice(0, MAX_SIGNED_PARTS);
209
+ const done = sign(batch)
201
210
  .then((signed) => {
202
211
  for (const grant of signed)
203
212
  grants.set(grant.partNumber, grant);
@@ -205,43 +214,67 @@ export async function uploadParts(sign, file, sizeBytes, plan, deps, parts = Arr
205
214
  .finally(() => {
206
215
  signing = null;
207
216
  });
217
+ signing = { batch, done };
218
+ }
219
+ const { batch, done } = signing;
220
+ await done;
221
+ // The store was asked for this part and did not sign it (or signed it already expiring).
222
+ if (batch.includes(partNumber) && !fresh(grants.get(partNumber))) {
223
+ throw new BuildToolError("store_sign_failed", `The store did not sign part ${partNumber}.`, "Recovery: retry; a retry continues the same upload.");
208
224
  }
209
- await signing;
210
225
  }
211
- throw new BuildToolError("store_sign_failed", `The store did not sign part ${partNumber}.`, "Recovery: retry; a retry continues the same upload.");
212
226
  };
213
- const worker = async () => {
214
- for (let partNumber = queue.shift(); partNumber !== undefined; partNumber = queue.shift()) {
215
- const body = await readPart(file, partNumber, plan.partSizeBytes, sizeBytes);
216
- for (let attempt = 0;; attempt++) {
227
+ const upload = async (partNumber) => {
228
+ const body = await readPart(file, partNumber, plan.partSizeBytes, sizeBytes);
229
+ for (let attempt = 0;; attempt++) {
230
+ let status = null;
231
+ let error = null;
232
+ try {
217
233
  const grant = await grantFor(partNumber);
218
- let status = null;
219
- try {
220
- const response = await deps.fetch(grant.url, {
221
- method: "PUT",
222
- headers: grant.headers,
223
- body,
224
- redirect: "error",
225
- signal: AbortSignal.timeout(600_000),
226
- });
227
- if (response.ok)
228
- break;
229
- status = response.status;
230
- }
231
- catch {
232
- status = null;
233
- }
234
- // An expired or refused signature gets a new URL on the next try.
235
- if (status === 400 || status === 403)
236
- grants.delete(partNumber);
237
- if (attempt >= PART_RETRIES) {
238
- throw new BuildToolError("store_part_upload_failed", `Part ${partNumber} of ${plan.partCount} did not upload${status ? ` (${status})` : ""}.`, "Recovery: retry with the same file and clientVersion; the upload continues where it stopped.");
239
- }
240
- await deps.sleep(500 * 2 ** attempt);
234
+ const response = await deps.fetch(grant.url, {
235
+ method: "PUT",
236
+ headers: grant.headers,
237
+ body,
238
+ redirect: "error",
239
+ signal: AbortSignal.any([stop.signal, AbortSignal.timeout(600_000)]),
240
+ });
241
+ if (response.ok)
242
+ return;
243
+ status = response.status;
244
+ }
245
+ catch (caught) {
246
+ error = caught;
247
+ }
248
+ if (stop.signal.aborted)
249
+ return;
250
+ // An expired or refused signature gets a new URL on the next try.
251
+ if (status === 400 || status === 403)
252
+ grants.delete(partNumber);
253
+ if (attempt >= PART_RETRIES) {
254
+ if (error instanceof BuildToolError)
255
+ throw error;
256
+ throw new BuildToolError("store_part_upload_failed", `Part ${partNumber} of ${plan.partCount} did not upload${status ? ` (${status})` : ""}.`, "Recovery: retry with the same file and clientVersion; the upload continues where it stopped.");
257
+ }
258
+ await deps.sleep(500 * 2 ** attempt);
259
+ if (stop.signal.aborted)
260
+ return;
261
+ }
262
+ };
263
+ const worker = async () => {
264
+ for (let partNumber = queue.shift(); partNumber !== undefined && !stop.signal.aborted; partNumber = queue.shift()) {
265
+ try {
266
+ await upload(partNumber);
267
+ unfinished.delete(partNumber);
268
+ }
269
+ catch (error) {
270
+ failure ??= error;
271
+ stop.abort();
241
272
  }
242
273
  }
243
274
  };
244
275
  await Promise.all(Array.from({ length: Math.min(CONCURRENCY, queue.length) }, worker));
276
+ if (stop.signal.aborted)
277
+ throw failure;
245
278
  }
246
279
  export function normalizeClientVersion(value) {
247
280
  const raw = value?.trim() ?? "";
@@ -186,8 +186,8 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
186
186
  value?: string | number | boolean | undefined;
187
187
  instance?: string | undefined;
188
188
  name?: string | undefined;
189
- cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
190
189
  space?: "local" | "global" | "both" | undefined;
190
+ cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
191
191
  blend?: number | undefined;
192
192
  position?: number | undefined;
193
193
  speed?: number | undefined;
@@ -205,8 +205,8 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
205
205
  value?: string | number | boolean | undefined;
206
206
  instance?: string | undefined;
207
207
  name?: string | undefined;
208
- cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
209
208
  space?: "local" | "global" | "both" | undefined;
209
+ cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
210
210
  blend?: number | undefined;
211
211
  position?: number | undefined;
212
212
  speed?: number | undefined;
@@ -64,10 +64,10 @@ export interface RemoteMcpResult {
64
64
  signIn: string;
65
65
  }
66
66
  /**
67
- * Adds the Summer Games MCP next to summer-engine in the same config file.
68
- * An entry that already points at the same URL, under any name, counts as
69
- * added and is left as it is. Returns null for agents without a remote-server
70
- * entry in agent-table.ts.
67
+ * Adds the Summer Games MCP next to summer-engine in the same config file, in
68
+ * the agent's own entry shape (remoteMcp in agent-table.ts). An entry that
69
+ * already points at the same URL, under any name, counts as added and is left
70
+ * as it is. Returns null for agents without remoteMcp.
71
71
  */
72
72
  export declare function configureSummerGamesMcp(options: Pick<AgentConfigOptions, "agent" | "scope" | "dryRun" | "cwd" | "env">): Promise<RemoteMcpResult | null>;
73
73
  export declare const DEFAULT_CHANNEL = "latest";
@@ -85,4 +85,4 @@ export declare function mcpEntry(format: McpFormat, server: StdioMcpServerConfig
85
85
  export declare function resolvePackageRoot(): string;
86
86
  /** Directory name Gemini expects the extension under; the manifest `name` must match it. */
87
87
  export declare const GEMINI_EXTENSION_DIR_NAME = "summer-engine";
88
- export declare function upsertTomlArrayBlock(content: string, block: string): string;
88
+ export declare function upsertTomlArrayBlock(content: string, block: string, name?: string): string;
@@ -79,10 +79,10 @@ export async function configureAgentMcp(options) {
79
79
  export const SUMMER_GAMES_MCP_SERVER_NAME = "summer-games";
80
80
  export const SUMMER_GAMES_MCP_URL = "https://mcp.summer.games/mcp";
81
81
  /**
82
- * Adds the Summer Games MCP next to summer-engine in the same config file.
83
- * An entry that already points at the same URL, under any name, counts as
84
- * added and is left as it is. Returns null for agents without a remote-server
85
- * entry in agent-table.ts.
82
+ * Adds the Summer Games MCP next to summer-engine in the same config file, in
83
+ * the agent's own entry shape (remoteMcp in agent-table.ts). An entry that
84
+ * already points at the same URL, under any name, counts as added and is left
85
+ * as it is. Returns null for agents without remoteMcp.
86
86
  */
87
87
  export async function configureSummerGamesMcp(options) {
88
88
  const spec = agentSpec(options.agent);
@@ -92,39 +92,68 @@ export async function configureSummerGamesMcp(options) {
92
92
  const target = resolveConfigTarget(spec, options.scope, resolve(options.cwd ?? process.cwd()), env);
93
93
  const write = !options.dryRun;
94
94
  const url = SUMMER_GAMES_MCP_URL;
95
+ const name = SUMMER_GAMES_MCP_SERVER_NAME;
96
+ const entry = spec.remoteMcp.entry(url);
97
+ const key = topLevelKey(spec.format);
95
98
  let existing;
96
99
  let changed = false;
97
- if (spec.remoteMcp.format === "toml-url") {
98
- const current = await readTextFileIfExists(target.path);
99
- existing = findTomlServerByUrl(current, url);
100
- if (!existing) {
101
- const name = SUMMER_GAMES_MCP_SERVER_NAME;
102
- const next = upsertTomlTable(current, `[mcp_servers.${name}]\nurl = ${tomlString(url)}\n`, name);
100
+ switch (spec.format) {
101
+ case "toml":
102
+ case "toml-array": {
103
+ const current = await readTextFileIfExists(target.path);
104
+ existing = spec.format === "toml" ? findTomlServerByUrl(current, url) : findTomlArrayServerByUrl(current, url);
105
+ if (existing)
106
+ break;
107
+ const next = spec.format === "toml"
108
+ ? upsertTomlTable(current, renderTomlTable(`[mcp_servers.${name}]`, entry), name)
109
+ : upsertTomlArrayBlock(current, renderTomlTable("[[mcp_servers]]", entry), name);
103
110
  changed = current !== next;
104
111
  if (write && changed)
105
112
  await writeTextFile(target.path, next);
113
+ break;
106
114
  }
107
- }
108
- else {
109
- const current = await readJsonConfig(target.path);
110
- existing = findJsonServerByUrl(current, url);
111
- if (!existing) {
115
+ case "yaml-goose":
116
+ case "yaml-hermes": {
117
+ const current = await readTextFileIfExists(target.path);
118
+ const parsed = current.trim() === "" ? {} : parseDocument(current).toJS();
119
+ existing = isJsonObject(parsed) ? findServerByUrl([parsed[key]], url) : undefined;
120
+ if (existing)
121
+ break;
122
+ ({ changed } = await upsertYamlConfig(target.path, [key, name], entry, write));
123
+ break;
124
+ }
125
+ case "json-gemini":
126
+ throw new Error("The Gemini extension manifest has no Summer Games entry.");
127
+ default: {
128
+ const current = await readJsonConfig(target.path);
129
+ const maps = [current[key]];
130
+ // Claude Code's ~/.claude.json also keeps per-project servers (`claude
131
+ // mcp add` without --scope user) under projects.<dir>.mcpServers.
132
+ if (isJsonObject(current.projects)) {
133
+ for (const project of Object.values(current.projects)) {
134
+ if (isJsonObject(project))
135
+ maps.push(project.mcpServers);
136
+ }
137
+ }
138
+ existing = findServerByUrl(maps, url);
139
+ if (existing)
140
+ break;
112
141
  const next = copyJsonObject(current);
113
- next.mcpServers = mergeNamedObject(next.mcpServers, SUMMER_GAMES_MCP_SERVER_NAME, { type: "http", url }, "mcpServers");
142
+ next[key] = mergeNamedObject(next[key], name, entry, key);
114
143
  const nextRendered = renderJsonFile(next);
115
144
  changed = renderJsonFile(current) !== nextRendered;
116
145
  if (write && changed)
117
146
  await writeTextFile(target.path, nextRendered);
118
147
  }
119
148
  }
120
- const serverName = existing ?? SUMMER_GAMES_MCP_SERVER_NAME;
149
+ const serverName = existing ?? name;
121
150
  return {
122
151
  serverName,
123
152
  url,
124
153
  path: target.path,
125
154
  changed,
126
155
  wrote: write && changed,
127
- ...(existing && existing !== SUMMER_GAMES_MCP_SERVER_NAME ? { existingName: existing } : {}),
156
+ ...(existing && existing !== name ? { existingName: existing } : {}),
128
157
  signIn: spec.remoteMcp.signIn(serverName),
129
158
  };
130
159
  }
@@ -142,27 +171,62 @@ function sameMcpUrl(a, b) {
142
171
  };
143
172
  return normalize(a) === normalize(b);
144
173
  }
145
- /** The name of a server with this URL in mcpServers. Claude Code's
146
- * ~/.claude.json also keeps per-project servers (`claude mcp add` without
147
- * --scope user) under projects.<dir>.mcpServers; those count too. */
148
- function findJsonServerByUrl(config, url) {
149
- const maps = [config.mcpServers];
150
- if (isJsonObject(config.projects)) {
151
- for (const project of Object.values(config.projects)) {
152
- if (isJsonObject(project))
153
- maps.push(project.mcpServers);
154
- }
155
- }
174
+ /** Agents name a remote server's address url, serverUrl (Windsurf,
175
+ * Antigravity), httpUrl (Qwen Code) or uri (Goose). */
176
+ const REMOTE_URL_FIELDS = ["url", "serverUrl", "httpUrl", "uri"];
177
+ /** The name of a server with this URL in any of these name -> entry maps. */
178
+ function findServerByUrl(maps, url) {
156
179
  for (const map of maps) {
157
180
  if (!isJsonObject(map))
158
181
  continue;
159
182
  for (const [name, entry] of Object.entries(map)) {
160
- if (isJsonObject(entry) && sameMcpUrl(entry.url, url))
183
+ if (isJsonObject(entry) && REMOTE_URL_FIELDS.some((field) => sameMcpUrl(entry[field], url)))
161
184
  return name;
162
185
  }
163
186
  }
164
187
  return undefined;
165
188
  }
189
+ /** The name of a [[mcp_servers]] block (Mistral Vibe) whose url is this URL. */
190
+ function findTomlArrayServerByUrl(content, url) {
191
+ let block;
192
+ const done = () => (block?.matches ? block.name : undefined);
193
+ for (const line of content.split(/\r?\n/)) {
194
+ if (/^\s*\[/.test(line)) {
195
+ if (done())
196
+ return done();
197
+ block = /^\s*\[\[mcp_servers\]\]\s*(?:#.*)?$/.test(line) ? { matches: false } : undefined;
198
+ continue;
199
+ }
200
+ if (!block)
201
+ continue;
202
+ const field = /^\s*(name|url)\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(line);
203
+ if (!field)
204
+ continue;
205
+ const value = field[2] ?? field[3];
206
+ if (field[1] === "name")
207
+ block.name = value;
208
+ else if (sameMcpUrl(value, url))
209
+ block.matches = true;
210
+ }
211
+ return done();
212
+ }
213
+ /** A TOML table: the header line, then key = value for each entry field. */
214
+ function renderTomlTable(header, entry) {
215
+ return `${[header, ...Object.entries(entry).map(([field, value]) => `${field} = ${tomlValue(value)}`)].join("\n")}\n`;
216
+ }
217
+ function tomlValue(value) {
218
+ if (typeof value === "string")
219
+ return tomlString(value);
220
+ if (typeof value === "number" || typeof value === "boolean")
221
+ return String(value);
222
+ if (Array.isArray(value))
223
+ return `[${value.map(tomlValue).join(", ")}]`;
224
+ if (isJsonObject(value)) {
225
+ const fields = Object.entries(value).map(([field, inner]) => `${field} = ${tomlValue(inner)}`);
226
+ return fields.length === 0 ? "{}" : `{ ${fields.join(", ")} }`;
227
+ }
228
+ throw new Error(`Cannot write ${JSON.stringify(value)} as TOML.`);
229
+ }
166
230
  /** The name of a [mcp_servers.<name>] table whose url is this URL. */
167
231
  function findTomlServerByUrl(content, url) {
168
232
  let table;
@@ -430,10 +494,10 @@ async function upsertTomlArrayConfig(path, server, write) {
430
494
  }
431
495
  return { changed };
432
496
  }
433
- export function upsertTomlArrayBlock(content, block) {
497
+ export function upsertTomlArrayBlock(content, block, name = SUMMER_MCP_SERVER_NAME) {
434
498
  const lines = content.split(/\r?\n/);
435
499
  const header = /^\s*\[\[mcp_servers\]\]\s*(?:#.*)?$/;
436
- const nameLine = new RegExp(`^\\s*name\\s*=\\s*"${SUMMER_MCP_SERVER_NAME}"\\s*(?:#.*)?$`);
500
+ const nameLine = new RegExp(`^\\s*name\\s*=\\s*"${name}"\\s*(?:#.*)?$`);
437
501
  let start = -1;
438
502
  let end = lines.length;
439
503
  for (let index = 0; index < lines.length; index += 1) {
@@ -22,7 +22,6 @@ export type ConfigScope = "user" | "project";
22
22
  * json-gemini the Gemini extension manifest (legacy)
23
23
  */
24
24
  export type McpFormat = "json" | "json-stdio" | "json-vscode" | "json-copilot" | "json-opencode" | "json-servers" | "json-zed" | "json-amp" | "json-crush" | "json-transport" | "toml" | "toml-array" | "yaml-goose" | "yaml-hermes" | "json-gemini";
25
- export type RemoteMcpFormat = "json-http" | "toml-url";
26
25
  export type SkillLocationKind = "skill-dir" | "cursor-rule-dir" | "windsurf-rule-file" | "cline-rule-dir" | "opencode-skill-dir";
27
26
  export interface PathContext {
28
27
  home: string;
@@ -72,14 +71,16 @@ export interface AgentSpec {
72
71
  envOverride: string;
73
72
  format: McpFormat;
74
73
  /**
75
- * How this agent lists a remote MCP server that signs in with OAuth on its
76
- * own (the Summer Games player MCP). Absent: setup links the engine only.
77
- * json-http { mcpServers: { <name>: { type: "http", url } } } (Claude Code)
78
- * toml-url [mcp_servers.<name>] url (Codex)
74
+ * The Summer Games player MCP (a remote server that signs in with OAuth on
75
+ * its own) in this agent's file, under the same key as summer-engine.
76
+ * entry is the server's value; for TOML it is written as key = value lines.
79
77
  * signIn tells the person how to sign in once, for the entry's name.
78
+ * Absent: setup links the engine only (no OAuth sign-in for remote servers,
79
+ * no file entry for them, or a sign-in api.summer.games has not been checked
80
+ * to accept).
80
81
  */
81
82
  remoteMcp?: {
82
- format: RemoteMcpFormat;
83
+ entry: (url: string) => Record<string, unknown>;
83
84
  signIn: (name: string) => string;
84
85
  };
85
86
  mcp: {