summer-engine 2.6.6 → 2.7.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -2
- package/.cursor-plugin/plugin.json +4 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +8 -4
- package/CHANGELOG.md +30 -0
- package/GEMINI.md +5 -4
- package/README.md +7 -4
- package/_persona/summer/SKILL.md +1 -0
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +4 -0
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +11 -2
- package/dist/lib/api-client.js +110 -9
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/skills-registry.d.ts +25 -1
- package/dist/lib/skills-registry.js +46 -0
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +79 -7
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +143 -5
- package/dist/mcp/tools/project-tools.js +25 -20
- package/dist/mcp/tools/scene-tools.js +101 -24
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +0 -0
- package/package.json +1 -1
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/debugging/debug/SKILL.md +23 -1
- package/skills/deployment/remote-deploy/SKILL.md +107 -0
- package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
- package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +6 -6
- package/skills/workflow/using-summer/SKILL.md +20 -1
- package/skills/workflow/writing-skills/render-graphs.js +0 -0
|
@@ -2,6 +2,27 @@ import { z } from "zod";
|
|
|
2
2
|
import { withEngine } from "./with-engine.js";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
function sceneMutationOps(ops) {
|
|
6
|
+
const saveIndexes = ops
|
|
7
|
+
.map((op, index) => op.op === "SaveScene" ? index : -1)
|
|
8
|
+
.filter((index) => index >= 0);
|
|
9
|
+
if (saveIndexes.length > 1) {
|
|
10
|
+
throw new Error("A scene mutation batch may contain only one SaveScene");
|
|
11
|
+
}
|
|
12
|
+
if (saveIndexes.length === 1) {
|
|
13
|
+
if (saveIndexes[0] !== ops.length - 1) {
|
|
14
|
+
throw new Error("SaveScene must be the final operation in a scene mutation batch");
|
|
15
|
+
}
|
|
16
|
+
return ops;
|
|
17
|
+
}
|
|
18
|
+
return [...ops, { op: "SaveScene" }];
|
|
19
|
+
}
|
|
20
|
+
function executeSceneMutation(client, scenePath, ops, options) {
|
|
21
|
+
return client.executeIdentityBoundOps(sceneMutationOps(ops), {
|
|
22
|
+
...(options ?? {}),
|
|
23
|
+
scenePath,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
5
26
|
async function readMainSceneFromProject(projectPath) {
|
|
6
27
|
if (!projectPath)
|
|
7
28
|
return null;
|
|
@@ -14,6 +35,20 @@ async function readMainSceneFromProject(projectPath) {
|
|
|
14
35
|
return null;
|
|
15
36
|
}
|
|
16
37
|
}
|
|
38
|
+
function requireSuccessfulOps(result, context) {
|
|
39
|
+
const receipt = (result ?? {});
|
|
40
|
+
const results = Array.isArray(receipt.results)
|
|
41
|
+
? receipt.results
|
|
42
|
+
: [];
|
|
43
|
+
const failed = results.find((entry) => entry?.ok === false);
|
|
44
|
+
if (receipt.status === "error" || failed) {
|
|
45
|
+
const error = (typeof failed?.error === "string" && failed.error) ||
|
|
46
|
+
(typeof receipt.error === "string" && receipt.error) ||
|
|
47
|
+
`${context} failed`;
|
|
48
|
+
throw new Error(error);
|
|
49
|
+
}
|
|
50
|
+
return receipt;
|
|
51
|
+
}
|
|
17
52
|
export function registerSceneTools(server) {
|
|
18
53
|
server.tool("summer_create_scene", `Create a new empty scene file safely.
|
|
19
54
|
|
|
@@ -45,19 +80,22 @@ Recommended workflow:
|
|
|
45
80
|
throw new Error("No scene open and could not resolve main scene. Call summer_get_project_context first, then open a known scene.");
|
|
46
81
|
}
|
|
47
82
|
await client.executeOps([{ op: "OpenScene", path: templateScene }]);
|
|
48
|
-
const tree = (await client.getSceneState());
|
|
83
|
+
const tree = (await client.getSceneState(templateScene));
|
|
49
84
|
const children = tree.data?.children ?? tree.children ?? [];
|
|
50
85
|
const removeOps = children
|
|
51
86
|
.map((c) => c.path)
|
|
52
87
|
.filter((p) => typeof p === "string" && p.length > 0)
|
|
53
88
|
.map((p) => ({ op: "RemoveNode", path: `./${p}` }));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
const createOps = [
|
|
90
|
+
...removeOps,
|
|
91
|
+
{ op: "SetProp", path: ".", key: "name", value: rootName },
|
|
92
|
+
{ op: "SaveScene", path },
|
|
93
|
+
];
|
|
94
|
+
const createReceipt = requireSuccessfulOps(await executeSceneMutation(client, templateScene, createOps, {
|
|
95
|
+
groupUndo: true,
|
|
96
|
+
}), `Creating ${path}`);
|
|
59
97
|
if (currentScene && currentScene !== path) {
|
|
60
|
-
await client.executeOps([{ op: "OpenScene", path: currentScene }]);
|
|
98
|
+
requireSuccessfulOps(await client.executeOps([{ op: "OpenScene", path: currentScene }]), `Restoring ${currentScene}`);
|
|
61
99
|
}
|
|
62
100
|
return {
|
|
63
101
|
ok: true,
|
|
@@ -65,14 +103,13 @@ Recommended workflow:
|
|
|
65
103
|
rootName,
|
|
66
104
|
templateScene,
|
|
67
105
|
restoredScene: currentScene,
|
|
106
|
+
receipt: createReceipt,
|
|
68
107
|
};
|
|
69
108
|
}));
|
|
70
109
|
server.tool("summer_add_node", `Add a new node to the scene tree.
|
|
71
110
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
2) summer_open_main_scene (if no scene open)
|
|
75
|
-
3) summer_get_scene_tree
|
|
111
|
+
Pass the exact res:// scenePath to mutate. The scene does not need to be the
|
|
112
|
+
active editor tab.
|
|
76
113
|
|
|
77
114
|
Common node types:
|
|
78
115
|
- 3D: Node3D, MeshInstance3D, CharacterBody3D, RigidBody3D, StaticBody3D, Camera3D, DirectionalLight3D, OmniLight3D, SpotLight3D, WorldEnvironment, CollisionShape3D, Area3D
|
|
@@ -81,10 +118,11 @@ Common node types:
|
|
|
81
118
|
- Audio: AudioStreamPlayer, AudioStreamPlayer3D
|
|
82
119
|
|
|
83
120
|
The parent path uses "./" prefix for relative paths from scene root. E.g., "./World" means the "World" child of the root node.`, {
|
|
121
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
84
122
|
parent: z.string().describe("Parent node path, e.g. './World' or './World/Enemies'"),
|
|
85
123
|
type: z.string().describe("Godot node type, e.g. 'MeshInstance3D', 'CharacterBody3D'"),
|
|
86
124
|
name: z.string().describe("Name for the new node, e.g. 'Player', 'MainCamera'"),
|
|
87
|
-
}, async ({ parent, type, name }) => withEngine(async (client) => client
|
|
125
|
+
}, async ({ scenePath, parent, type, name }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "AddNode", parent, type, name }])));
|
|
88
126
|
server.tool("summer_set_prop", `Set a property on a node. This is the primary way to configure nodes after adding them.
|
|
89
127
|
|
|
90
128
|
VALUE FORMAT — Godot string syntax for complex types:
|
|
@@ -106,29 +144,37 @@ COMMON PROPERTIES:
|
|
|
106
144
|
- shadow_enabled: true — for lights
|
|
107
145
|
- light_energy: 1.5 — light intensity
|
|
108
146
|
- fov: 75.0 — camera field of view`, {
|
|
147
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
109
148
|
path: z.string().describe("Node path, e.g. './World/Player'"),
|
|
110
149
|
key: z.string().describe("Property name, e.g. 'position', 'mesh', 'visible'"),
|
|
111
150
|
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format for complex types, native JSON for primitives"),
|
|
112
|
-
}, async ({ path, key, value }) => withEngine(async (client) => client
|
|
151
|
+
}, async ({ scenePath, path, key, value }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "SetProp", path, key, value }])));
|
|
113
152
|
server.tool("summer_set_resource_property", `Set a nested property on a resource attached to a node.
|
|
114
153
|
|
|
115
154
|
Use when you need to modify a sub-property of a resource, like:
|
|
116
155
|
- CollisionShape3D shape size: nodePath="./Player/CollisionShape3D", resourceProperty="shape", subProperty="size", value="Vector3(1, 2, 1)"
|
|
117
156
|
- Material albedo color: nodePath="./Floor", resourceProperty="material_override", subProperty="albedo_color", value="Color(0.2, 0.5, 0.2, 1)"
|
|
118
157
|
- Mesh size: nodePath="./Box", resourceProperty="mesh", subProperty="size", value="Vector3(2, 2, 2)"`, {
|
|
158
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
119
159
|
nodePath: z.string().describe("Node path, e.g. './Player/CollisionShape3D'"),
|
|
120
160
|
resourceProperty: z.string().describe("Resource property on the node, e.g. 'shape', 'mesh', 'material_override'"),
|
|
121
161
|
subProperty: z.string().describe("Property on the resource, e.g. 'size', 'radius', 'albedo_color'"),
|
|
122
162
|
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format"),
|
|
123
|
-
}, async ({ nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => client
|
|
163
|
+
}, async ({ scenePath, nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [
|
|
124
164
|
{ op: "SetResourceProperty", nodePath, resourceProperty, subProperty, value },
|
|
125
165
|
])));
|
|
126
|
-
server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.", {
|
|
127
|
-
|
|
166
|
+
server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.", {
|
|
167
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
168
|
+
path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'"),
|
|
169
|
+
}, async ({ scenePath, path }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "RemoveNode", path }])));
|
|
170
|
+
server.tool("summer_save_scene", "Save an explicit scene to disk. Mutation tools already append one save; use this for a standalone save or save-as.", {
|
|
171
|
+
scenePath: z.string().describe("Scene to save, e.g. 'res://main.tscn'"),
|
|
172
|
+
path: z.string().optional().describe("Optional save-as path, e.g. 'res://levels/level2.tscn'"),
|
|
173
|
+
}, async ({ scenePath, path }) => withEngine(async (client) => {
|
|
128
174
|
const op = { op: "SaveScene" };
|
|
129
175
|
if (path)
|
|
130
176
|
op.path = path;
|
|
131
|
-
return client
|
|
177
|
+
return executeSceneMutation(client, scenePath, [op]);
|
|
132
178
|
}));
|
|
133
179
|
server.tool("summer_open_scene", `Open a scene file in the editor. Use this to switch between scenes.
|
|
134
180
|
|
|
@@ -142,14 +188,15 @@ Do not guess paths. Prefer:
|
|
|
142
188
|
- Compose scenes from smaller scenes (e.g., add a "Player" scene into a "Level" scene)
|
|
143
189
|
|
|
144
190
|
The scene must already exist in the project. Use summer_import_from_url first if importing from external sources.`, {
|
|
191
|
+
scenePath: z.string().describe("Target scene to receive the instance, e.g. 'res://main.tscn'"),
|
|
145
192
|
parent: z.string().describe("Parent node path, e.g. './World'"),
|
|
146
193
|
scene: z.string().describe("Scene/model path, e.g. 'res://player.tscn' or 'res://models/tree.glb'"),
|
|
147
194
|
name: z.string().optional().describe("Override the instance name"),
|
|
148
|
-
}, async ({ parent, scene, name }) => withEngine(async (client) => {
|
|
195
|
+
}, async ({ scenePath, parent, scene, name }) => withEngine(async (client) => {
|
|
149
196
|
const op = { op: "InstantiateScene", parent, scene };
|
|
150
197
|
if (name)
|
|
151
198
|
op.name = name;
|
|
152
|
-
return client
|
|
199
|
+
return executeSceneMutation(client, scenePath, [op]);
|
|
153
200
|
}));
|
|
154
201
|
server.tool("summer_connect_signal", `Connect a signal between two nodes. Signals are Godot's event system — they notify when something happens.
|
|
155
202
|
|
|
@@ -161,11 +208,12 @@ Common signals:
|
|
|
161
208
|
- "input_event" — CollisionObject received input
|
|
162
209
|
|
|
163
210
|
The receiver node must have a script with the specified method.`, {
|
|
211
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
164
212
|
emitter: z.string().describe("Node that fires the signal, e.g. './Player/HitArea'"),
|
|
165
213
|
signal: z.string().describe("Signal name, e.g. 'body_entered'"),
|
|
166
214
|
receiver: z.string().describe("Node with the handler script, e.g. './Player'"),
|
|
167
215
|
method: z.string().describe("Method name in the receiver's script, e.g. '_on_hit_area_body_entered'"),
|
|
168
|
-
}, async ({ emitter, signal, receiver, method }) => withEngine(async (client) => client
|
|
216
|
+
}, async ({ scenePath, emitter, signal, receiver, method }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [
|
|
169
217
|
{ op: "ConnectSignal", emitter, signal, receiver, method },
|
|
170
218
|
])));
|
|
171
219
|
server.tool("summer_select_node", "Select a node in the editor's scene tree and show it in the inspector panel. Useful for focusing the editor on a specific node.", {
|
|
@@ -178,16 +226,17 @@ The receiver node must have a script with the specified method.`, {
|
|
|
178
226
|
return client.executeOps([op]);
|
|
179
227
|
}));
|
|
180
228
|
server.tool("summer_replace_node", "Replace a node with a different type or scene, preserving its position in the tree and its children. Useful for changing a StaticBody3D to a RigidBody3D, or swapping a placeholder with a proper prefab.", {
|
|
229
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
181
230
|
path: z.string().describe("Node path to replace"),
|
|
182
231
|
type: z.string().optional().describe("New node type, e.g. 'RigidBody3D'"),
|
|
183
232
|
scene: z.string().optional().describe("Scene to replace with, e.g. 'res://enemies/boss.tscn'"),
|
|
184
|
-
}, async ({ path, type, scene }) => withEngine(async (client) => {
|
|
233
|
+
}, async ({ scenePath, path, type, scene }) => withEngine(async (client) => {
|
|
185
234
|
const op = { op: "ReplaceNode", path };
|
|
186
235
|
if (type)
|
|
187
236
|
op.type = type;
|
|
188
237
|
if (scene)
|
|
189
238
|
op.scene = scene;
|
|
190
|
-
return client
|
|
239
|
+
return executeSceneMutation(client, scenePath, [op]);
|
|
191
240
|
}));
|
|
192
241
|
server.tool("summer_inspect_node", `Get all editable properties of a node with their current values, types, and resource info.
|
|
193
242
|
|
|
@@ -213,7 +262,35 @@ Each op in the array uses the same format as the individual tools:
|
|
|
213
262
|
|
|
214
263
|
RAW RUNTIME OPS (interactive verification — engine-build dependent; structured failure_reason incl "unsupported" passes through verbatim):
|
|
215
264
|
- SimulateInput — drive the RUNNING game (summer_play first): {"op": "SimulateInput", "type": "action", "action": "jump", "pressed": true}. type is "action" | "key" | "mouse_click" | "axis".
|
|
216
|
-
- RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "extends SummerProbeBase\\nfunc _ready(): await super._ready(); report('ok', true); finish()", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report()/save_frame()/press()/key()/finish()
|
|
265
|
+
- RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "extends SummerProbeBase\\nfunc _ready(): await super._ready(); report('ok', true); finish()", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report()/save_frame()/press()/key()/finish().
|
|
266
|
+
|
|
267
|
+
Do not mix OpenScene with scene mutations in one batch. OpenScene is a UI action;
|
|
268
|
+
send it separately. scenePath selects every mutation target. The tool appends one
|
|
269
|
+
final SaveScene when the batch mutates a scene; if supplied explicitly, SaveScene
|
|
270
|
+
must appear exactly once and be the final operation.`, {
|
|
271
|
+
scenePath: z.string().optional().describe("Required when ops contains scene mutations; exact res:// target scene path"),
|
|
217
272
|
ops: z.array(z.record(z.unknown())).describe("Array of operation objects, each with 'op' plus its parameters"),
|
|
218
|
-
}, async ({ ops }) => withEngine(async (client) =>
|
|
273
|
+
}, async ({ scenePath, ops }) => withEngine(async (client) => {
|
|
274
|
+
const rawFileMutation = ops.find((op) => {
|
|
275
|
+
const kind = String(op.op ?? "");
|
|
276
|
+
return kind === "WriteFile" || kind === "ReplaceText";
|
|
277
|
+
});
|
|
278
|
+
if (rawFileMutation) {
|
|
279
|
+
throw new Error(`summer_batch does not accept raw ${String(rawFileMutation.op)} operations. ` +
|
|
280
|
+
"Use summer_write_file or summer_replace_text so project identity, content guards, and same-file ordering are enforced.");
|
|
281
|
+
}
|
|
282
|
+
const sceneMutations = new Set([
|
|
283
|
+
"AddNode", "RemoveNode", "MoveNode", "ReparentNode", "ReplaceNode",
|
|
284
|
+
"SetProp", "SetResourceProperty", "ConnectSignal", "DisconnectSignal",
|
|
285
|
+
"InstantiateScene", "SaveScene", "Undo",
|
|
286
|
+
]);
|
|
287
|
+
const needsScenePath = ops.some((op) => sceneMutations.has(String(op.op ?? "")));
|
|
288
|
+
if (needsScenePath && !scenePath) {
|
|
289
|
+
throw new Error("summer_batch requires scenePath when ops contains scene mutations");
|
|
290
|
+
}
|
|
291
|
+
const options = { groupUndo: true, ...(scenePath ? { scenePath } : {}) };
|
|
292
|
+
return needsScenePath
|
|
293
|
+
? executeSceneMutation(client, scenePath, ops, options)
|
|
294
|
+
: client.executeOps(ops, options);
|
|
295
|
+
}));
|
|
219
296
|
}
|
|
@@ -19,7 +19,10 @@ const SUCCESS_TERMINAL_STATES = new Set(["applied", "no_op"]);
|
|
|
19
19
|
// `error` string (queue-full / lease-reject / identity-mismatch / no-progress
|
|
20
20
|
// timeout frequently arrive with terminalState set and results[] absent).
|
|
21
21
|
const TERMINAL_STATE_MESSAGES = {
|
|
22
|
-
timed_out: "Engine operation timed out
|
|
22
|
+
timed_out: "Engine operation timed out. Its final state is unknown; inspect the target before retrying.",
|
|
23
|
+
still_queued: "Summer Engine accepted the operation, but it was still queued when the client stopped waiting. It may still run; do not retry blindly.",
|
|
24
|
+
still_running: "Summer Engine accepted and started the operation, but no final receipt arrived. It may still be running or may already have applied; inspect the target before retrying.",
|
|
25
|
+
uncertain: "Summer Engine did not provide a final operation receipt. The current state is uncertain; inspect the target before retrying.",
|
|
23
26
|
not_connected: "Summer Engine is not connected (terminalState: not_connected). Nothing was applied.",
|
|
24
27
|
identity_mismatch: "Operation rejected — wrong project/instance (terminalState: identity_mismatch). Nothing was mutated.",
|
|
25
28
|
content_mismatch: "Operation rejected — content changed since last read (terminalState: content_mismatch). Nothing was applied.",
|
|
@@ -117,9 +120,6 @@ function buildActionHint(message) {
|
|
|
117
120
|
if (normalized.includes("failed to open scene")) {
|
|
118
121
|
return "Scene path could not be opened. Call `summer_get_project_context` to get `mainScene`, then open that exact path. Avoid guessing scene filenames.";
|
|
119
122
|
}
|
|
120
|
-
if (normalized.includes("writefile cannot edit .tscn/.scn")) {
|
|
121
|
-
return "Write .gd/.cs/.json/docs/simple config files with normal file-edit tools. Use Summer MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, and visual verification.";
|
|
122
|
-
}
|
|
123
123
|
return null;
|
|
124
124
|
}
|
|
125
125
|
export async function withEngine(fn, opts) {
|
|
File without changes
|
package/hooks/run-hook.cmd
CHANGED
|
File without changes
|
package/hooks/session-start.sh
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Local Summer CLI and MCP server for Summer Engine. Install and run the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games with bundled skills, hooks, and plugins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"summer-engine",
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
6
|
category: 2d-assets
|
|
7
7
|
user-invocable: true
|
|
8
|
-
allowed-tools: Read Grep Glob Write Edit summer_generate_image summer_search_assets summer_import_from_url
|
|
8
|
+
allowed-tools: Read Grep Glob Write Edit summer_get_studio_workflow summer_generate_image summer_slice_asset_sheet summer_search_assets summer_import_asset_by_id summer_import_from_url
|
|
9
9
|
paths: ["assets/**", "art/**", "sprites/**"]
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -58,13 +58,16 @@ Direct the user there if they're working in the browser. The wizard:
|
|
|
58
58
|
- Has per-slice HD upscale + bulk "Upscale all"
|
|
59
59
|
- Saves as a pack ArtAsset visible in the user's library
|
|
60
60
|
|
|
61
|
-
If the user is in a chat / agent context
|
|
61
|
+
If the user is in a chat / agent context, use the programmatic path below. It
|
|
62
|
+
now calls the same server-side slicer as the Studio wizard.
|
|
62
63
|
|
|
63
64
|
## Programmatic path (chat / agent driving the engine)
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
1. **Load the exact current recipe.** Call
|
|
67
|
+
`summer_get_studio_workflow({workflowId: "asset-pack"})`. Follow its ordered
|
|
68
|
+
steps and report any `manual` handoff instead of pretending it ran.
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
2. **Generate the sheet.** Call `summer_generate_image` with a prompt like:
|
|
68
71
|
|
|
69
72
|
```
|
|
70
73
|
<theme> asset sheet: <comma-separated list of items>. Each item isolated with empty space around it, transparent or neutral background.
|
|
@@ -74,21 +77,18 @@ The slicing + classification endpoints are not yet exposed as `summer_*` MCP too
|
|
|
74
77
|
- 1024x1024 is the sweet spot. Larger sheets fragment in the slicer when individual assets get below ~64px.
|
|
75
78
|
- Keep the asset list to 6–20 items per sheet. Past 20, model attention degrades and slices get muddy.
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
3. **Detect + name + classify.** Send the transparent PNG to a vision LLM (Gemini 3 Pro is currently the strongest at bbox + category in one shot). Use this system prompt structure:
|
|
80
|
+
3. **Slice, remove background, detect, name, classify, crop, and save.** Pass
|
|
81
|
+
the returned image asset id to:
|
|
80
82
|
|
|
81
83
|
```
|
|
82
|
-
|
|
83
|
-
[{"name":"snake_case_name","category":"character|terrain|nature|lighting|water|decorative|buildings|ui|vfx","x":<0-100>,"y":<0-100>,"w":<0-100>,"h":<0-100>}]
|
|
84
|
-
|
|
85
|
-
Coordinates are percentages of image dimensions, top-left origin.
|
|
86
|
-
Names are 1-3 words, snake_case, specific (stone_lantern, not lamp).
|
|
84
|
+
summer_slice_asset_sheet({ assetId: "<generated asset id>" })
|
|
87
85
|
```
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
This is the same cloud slicing pipeline used by the Studio flow. Keep the
|
|
88
|
+
returned slice ids/names/categories; do not reimplement the vision boxes
|
|
89
|
+
locally.
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
4. **Optional upscale.** For any crop where longest side < 256px, send through fal flux-2/edit i2i with prompt:
|
|
92
92
|
|
|
93
93
|
```
|
|
94
94
|
Recreate this exact <name> at high resolution. Preserve every detail, the exact pose, the exact style. Sharper edges, more detail. Transparent background. Single isolated asset, no scene.
|
|
@@ -97,7 +97,9 @@ The slicing + classification endpoints are not yet exposed as `summer_*` MCP too
|
|
|
97
97
|
- Scale longest side to 1024, preserve aspect ratio (FLUX requires multiples of 32).
|
|
98
98
|
- Don't blindly upscale everything — high-res slices get diminishing returns and you spend $0.04/slice.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
5. **Import what the game needs.** Use the returned asset ids with
|
|
101
|
+
`summer_import_asset_by_id`. Keep the source pack in My Assets so the agent
|
|
102
|
+
and the human Studio session see the same result.
|
|
101
103
|
|
|
102
104
|
## Prompt patterns that work
|
|
103
105
|
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: [Cursor, Claude Code, Codex, Windsurf, Gemini, OpenCode]
|
|
6
6
|
category: debugging
|
|
7
7
|
user-invocable: true
|
|
8
|
-
allowed-tools: Read Edit Grep summer_get_diagnostics summer_get_script_errors summer_get_console summer_clear_console summer_get_debugger_errors summer_play summer_stop summer_is_running summer_inspect_node summer_inspect_resource summer_get_scene_tree
|
|
8
|
+
allowed-tools: Read Edit Grep summer_create_debug_report summer_get_diagnostics summer_get_script_errors summer_get_console summer_clear_console summer_get_debugger_errors summer_play summer_stop summer_is_running summer_inspect_node summer_inspect_resource summer_get_scene_tree
|
|
9
9
|
paths: ["**/*.gd", "**/*.cs", "**/*.tscn", "**/*.tres", "project.godot"]
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -18,6 +18,7 @@ The disciplined debugging loop for Godot/Summer projects. Read the error before
|
|
|
18
18
|
## When to use this skill
|
|
19
19
|
|
|
20
20
|
- The user says "it crashes", "it broke", "throws an error", "doesn't work", "freezes", "wrong behavior".
|
|
21
|
+
- The user invokes `/summer debug`, asks for a support report, or wants something they can send to Summer.
|
|
21
22
|
- The user pastes a stack trace.
|
|
22
23
|
- A previous build or test step failed and the user wants to fix it.
|
|
23
24
|
|
|
@@ -35,6 +36,27 @@ The disciplined debugging loop for Godot/Summer projects. Read the error before
|
|
|
35
36
|
|
|
36
37
|
Do not skip steps. Do not loop back to "Hypothesize" without re-running the cheap diagnostic.
|
|
37
38
|
|
|
39
|
+
### 0. Support report mode
|
|
40
|
+
|
|
41
|
+
If the user asks for `/summer debug`, "send this to Summer", "make a report", or they are stuck in Codex/cloud/another agent environment, create a portable report first:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
summer_create_debug_report({
|
|
45
|
+
issue: "<one-sentence user symptom>",
|
|
46
|
+
include_play_session: <true only when the issue appears during runtime>
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Tell the user where the Markdown file was written and remind them to review local paths and stack traces before sending. Then continue the normal debug loop only if they also asked you to fix the bug.
|
|
51
|
+
|
|
52
|
+
Fallback when the MCP tool is not available but shell is available:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
summer debug "<one-sentence user symptom>"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Add `--play` only when the issue appears after pressing Play.
|
|
59
|
+
|
|
38
60
|
### 1. Listen
|
|
39
61
|
|
|
40
62
|
Ask the user **one** focused question and wait for the answer:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: remote-deploy
|
|
3
|
+
description: Use when the user wants to run or test the game on a real target instead of the editor — a phone, tablet, or another computer ("deploy to my phone", "run on device", "test on Android/iOS", "remote deploy", "one-click deploy", "play on hardware"). Covers the topnav Remote Deploy button, runnable export presets, export templates, and on-device remote debugging. Use when the Remote Deploy button is greyed out and the user wants to know why.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: deployment
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Edit summer_project_setting summer_get_diagnostics summer_get_console summer_get_script_errors
|
|
9
|
+
paths: ["project.godot", "export_presets.cfg"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /remote-deploy — Test the Game on a Real Device
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Remote Deploy runs the game on an actual target device — a phone, tablet, or another machine — instead of inside the editor. It exports a **debug** build, installs and launches it on the device, and connects the running game back to the editor's debugger so the Output panel, errors, breakpoints, and profiler all work exactly like a local run.
|
|
17
|
+
|
|
18
|
+
**Core principle:** Remote Deploy is "Play, but on the hardware." Same debug loop, real device.
|
|
19
|
+
|
|
20
|
+
It is **not** the same as two neighbours:
|
|
21
|
+
|
|
22
|
+
| Action | What it does | Use it for |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| **Play** (▶) | Runs in the editor (embedded or windowed) | Fast iteration on the dev machine |
|
|
25
|
+
| **Remote Deploy** | Debug build → installs + runs on a device, remote-debugged | Testing real input/perf/screen on hardware |
|
|
26
|
+
| **export-and-ship** (`summer:export-and-ship`) | Final, signed distribution builds | Submitting to stores |
|
|
27
|
+
|
|
28
|
+
If the user wants a release build for Steam / itch / the App Store, that is `summer:export-and-ship`, not this.
|
|
29
|
+
|
|
30
|
+
## Where it is in the UI
|
|
31
|
+
|
|
32
|
+
- **Agent-layout topnav:** the button with the remote-play icon, immediately right of the **Play/Stop** button and the **`▾`** run-options dropdown. Hover tooltip reads **"Remote Deploy"**.
|
|
33
|
+
- Clicking it opens a **dropdown of detected targets**, grouped by platform (e.g. an `Android` header with each connected device under it). Picking a target deploys and runs there.
|
|
34
|
+
- It is **disabled (greyed out)** with the tooltip **"No Remote Deploy export presets configured."** until the prerequisites below are met. A greyed button is the #1 thing users ask about — it almost always means "no runnable preset or no device detected," not a bug.
|
|
35
|
+
|
|
36
|
+
## Prerequisites — what makes the button light up
|
|
37
|
+
|
|
38
|
+
The button auto-enables the moment a runnable preset **and** at least one detected target both exist for the same platform.
|
|
39
|
+
|
|
40
|
+
| Requirement | How to satisfy it | Why it matters |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| A **runnable export preset** for the platform | Project → Export → add a preset (Android / iOS / Web / desktop) → toggle **Runnable** on | The dropdown only lists platforms that have a *runnable* preset. A preset that exists but isn't marked Runnable does **not** count. |
|
|
43
|
+
| **Export templates** installed for that platform/engine version | Export dialog → **Manage Export Templates…** → download | Needed to actually build the debug build when you deploy. |
|
|
44
|
+
| The platform **toolchain** | Android: Android SDK + `adb` + a debug keystore. iOS: Xcode + signing. Web: any browser. | The deploy step shells out to these; without them the build can't be installed/run. |
|
|
45
|
+
| A **detected target** | Android: a device with USB debugging enabled (and authorized), or a running emulator. iOS: a connected, trusted device. Web: the built-in **"Run in Browser"** target. | The button enables only when ≥1 target is reported for a runnable platform. |
|
|
46
|
+
|
|
47
|
+
**Important nuance:** the button does **not** pre-check that templates/toolchain are installed. Enable state is gated only on *runnable preset + detected target*. If templates or the toolchain are missing, the deploy **fails at run time** and the errors appear in a result dialog. So:
|
|
48
|
+
|
|
49
|
+
> **Runnable preset + a detected device make the button appear/enable. Installed export templates make the deploy actually succeed.**
|
|
50
|
+
|
|
51
|
+
This matters when guiding a user mid-setup (e.g. building out a mobile flow): you can see the button enable as soon as a phone is plugged in and the preset is Runnable, even before templates finish downloading.
|
|
52
|
+
|
|
53
|
+
## How it works (the flow)
|
|
54
|
+
|
|
55
|
+
1. The editor continuously polls the export platforms and rebuilds the dropdown: each runnable platform, with each of its detected devices listed underneath.
|
|
56
|
+
2. The user picks a target. Godot exports a **debug** build for that preset.
|
|
57
|
+
3. The build is installed and launched on the device (e.g. `adb install` + launch for Android).
|
|
58
|
+
4. The running game opens a **remote-debug** connection back to the editor — Output, the debugger, and the profiler reflect the on-device session. Stop it from the editor like any normal run.
|
|
59
|
+
5. Any export/deploy error (missing template, no toolchain, unsigned build, device went away) is surfaced in a **result dialog**. Read it; it names the prerequisite to fix.
|
|
60
|
+
|
|
61
|
+
## Mobile quick paths
|
|
62
|
+
|
|
63
|
+
**Android**
|
|
64
|
+
1. Enable Developer Options → USB debugging on the phone; plug it in; accept the "Allow USB debugging?" prompt.
|
|
65
|
+
2. Confirm `adb devices` lists it as `device` (not `unauthorized`).
|
|
66
|
+
3. Project → Export → Android preset → **Runnable** on; install Android export templates.
|
|
67
|
+
4. The phone appears in the Remote Deploy dropdown → pick it.
|
|
68
|
+
|
|
69
|
+
**iOS**
|
|
70
|
+
1. Connect and trust the device; ensure a valid signing team in the iOS preset.
|
|
71
|
+
2. Xcode command-line tools installed; iOS export templates installed.
|
|
72
|
+
3. The device appears in the dropdown → pick it.
|
|
73
|
+
|
|
74
|
+
**Web (sanity check on the target browser)**
|
|
75
|
+
1. Web (HTML5) preset → **Runnable** on; Web export templates installed.
|
|
76
|
+
2. Pick **"Run in Browser"** from the dropdown.
|
|
77
|
+
|
|
78
|
+
## Guiding the user (orchestrator playbook)
|
|
79
|
+
|
|
80
|
+
When a user says "deploy to my phone" / "run this on device" and the button is greyed:
|
|
81
|
+
|
|
82
|
+
1. **Check the build is healthy first** — a project with script errors won't export.
|
|
83
|
+
- `summer_get_script_errors` and `summer_get_diagnostics` clean? If not, fix those before anything else.
|
|
84
|
+
- `summer_get_console` for "import failed" noise.
|
|
85
|
+
2. **Confirm a runnable preset exists** for the target platform — Read/Grep `export_presets.cfg` for the platform and a `runnable=true` entry. If missing, walk them through Project → Export → add preset → mark Runnable.
|
|
86
|
+
3. **Confirm the main scene is set** — `summer_project_setting` / `project.godot` `application/run/main_scene`. A deploy to a blank main scene launches to nothing.
|
|
87
|
+
4. **Confirm export templates + toolchain** for the platform are installed (Manage Export Templates; adb/Xcode/browser).
|
|
88
|
+
5. **Confirm a device is detected** — plugged in, debugging enabled, authorized.
|
|
89
|
+
6. Then point them at the topnav **Remote Deploy** button → pick the device.
|
|
90
|
+
|
|
91
|
+
Order matters: preset → templates/toolchain → device. Resolve them top-down; the button enables as soon as preset + device are both true, and the deploy succeeds once templates/toolchain are present.
|
|
92
|
+
|
|
93
|
+
## Common mistakes
|
|
94
|
+
|
|
95
|
+
- **Confusing it with shipping.** Remote Deploy makes *debug* builds for testing. Store/distribution builds are `summer:export-and-ship`.
|
|
96
|
+
- **Preset exists but isn't Runnable** → button stays greyed. The Runnable toggle is the gate, not the preset's existence.
|
|
97
|
+
- **Expecting it to work without export templates** → button may be enabled (device detected) but the deploy then fails in the result dialog. Install templates.
|
|
98
|
+
- **Android device `unauthorized`** in `adb devices` → the device won't show as a target. Re-accept the USB-debugging prompt on the phone.
|
|
99
|
+
- **Trying to script it via MCP** — there is no `summer_*` tool that triggers Remote Deploy; it is a UI action. The orchestrator's job is to get the prerequisites green and point the user at the button.
|
|
100
|
+
|
|
101
|
+
## Quick reference
|
|
102
|
+
|
|
103
|
+
| Button state | Meaning | Fix |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| Greyed, "No Remote Deploy export presets configured." | No runnable preset **or** no detected device | Add a Runnable preset; connect/authorize a device |
|
|
106
|
+
| Enabled, dropdown lists devices | Ready to deploy | Pick a device to build + run |
|
|
107
|
+
| Deploy fails in result dialog | Templates/toolchain/signing/device issue | Read the dialog; install the named missing piece |
|
|
@@ -146,21 +146,21 @@ Compose the 1-page brief. Format:
|
|
|
146
146
|
|
|
147
147
|
### 9. Ask permission and write
|
|
148
148
|
|
|
149
|
-
**Preferred (Summer MCP
|
|
149
|
+
**Preferred (Summer MCP):**
|
|
150
150
|
|
|
151
151
|
> May I create `.summer/GameSoul.md` with this brief?
|
|
152
152
|
|
|
153
|
-
On yes, use `
|
|
153
|
+
On yes, use guarded `summer_write_file` — `.summer/GameSoul.md` is plain markdown but still belongs to the bound project:
|
|
154
154
|
|
|
155
155
|
```
|
|
156
|
-
|
|
156
|
+
summer_write_file path="res://.summer/GameSoul.md" create_only=true content="<brief>"
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
If `.summer/GameSoul.md` already exists, read it first, show what would change, ask:
|
|
160
160
|
|
|
161
161
|
> `.summer/GameSoul.md` already exists. May I overwrite it, or merge into a "Revision 2" section below the existing brief?
|
|
162
162
|
|
|
163
|
-
**Fallback (
|
|
163
|
+
**Fallback (Summer MCP unavailable — agent is read-only):**
|
|
164
164
|
|
|
165
165
|
Print the full brief to the user with the explicit instruction:
|
|
166
166
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: summer-cloud
|
|
3
|
+
description: Use when the user wants to sync a Summer Engine project, move it to another machine or laptop, share the exact bytes with a collaborator, back up big generated assets that do not belong in git, restore an earlier version, or wants a safety net before a risky bulk operation. Trigger on "sync", "Summer Cloud", "cloud push", "cloud pull", "move to another machine", "other computer", "second machine", "share project", "back up assets", "big assets", "collaborator", "teammate", "restore a version", "checkpoint", "conflict".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: scene-and-project
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read summer_cloud_init summer_cloud_status summer_cloud_push summer_cloud_pull summer_cloud_restore summer_cloud_checkpoints summer_cloud_conflicts
|
|
9
|
+
paths: ["summer-cloud.json"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Summer Cloud: Project Sync
|
|
13
|
+
|
|
14
|
+
## What Summer Cloud is
|
|
15
|
+
|
|
16
|
+
Summer Cloud is content-addressed sync for a whole Summer Engine project. Every tracked file is identified by its sha256 hash, so identical bytes upload once and dedupe across versions and projects. It syncs the entire project tree at once (code, scenes, resources, import sidecars, and the heavy binary assets), not a hand-picked folder. The only git-tracked cloud artifact is a tiny `summer-cloud.json` binding at the project root; all real sync state lives under `.summer/local/cloud/` and is never committed.
|
|
17
|
+
|
|
18
|
+
This is not a folder backup and it is not a git replacement. Code that lives happily in git can stay in git. What Cloud adds is a way to move the large generated binaries (models, textures, audio, video) that do not belong in a git repo, and to keep a project byte-identical across machines and collaborators. Git and Cloud can run side by side; the binding file merges trivially.
|
|
19
|
+
|
|
20
|
+
## When to offer it (proactive triggers)
|
|
21
|
+
|
|
22
|
+
Surface Cloud, do not wait to be asked, when any of these are true:
|
|
23
|
+
|
|
24
|
+
- The project has heavy generated assets (large `.glb`, `.png`, `.wav`, `.ogg`, `.mp4` under `assets/`) that should not go into git. Cloud is how those travel.
|
|
25
|
+
- The user mentions a second machine, a laptop, "my other computer", or working from somewhere else. Cloud is how the same bytes show up there.
|
|
26
|
+
- A teammate or collaborator needs the exact same project state. Add them as a collaborator and they pull.
|
|
27
|
+
- A risky bulk operation is about to run (mass asset import, a large refactor, a destructive cleanup). A push first gives a restorable version to fall back to.
|
|
28
|
+
|
|
29
|
+
If none of these apply, do not push Cloud on the user. It is a tool for a real need, not a default step.
|
|
30
|
+
|
|
31
|
+
## Command and tool surface
|
|
32
|
+
|
|
33
|
+
| Action | CLI | MCP tool |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Enable Cloud for this project | `summer cloud init` | `summer_cloud_init` |
|
|
36
|
+
| Show what would push/pull | `summer cloud status` | `summer_cloud_status` |
|
|
37
|
+
| Upload local changes | `summer cloud push` | `summer_cloud_push` |
|
|
38
|
+
| Download remote changes | `summer cloud pull` | `summer_cloud_pull` |
|
|
39
|
+
| Restore an earlier version or a local checkpoint | `summer cloud restore --version N` / `--checkpoint <stamp>` | `summer_cloud_restore` |
|
|
40
|
+
| List local pre-sync checkpoints | `summer cloud checkpoints` | `summer_cloud_checkpoints` |
|
|
41
|
+
| List or recover conflict sets | `summer cloud conflicts list` / `restore <path>` | `summer_cloud_conflicts` |
|
|
42
|
+
|
|
43
|
+
Run `summer cloud status` before any push or pull. It reports the file counts, what would move in each direction, and whether any conflict sets are waiting. Never push blind.
|
|
44
|
+
|
|
45
|
+
## The safety model (respect this)
|
|
46
|
+
|
|
47
|
+
The whole point of Cloud is that user data is never destroyed silently. The rules below are how that holds. Do not work around them.
|
|
48
|
+
|
|
49
|
+
- **Every push is a restorable version.** The server keeps the version history within the retention window. Version numbers are never reused. If a bad push lands, `summer cloud restore --version N` brings the project back as a new head version, then pulls it locally.
|
|
50
|
+
- **Local checkpoints come before destructive applies.** Before a pull modifies or deletes any existing file, the client writes a full-tree checkpoint into the project's local SummerGit repo. `summer cloud checkpoints` lists them; `summer cloud restore --checkpoint <stamp>` rolls back. Note the honest caveat the restore prints: it restores bytes but does not delete files a bad sync added, so review and remove any extras it lists.
|
|
51
|
+
- **Large deletions are gated.** A push that would delete many cloud files (more than 10 files, or at least 20% of the tracked tree, or the entire tree) requires `confirmDeletes: true` on the MCP tool (or `--confirm-deletes` on the CLI). Never pass it blindly. First run `summer cloud status`, read the delete count out loud to the user, and only set the flag once they confirm that count is what they intend. If the local tree reads as completely empty against a non-empty base (a wrong directory or an unmounted volume), the push aborts outright and no flag can force it.
|
|
52
|
+
- **Conflicts become conflict sets, never silent overwrites.** Concurrent edits to the same file are resolved deterministically: the first sync to land wins the canonical path (this is "first to sync wins", not "most recent save wins", because there are no wall clocks). The losing bytes are preserved, never discarded, in a local conflict set and backed up to the cloud. Surface them with `summer cloud conflicts list` and recover any with `summer cloud conflicts restore <path>`, which re-enters the file as a fresh edit. `project.godot` is the one file merged key by key rather than whole-file, so input actions and autoloads from both sides survive.
|
|
53
|
+
- **A modify always beats a delete.** If one side edited a file and the other deleted it, the edited file is kept and the deletion is dropped, with a notice. Offline work is never lost to someone else's delete.
|
|
54
|
+
|
|
55
|
+
## First sync and the bootstrap choice
|
|
56
|
+
|
|
57
|
+
When a project already has files on both the local disk and in the cloud but this machine has no sync base (for example a fresh git clone of a project that was cloud-enabled elsewhere), auto-sync will not run. The tool returns a bootstrap choice. Present it to the user and let them pick:
|
|
58
|
+
|
|
59
|
+
- `keep-cloud` (pull): replace local divergent files with the cloud version. Local divergent files are checkpointed first.
|
|
60
|
+
- `keep-local` (push): make the local tree the new cloud version.
|
|
61
|
+
- `merge`: converge automatically where hashes already match (usually most files); the rest get normal conflict treatment.
|
|
62
|
+
|
|
63
|
+
If the project folder was moved or Finder-copied, sync blocks until you confirm whether to adopt the new path (`adoptPath: true`) or fork it into a separate cloud project. Two folders must never fight over one project.
|
|
64
|
+
|
|
65
|
+
## Honest v1 limits
|
|
66
|
+
|
|
67
|
+
State these plainly when they are relevant; do not paper over them.
|
|
68
|
+
|
|
69
|
+
- **One linear head per project. No branches yet.** There is a single history line. If a user wants an isolated line to experiment with assets, fork to a separate cloud project: delete `summer-cloud.json` and run `summer cloud init` to create a new binding. Because storage is content-addressed and dedupes, the fork is nearly free in storage.
|
|
70
|
+
- **Per-file cap is 4.995 GiB.** A single file larger than that is rejected with a clear error. There is no multipart path in v1.
|
|
71
|
+
- **No resumable uploads.** A dropped connection mid-upload on a multi-GiB file restarts that file from zero. For large pushes, prefer a stable connection.
|
|
72
|
+
- **No locking or presence.** Two people editing the same binary at once produces a conflict (recoverable, never lost), not a lock.
|
|
73
|
+
- **Git branch switches read as edits.** Switching git branches on a cloud-synced project looks like local edits to Cloud and will push as such. Guidance: sync before and after branch operations, or keep cloud projects on one branch.
|
|
74
|
+
- **Storage rides the plan tiers.** Free, Pro, Pro Plus, and Ultra each have a storage cap. A push past the cap returns a quota error pointing at the upgrade path; surface that to the user rather than retrying.
|
|
75
|
+
|
|
76
|
+
## Hard rule: drive everything through the CLI or MCP
|
|
77
|
+
|
|
78
|
+
Never hand-edit anything under `.summer/local/cloud/` (base, journal, hash cache, staging, checkpoints, conflict sets). That state is the integrity model; editing it by hand can corrupt the 3-way base and cause data loss on the next sync. The only file you ever touch directly is the tiny git-tracked `summer-cloud.json` binding, and even then only to remove `pinnedVersion` for a read-only checkout or to delete it when intentionally forking. Every real operation goes through `summer cloud ...` or the `summer_cloud_*` tools.
|
|
79
|
+
|
|
80
|
+
## Typical flows
|
|
81
|
+
|
|
82
|
+
- **Move to a second machine.** On machine A: `summer cloud init`, then `summer cloud push`. Commit `summer-cloud.json` so it travels with git, or share the project id. On machine B: get the project (git clone or copy), then `summer cloud pull`. If both sides already had files, take the bootstrap choice.
|
|
83
|
+
- **Back up big assets before a bulk import.** `summer cloud status`, then `summer cloud push`. The resulting version is the restore point.
|
|
84
|
+
- **Recover from a bad change.** `summer cloud checkpoints` (or `summer cloud status` for cloud versions), then `summer cloud restore --checkpoint <stamp>` for local-only work or `--version N` for a pushed version. Remove any extra files the restore lists.
|
|
85
|
+
|
|
86
|
+
## Fallback
|
|
87
|
+
|
|
88
|
+
If the MCP tools are unavailable, run the `summer cloud ...` CLI commands directly. The CLI and MCP tools are one to one and share the same engine. If neither is available, the project is not set up; point the user at `summer login` (Cloud auth comes from the login token) and `summer cloud init`.
|
|
89
|
+
|
|
90
|
+
## Collaborative protocol
|
|
91
|
+
|
|
92
|
+
Push and pull move bytes and can replace files. Always run `summer cloud status` and say what will move before you push or pull: "Push will upload 4 files and delete 0. OK to push?" For any gated deletion, state the exact count and get explicit confirmation before setting `confirmDeletes`. See `../../references/collaborative-protocol.md`.
|