summer-engine 1.5.0 → 2.2.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/{marketplace.json → .claude-plugin/marketplace.json} +3 -3
- package/.claude-plugin/plugin.json +52 -11
- package/.codex-plugin/plugin.json +69 -8
- package/.cursor-plugin/plugin.json +53 -12
- package/.factory-plugin/plugin.json +20 -0
- package/.opencode/INSTALL.md +75 -0
- package/.opencode/plugins/summer.js +9 -6
- package/AGENTS.md +51 -0
- package/CHANGELOG.md +31 -1
- package/CLAUDE.md +5 -0
- package/GEMINI.md +56 -0
- package/README.md +409 -80
- package/_persona/summer/SKILL.md +113 -0
- package/dist/bin/summer.js +0 -0
- package/dist/commands/assets.d.ts +2 -0
- package/dist/commands/assets.js +189 -0
- package/dist/commands/create.js +55 -17
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +127 -0
- package/dist/commands/install.js +2 -2
- package/dist/commands/list.js +24 -5
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +170 -0
- package/dist/commands/scene.d.ts +2 -0
- package/dist/commands/scene.js +290 -0
- package/dist/lib/banner.js +2 -2
- package/dist/lib/log-filters.d.ts +53 -0
- package/dist/lib/log-filters.js +130 -0
- package/dist/lib/plugin-manifests.test.d.ts +1 -0
- package/dist/lib/plugin-manifests.test.js +134 -0
- package/dist/lib/remote-templates.d.ts +25 -0
- package/dist/lib/remote-templates.js +86 -0
- package/dist/lib/skills-registry.d.ts +283 -3
- package/dist/lib/skills-registry.js +522 -2
- package/dist/lib/telemetry.d.ts +13 -0
- package/dist/lib/telemetry.js +63 -0
- package/dist/lib/with-engine.d.ts +14 -0
- package/dist/lib/with-engine.js +109 -0
- package/dist/mcp/tools/asset-tools.js +12 -13
- package/dist/mcp/tools/debug-tools.js +32 -9
- package/dist/mcp/tools/generate-tools.js +101 -0
- package/dist/mcp/tools/with-engine.js +4 -0
- package/gemini-extension.json +2 -2
- 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 +11 -5
- package/skills/2d-assets/character-portrait/SKILL.md +196 -0
- package/skills/2d-assets/concept-art/SKILL.md +152 -0
- package/skills/2d-assets/pixel-art/SKILL.md +180 -0
- package/skills/2d-assets/skybox-panorama/SKILL.md +207 -0
- package/skills/2d-assets/sprite-sheet/SKILL.md +239 -0
- package/skills/2d-assets/tileable-texture/SKILL.md +205 -0
- package/skills/2d-assets/ui-graphics/SKILL.md +210 -0
- package/skills/3d-assets/character-model/SKILL.md +225 -0
- package/skills/3d-assets/environment-kit/SKILL.md +164 -0
- package/skills/3d-assets/organic-model/SKILL.md +192 -0
- package/skills/3d-assets/prop-model/SKILL.md +133 -0
- package/skills/3d-assets/vehicle-model/SKILL.md +202 -0
- package/skills/README.md +1 -1
- package/skills/_meta/skill-create/SKILL.md +1 -1
- package/skills/_meta/skill-improve/SKILL.md +1 -1
- package/skills/_meta/skill-test/SKILL.md +1 -1
- package/skills/_meta/using-summer/SKILL.md +130 -0
- package/skills/ai-and-npcs/design-npc/SKILL.md +3 -1
- package/skills/animation/animation-tree/SKILL.md +260 -0
- package/skills/animation/facial-and-lipsync/SKILL.md +282 -0
- package/skills/animation/generate-motion/SKILL.md +129 -0
- package/skills/animation/procedural-animation/SKILL.md +234 -0
- package/skills/animation/retarget/SKILL.md +172 -0
- package/skills/asset-pipeline/asset-strategy/SKILL.md +102 -212
- package/skills/audio/adaptive-music/SKILL.md +326 -0
- package/skills/audio/ambient-bed/SKILL.md +278 -0
- package/skills/audio/music-track/SKILL.md +268 -0
- package/skills/audio/sound-effect/SKILL.md +237 -0
- package/skills/audio/voice-line/SKILL.md +253 -0
- package/skills/catalog.yaml +266 -5
- package/skills/character-controllers/fps-controller/SKILL.md +1 -1
- package/skills/debugging/debug/SKILL.md +102 -82
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
- package/skills/scene-and-project/browse-templates/SKILL.md +105 -0
- package/skills/scene-and-project/make-game/SKILL.md +159 -346
- package/skills/scene-and-project/new-project/SKILL.md +106 -0
- package/skills/scene-and-project/scene-composition/SKILL.md +1 -1
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
- package/skills/video/animated-loop/SKILL.md +200 -0
- package/skills/video/cinematic-cutscene/SKILL.md +204 -0
- package/skills/video/trailer-shot/SKILL.md +203 -0
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +28 -0
- package/skills/visual-effects/_building-blocks/noise-3d-fbm.gdshaderinc +45 -0
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +51 -0
- package/skills/visual-effects/{vfx → game-feel}/SKILL.md +1 -1
- package/skills/visual-effects/recipes/dissolve/SKILL.md +349 -0
- package/skills/visual-effects/recipes/fire/SKILL.md +387 -0
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +330 -0
- package/skills/visual-effects/recipes/lightning/SKILL.md +372 -0
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +377 -0
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +326 -0
- package/skills/visual-effects/recipes/smoke/SKILL.md +381 -0
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +340 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { withEngineClient } from "../lib/with-engine.js";
|
|
5
|
+
import { outputResult, outputError, parseValue, readJsonArg, isJsonMode, } from "../lib/format.js";
|
|
6
|
+
async function readMainSceneFromProject(projectPath) {
|
|
7
|
+
if (!projectPath)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const text = await readFile(join(projectPath, "project.godot"), "utf-8");
|
|
11
|
+
const match = text.match(/run\/main_scene="([^"]+)"/);
|
|
12
|
+
return match?.[1] ?? null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const projectCommand = new Command("project").description("Project context and settings (requires running engine)");
|
|
19
|
+
projectCommand
|
|
20
|
+
.command("context")
|
|
21
|
+
.description("Get project context: health, current scene, main scene, project path")
|
|
22
|
+
.action(async (_opts, cmd) => {
|
|
23
|
+
const fmt = isJsonMode(cmd);
|
|
24
|
+
try {
|
|
25
|
+
const result = await withEngineClient(async (client) => {
|
|
26
|
+
const [health, projectState] = await Promise.all([
|
|
27
|
+
client.health(),
|
|
28
|
+
client.getProjectState(),
|
|
29
|
+
]);
|
|
30
|
+
const healthObj = (health ?? {});
|
|
31
|
+
const mainScene = await readMainSceneFromProject(typeof healthObj.project_path === "string"
|
|
32
|
+
? healthObj.project_path
|
|
33
|
+
: undefined);
|
|
34
|
+
return {
|
|
35
|
+
health,
|
|
36
|
+
project: projectState,
|
|
37
|
+
mainScene,
|
|
38
|
+
guidance: mainScene
|
|
39
|
+
? "Use `summer scene open` with mainScene if no scene is open."
|
|
40
|
+
: "Main scene not found in project.godot. Open a known scene path explicitly.",
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
outputResult(result, fmt);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
projectCommand
|
|
50
|
+
.command("open-main")
|
|
51
|
+
.description("Open the project's main scene from project.godot")
|
|
52
|
+
.action(async (_opts, cmd) => {
|
|
53
|
+
const fmt = isJsonMode(cmd);
|
|
54
|
+
try {
|
|
55
|
+
const result = await withEngineClient(async (client) => {
|
|
56
|
+
const health = (await client.health());
|
|
57
|
+
const projectPath = typeof health.project_path === "string"
|
|
58
|
+
? health.project_path
|
|
59
|
+
: undefined;
|
|
60
|
+
const mainScene = await readMainSceneFromProject(projectPath);
|
|
61
|
+
if (!mainScene) {
|
|
62
|
+
throw new Error("Could not resolve main_scene from project.godot. Run `summer project context` and open a scene explicitly.");
|
|
63
|
+
}
|
|
64
|
+
return client.executeOps([{ op: "OpenScene", path: mainScene }]);
|
|
65
|
+
});
|
|
66
|
+
outputResult(result, fmt);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
projectCommand
|
|
73
|
+
.command("setting <key> <value>")
|
|
74
|
+
.description('Set a project setting (e.g. summer project setting application/config/name "My Game")')
|
|
75
|
+
.action(async (key, value, _opts, cmd) => {
|
|
76
|
+
const fmt = isJsonMode(cmd);
|
|
77
|
+
try {
|
|
78
|
+
const result = await withEngineClient((c) => c.executeOps([
|
|
79
|
+
{ op: "ProjectSetting", key, value: parseValue(value) },
|
|
80
|
+
]));
|
|
81
|
+
outputResult(result, fmt);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
projectCommand
|
|
88
|
+
.command("input-bind <name> <events>")
|
|
89
|
+
.description('Bind input events to an action (e.g. summer project input-bind jump \'[{"type":"key","key":"Space"}]\')')
|
|
90
|
+
.action(async (name, eventsJson, _opts, cmd) => {
|
|
91
|
+
const fmt = isJsonMode(cmd);
|
|
92
|
+
try {
|
|
93
|
+
const events = JSON.parse(eventsJson);
|
|
94
|
+
const result = await withEngineClient((c) => c.executeOps([
|
|
95
|
+
{ op: "InputMapAddAction", name },
|
|
96
|
+
{ op: "InputMapBind", name, events },
|
|
97
|
+
]));
|
|
98
|
+
outputResult(result, fmt);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
projectCommand
|
|
105
|
+
.command("import <url>")
|
|
106
|
+
.description("Download a file from a URL and import into the project")
|
|
107
|
+
.option("--path <path>", "Target path in project (auto-inferred from URL if omitted)")
|
|
108
|
+
.action(async (url, opts, cmd) => {
|
|
109
|
+
const fmt = isJsonMode(cmd);
|
|
110
|
+
try {
|
|
111
|
+
const op = { op: "ImportFromUrl", url };
|
|
112
|
+
if (opts.path)
|
|
113
|
+
op.path = opts.path;
|
|
114
|
+
const result = await withEngineClient((c) => c.executeOps([op]));
|
|
115
|
+
outputResult(result, fmt);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
projectCommand
|
|
122
|
+
.command("import-batch <json>")
|
|
123
|
+
.description('Import multiple files. Pass JSON array of {url, path} or "-" for stdin')
|
|
124
|
+
.action(async (jsonArg, _opts, cmd) => {
|
|
125
|
+
const fmt = isJsonMode(cmd);
|
|
126
|
+
try {
|
|
127
|
+
const imports = (await readJsonArg(jsonArg));
|
|
128
|
+
const result = await withEngineClient((c) => c.executeOps([{ op: "ImportFromUrlBatch", imports }]));
|
|
129
|
+
outputResult(result, fmt);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
projectCommand
|
|
136
|
+
.command("playbook")
|
|
137
|
+
.description("Show the AI agent operating guide for Summer Engine")
|
|
138
|
+
.action(async (_opts, cmd) => {
|
|
139
|
+
const fmt = isJsonMode(cmd);
|
|
140
|
+
const playbook = {
|
|
141
|
+
startupChecklist: [
|
|
142
|
+
"Decide first: file edit or engine operation.",
|
|
143
|
+
"For normal code/content edits (.gd/.tscn/.tres/.ts), use host file-edit tools directly.",
|
|
144
|
+
"Call `summer project context` first.",
|
|
145
|
+
"If no scene is open, call `summer project open-main`.",
|
|
146
|
+
"Call `summer scene tree` before structural edits.",
|
|
147
|
+
"Call `summer scene save` after edits.",
|
|
148
|
+
],
|
|
149
|
+
safeDefaults: [
|
|
150
|
+
"Never guess scene filenames (main.tscn/Main.tscn).",
|
|
151
|
+
"Do not route ordinary code edits through engine ops when direct file tools are available.",
|
|
152
|
+
"Never remove multiple top-level nodes unless user explicitly requests destructive edits.",
|
|
153
|
+
"Never write .tscn/.scn with file write tools; use scene commands.",
|
|
154
|
+
],
|
|
155
|
+
preferredFlow: [
|
|
156
|
+
"summer project context",
|
|
157
|
+
"summer project open-main (if needed)",
|
|
158
|
+
"summer scene tree",
|
|
159
|
+
"edit via summer scene add / summer scene prop / summer scene rprop",
|
|
160
|
+
"summer scene save",
|
|
161
|
+
"summer debug diagnostics",
|
|
162
|
+
],
|
|
163
|
+
recovery: [
|
|
164
|
+
"If you see 'no scene open': run summer project open-main.",
|
|
165
|
+
"If open fails: re-check mainScene from summer project context.",
|
|
166
|
+
"If save fails: verify scene is open and game is not running.",
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
outputResult(playbook, fmt);
|
|
170
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { withEngineClient } from "../lib/with-engine.js";
|
|
5
|
+
import { outputResult, outputError, parseValue, readJsonArg, isJsonMode, } from "../lib/format.js";
|
|
6
|
+
async function readMainSceneFromProject(projectPath) {
|
|
7
|
+
if (!projectPath)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const text = await readFile(join(projectPath, "project.godot"), "utf-8");
|
|
11
|
+
const match = text.match(/run\/main_scene="([^"]+)"/);
|
|
12
|
+
return match?.[1] ?? null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const sceneCommand = new Command("scene").description("Scene editing commands (requires running engine)");
|
|
19
|
+
// --- Tier 1 ---
|
|
20
|
+
sceneCommand
|
|
21
|
+
.command("tree")
|
|
22
|
+
.description("Get the full scene tree of the currently open scene")
|
|
23
|
+
.action(async (_opts, cmd) => {
|
|
24
|
+
const fmt = isJsonMode(cmd);
|
|
25
|
+
try {
|
|
26
|
+
const result = await withEngineClient((c) => c.getSceneState());
|
|
27
|
+
outputResult(result, fmt);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
sceneCommand
|
|
34
|
+
.command("open <path>")
|
|
35
|
+
.description("Open a scene file in the editor (e.g. res://main.tscn)")
|
|
36
|
+
.action(async (path, _opts, cmd) => {
|
|
37
|
+
const fmt = isJsonMode(cmd);
|
|
38
|
+
try {
|
|
39
|
+
const result = await withEngineClient((c) => c.executeOps([{ op: "OpenScene", path }]));
|
|
40
|
+
outputResult(result, fmt);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
sceneCommand
|
|
47
|
+
.command("add <parent> <type> <name>")
|
|
48
|
+
.description("Add a node to the scene tree (e.g. summer scene add ./World MeshInstance3D Floor)")
|
|
49
|
+
.action(async (parent, type, name, _opts, cmd) => {
|
|
50
|
+
const fmt = isJsonMode(cmd);
|
|
51
|
+
try {
|
|
52
|
+
const result = await withEngineClient((c) => c.executeOps([{ op: "AddNode", parent, type, name }]));
|
|
53
|
+
outputResult(result, fmt);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
sceneCommand
|
|
60
|
+
.command("prop <path> <key> <value>")
|
|
61
|
+
.description('Set a property on a node (e.g. summer scene prop ./Player position "Vector3(0,5,0)")')
|
|
62
|
+
.action(async (path, key, value, _opts, cmd) => {
|
|
63
|
+
const fmt = isJsonMode(cmd);
|
|
64
|
+
try {
|
|
65
|
+
const result = await withEngineClient((c) => c.executeOps([{ op: "SetProp", path, key, value: parseValue(value) }]));
|
|
66
|
+
outputResult(result, fmt);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
sceneCommand
|
|
73
|
+
.command("save [path]")
|
|
74
|
+
.description("Save the current scene (optionally save-as to a new path)")
|
|
75
|
+
.action(async (path, _opts, cmd) => {
|
|
76
|
+
const fmt = isJsonMode(cmd);
|
|
77
|
+
try {
|
|
78
|
+
const op = { op: "SaveScene" };
|
|
79
|
+
if (path)
|
|
80
|
+
op.path = path;
|
|
81
|
+
const result = await withEngineClient((c) => c.executeOps([op]));
|
|
82
|
+
outputResult(result, fmt);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
sceneCommand
|
|
89
|
+
.command("inspect <path>")
|
|
90
|
+
.description("Get all editable properties of a node (e.g. summer scene inspect Player)")
|
|
91
|
+
.action(async (path, _opts, cmd) => {
|
|
92
|
+
const fmt = isJsonMode(cmd);
|
|
93
|
+
try {
|
|
94
|
+
const result = await withEngineClient((c) => c.inspectNode(path));
|
|
95
|
+
outputResult(result, fmt);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
sceneCommand
|
|
102
|
+
.command("batch <json>")
|
|
103
|
+
.description('Execute multiple ops in one undo step. Pass JSON array or "-" for stdin')
|
|
104
|
+
.action(async (jsonArg, _opts, cmd) => {
|
|
105
|
+
const fmt = isJsonMode(cmd);
|
|
106
|
+
try {
|
|
107
|
+
const ops = (await readJsonArg(jsonArg));
|
|
108
|
+
const result = await withEngineClient((c) => c.executeOps(ops, { groupUndo: true }));
|
|
109
|
+
outputResult(result, fmt);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// --- Tier 2 ---
|
|
116
|
+
sceneCommand
|
|
117
|
+
.command("remove <path>")
|
|
118
|
+
.description("Remove a node and all its children from the scene tree")
|
|
119
|
+
.action(async (path, _opts, cmd) => {
|
|
120
|
+
const fmt = isJsonMode(cmd);
|
|
121
|
+
try {
|
|
122
|
+
const result = await withEngineClient((c) => c.executeOps([{ op: "RemoveNode", path }]));
|
|
123
|
+
outputResult(result, fmt);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
sceneCommand
|
|
130
|
+
.command("rprop <nodePath> <resourceProperty> <subProperty> <value>")
|
|
131
|
+
.description("Set a nested resource property (e.g. summer scene rprop ./Player/Col shape size \"Vector3(1,2,1)\")")
|
|
132
|
+
.action(async (nodePath, resourceProperty, subProperty, value, _opts, cmd) => {
|
|
133
|
+
const fmt = isJsonMode(cmd);
|
|
134
|
+
try {
|
|
135
|
+
const result = await withEngineClient((c) => c.executeOps([
|
|
136
|
+
{
|
|
137
|
+
op: "SetResourceProperty",
|
|
138
|
+
nodePath,
|
|
139
|
+
resourceProperty,
|
|
140
|
+
subProperty,
|
|
141
|
+
value: parseValue(value),
|
|
142
|
+
},
|
|
143
|
+
]));
|
|
144
|
+
outputResult(result, fmt);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
sceneCommand
|
|
151
|
+
.command("instantiate <parent> <scene>")
|
|
152
|
+
.description("Add an existing scene/model as a child (e.g. summer scene instantiate ./World res://player.tscn)")
|
|
153
|
+
.option("--name <name>", "Override the instance name")
|
|
154
|
+
.action(async (parent, scene, opts, cmd) => {
|
|
155
|
+
const fmt = isJsonMode(cmd);
|
|
156
|
+
try {
|
|
157
|
+
const op = {
|
|
158
|
+
op: "InstantiateScene",
|
|
159
|
+
parent,
|
|
160
|
+
scene,
|
|
161
|
+
};
|
|
162
|
+
if (opts.name)
|
|
163
|
+
op.name = opts.name;
|
|
164
|
+
const result = await withEngineClient((c) => c.executeOps([op]));
|
|
165
|
+
outputResult(result, fmt);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
sceneCommand
|
|
172
|
+
.command("create <path>")
|
|
173
|
+
.description("Create a new empty scene file")
|
|
174
|
+
.option("--root-name <name>", "Root node name", "Main")
|
|
175
|
+
.option("--yes", "Skip safety confirmation")
|
|
176
|
+
.action(async (path, opts, cmd) => {
|
|
177
|
+
const fmt = isJsonMode(cmd);
|
|
178
|
+
if (!opts.yes) {
|
|
179
|
+
outputError("Scene creation uses a temporary mutation strategy. Pass --yes to confirm.", fmt);
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const result = await withEngineClient(async (client) => {
|
|
183
|
+
const health = (await client.health());
|
|
184
|
+
const currentScene = typeof health.scene === "string" && health.scene.length > 0
|
|
185
|
+
? health.scene
|
|
186
|
+
: null;
|
|
187
|
+
const projectPath = typeof health.project_path === "string"
|
|
188
|
+
? health.project_path
|
|
189
|
+
: undefined;
|
|
190
|
+
const mainScene = await readMainSceneFromProject(projectPath);
|
|
191
|
+
const templateScene = currentScene || mainScene;
|
|
192
|
+
if (!templateScene) {
|
|
193
|
+
throw new Error("No scene open and could not resolve main scene. Run `summer project context` first.");
|
|
194
|
+
}
|
|
195
|
+
await client.executeOps([{ op: "OpenScene", path: templateScene }]);
|
|
196
|
+
const tree = (await client.getSceneState());
|
|
197
|
+
const children = tree.data?.children ?? tree.children ?? [];
|
|
198
|
+
const removeOps = children
|
|
199
|
+
.map((c) => c.path)
|
|
200
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
201
|
+
.map((p) => ({ op: "RemoveNode", path: `./${p}` }));
|
|
202
|
+
if (removeOps.length > 0) {
|
|
203
|
+
await client.executeOps(removeOps, { groupUndo: true });
|
|
204
|
+
}
|
|
205
|
+
await client.executeOps([
|
|
206
|
+
{ op: "SetProp", path: ".", key: "name", value: opts.rootName },
|
|
207
|
+
]);
|
|
208
|
+
await client.executeOps([{ op: "SaveScene", path }]);
|
|
209
|
+
if (currentScene && currentScene !== path) {
|
|
210
|
+
await client.executeOps([{ op: "OpenScene", path: currentScene }]);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
created: path,
|
|
215
|
+
rootName: opts.rootName,
|
|
216
|
+
templateScene,
|
|
217
|
+
restoredScene: currentScene,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
outputResult(result, fmt);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
sceneCommand
|
|
227
|
+
.command("replace <path>")
|
|
228
|
+
.description("Replace a node with a different type or scene")
|
|
229
|
+
.option("--type <type>", "New node type (e.g. RigidBody3D)")
|
|
230
|
+
.option("--scene <scene>", "Scene to replace with (e.g. res://enemies/boss.tscn)")
|
|
231
|
+
.action(async (path, opts, cmd) => {
|
|
232
|
+
const fmt = isJsonMode(cmd);
|
|
233
|
+
try {
|
|
234
|
+
const op = { op: "ReplaceNode", path };
|
|
235
|
+
if (opts.type)
|
|
236
|
+
op.type = opts.type;
|
|
237
|
+
if (opts.scene)
|
|
238
|
+
op.scene = opts.scene;
|
|
239
|
+
const result = await withEngineClient((c) => c.executeOps([op]));
|
|
240
|
+
outputResult(result, fmt);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
sceneCommand
|
|
247
|
+
.command("signal <emitter> <signal> <receiver> <method>")
|
|
248
|
+
.description("Connect a signal between two nodes (e.g. summer scene signal ./HitArea body_entered ./Player _on_hit)")
|
|
249
|
+
.action(async (emitter, signal, receiver, method, _opts, cmd) => {
|
|
250
|
+
const fmt = isJsonMode(cmd);
|
|
251
|
+
try {
|
|
252
|
+
const result = await withEngineClient((c) => c.executeOps([
|
|
253
|
+
{ op: "ConnectSignal", emitter, signal, receiver, method },
|
|
254
|
+
]));
|
|
255
|
+
outputResult(result, fmt);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
sceneCommand
|
|
262
|
+
.command("select <nodePath>")
|
|
263
|
+
.description("Select a node in the editor inspector")
|
|
264
|
+
.option("--scene <scenePath>", "Open this scene first, then select")
|
|
265
|
+
.action(async (nodePath, opts, cmd) => {
|
|
266
|
+
const fmt = isJsonMode(cmd);
|
|
267
|
+
try {
|
|
268
|
+
const op = { op: "SelectNode", nodePath };
|
|
269
|
+
if (opts.scene)
|
|
270
|
+
op.scenePath = opts.scene;
|
|
271
|
+
const result = await withEngineClient((c) => c.executeOps([op]));
|
|
272
|
+
outputResult(result, fmt);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
sceneCommand
|
|
279
|
+
.command("inspect-resource <path>")
|
|
280
|
+
.description("Get all properties of a resource (material, mesh, shape, etc.)")
|
|
281
|
+
.action(async (path, _opts, cmd) => {
|
|
282
|
+
const fmt = isJsonMode(cmd);
|
|
283
|
+
try {
|
|
284
|
+
const result = await withEngineClient((c) => c.inspectResource(path));
|
|
285
|
+
outputResult(result, fmt);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
outputError(err instanceof Error ? err.message : String(err), fmt);
|
|
289
|
+
}
|
|
290
|
+
});
|
package/dist/lib/banner.js
CHANGED
|
@@ -84,10 +84,10 @@ export function getWelcome(version) {
|
|
|
84
84
|
];
|
|
85
85
|
const footerLines = [
|
|
86
86
|
pad("", 69),
|
|
87
|
-
pad(` ${sunC2}summer
|
|
87
|
+
pad(` ${sunC2}summer${reset} · ${dim}local${reset}`, 69),
|
|
88
88
|
pad(` ${dim}Docs: https://summerengine.com/docs/mcp${reset}`, 69)
|
|
89
89
|
];
|
|
90
|
-
const title = ` Summer
|
|
90
|
+
const title = ` Summer v${version} `;
|
|
91
91
|
const titleLen = stripAnsi(title).length;
|
|
92
92
|
const totalDashes = 69 - titleLen;
|
|
93
93
|
const leftDashes = Math.floor(totalDashes / 2);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processors for engine log/diagnostic payloads before they are returned
|
|
3
|
+
* to the AI model. The engine API stays lossless; the AI-facing surface
|
|
4
|
+
* trims, dedupes, and prioritizes.
|
|
5
|
+
*
|
|
6
|
+
* Two passes:
|
|
7
|
+
* - dedupe(): collapse consecutive identical messages into one with a
|
|
8
|
+
* `(×N)` count suffix so a 200-frame `null check` flood reads as one line.
|
|
9
|
+
* - errorsOnly(): drop info/std lines, keep errors and warnings.
|
|
10
|
+
*/
|
|
11
|
+
export type LogEntry = {
|
|
12
|
+
message?: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
severity?: string;
|
|
16
|
+
level?: string;
|
|
17
|
+
timestamp?: string;
|
|
18
|
+
[k: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
export interface FilterOptions {
|
|
21
|
+
/** Drop everything that isn't an error or warning. Default: false. */
|
|
22
|
+
errorsOnly?: boolean;
|
|
23
|
+
/** Drop everything that isn't an error. Default: false. */
|
|
24
|
+
errorsOnlyStrict?: boolean;
|
|
25
|
+
/** Hard cap on returned entries after dedupe. Default: 100. */
|
|
26
|
+
maxEntries?: number;
|
|
27
|
+
/** Skip dedupe — return everything as-is. Default: false. */
|
|
28
|
+
noDedupe?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface FilterSummary {
|
|
31
|
+
totalIn: number;
|
|
32
|
+
totalOut: number;
|
|
33
|
+
duplicatesCollapsed: number;
|
|
34
|
+
droppedByLevel: number;
|
|
35
|
+
truncated: number;
|
|
36
|
+
}
|
|
37
|
+
export interface FilterResult<T extends LogEntry = LogEntry> {
|
|
38
|
+
entries: T[];
|
|
39
|
+
summary: FilterSummary;
|
|
40
|
+
}
|
|
41
|
+
export declare function filterAndDedupe<T extends LogEntry>(entries: T[], opts?: FilterOptions): FilterResult<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Walks a parsed engine response and applies filterAndDedupe to the first
|
|
44
|
+
* recognized log-bearing array. Mutates a clone — the input is not modified.
|
|
45
|
+
*
|
|
46
|
+
* Returns a tuple of [shapedResult, summary] where shapedResult is the same
|
|
47
|
+
* shape as input but with the array replaced and a `_filter` summary
|
|
48
|
+
* appended at the same level so the model can see what was hidden.
|
|
49
|
+
*/
|
|
50
|
+
export declare function shapeEngineLogResponse(raw: unknown, opts?: FilterOptions): {
|
|
51
|
+
result: unknown;
|
|
52
|
+
summary: FilterSummary | null;
|
|
53
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const ERROR_TYPES = new Set(["error", "fatal", "critical"]);
|
|
2
|
+
const WARNING_TYPES = new Set(["warning", "warn"]);
|
|
3
|
+
const NOISE_TYPES = new Set(["info", "debug", "std", "stdout", "trace", "verbose"]);
|
|
4
|
+
function entryKey(e) {
|
|
5
|
+
// Identical message + same type counts as a duplicate.
|
|
6
|
+
// Timestamps differ by frame, so they're excluded from the key.
|
|
7
|
+
const msg = (e.message ?? e.text ?? "").trim();
|
|
8
|
+
const type = (e.type ?? e.severity ?? e.level ?? "").toLowerCase();
|
|
9
|
+
return `${type}::${msg}`;
|
|
10
|
+
}
|
|
11
|
+
function entryLevel(e) {
|
|
12
|
+
const t = (e.type ?? e.severity ?? e.level ?? "").toLowerCase();
|
|
13
|
+
if (ERROR_TYPES.has(t))
|
|
14
|
+
return "error";
|
|
15
|
+
if (WARNING_TYPES.has(t))
|
|
16
|
+
return "warning";
|
|
17
|
+
if (NOISE_TYPES.has(t))
|
|
18
|
+
return "noise";
|
|
19
|
+
return "other";
|
|
20
|
+
}
|
|
21
|
+
export function filterAndDedupe(entries, opts = {}) {
|
|
22
|
+
const { errorsOnly = false, errorsOnlyStrict = false, maxEntries = 100, noDedupe = false } = opts;
|
|
23
|
+
const summary = {
|
|
24
|
+
totalIn: entries.length,
|
|
25
|
+
totalOut: 0,
|
|
26
|
+
duplicatesCollapsed: 0,
|
|
27
|
+
droppedByLevel: 0,
|
|
28
|
+
truncated: 0,
|
|
29
|
+
};
|
|
30
|
+
// Pass 1: level filter
|
|
31
|
+
let kept = entries;
|
|
32
|
+
if (errorsOnlyStrict || errorsOnly) {
|
|
33
|
+
kept = entries.filter((e) => {
|
|
34
|
+
const level = entryLevel(e);
|
|
35
|
+
if (errorsOnlyStrict)
|
|
36
|
+
return level === "error";
|
|
37
|
+
return level === "error" || level === "warning";
|
|
38
|
+
});
|
|
39
|
+
summary.droppedByLevel = entries.length - kept.length;
|
|
40
|
+
}
|
|
41
|
+
// Pass 2: dedupe consecutive identical entries
|
|
42
|
+
let deduped;
|
|
43
|
+
if (noDedupe) {
|
|
44
|
+
deduped = kept;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
deduped = [];
|
|
48
|
+
let lastKey = null;
|
|
49
|
+
let lastIdx = -1;
|
|
50
|
+
let runCount = 0;
|
|
51
|
+
for (const e of kept) {
|
|
52
|
+
const key = entryKey(e);
|
|
53
|
+
if (key === lastKey && lastIdx >= 0) {
|
|
54
|
+
runCount += 1;
|
|
55
|
+
const tagged = { ...deduped[lastIdx] };
|
|
56
|
+
tagged.duplicate_count = runCount;
|
|
57
|
+
const baseMsg = (tagged.message ?? tagged.text ?? "");
|
|
58
|
+
const stripped = baseMsg.replace(/\s*\(×\d+\)\s*$/, "");
|
|
59
|
+
if (tagged.message !== undefined)
|
|
60
|
+
tagged.message = `${stripped} (×${runCount})`;
|
|
61
|
+
else if (tagged.text !== undefined)
|
|
62
|
+
tagged.text = `${stripped} (×${runCount})`;
|
|
63
|
+
deduped[lastIdx] = tagged;
|
|
64
|
+
summary.duplicatesCollapsed += 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
deduped.push(e);
|
|
68
|
+
lastKey = key;
|
|
69
|
+
lastIdx = deduped.length - 1;
|
|
70
|
+
runCount = 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Pass 3: truncate
|
|
75
|
+
let final = deduped;
|
|
76
|
+
if (deduped.length > maxEntries) {
|
|
77
|
+
final = deduped.slice(0, maxEntries);
|
|
78
|
+
summary.truncated = deduped.length - maxEntries;
|
|
79
|
+
}
|
|
80
|
+
summary.totalOut = final.length;
|
|
81
|
+
return { entries: final, summary };
|
|
82
|
+
}
|
|
83
|
+
const ENTRY_KEYS = ["messages", "errors", "entries", "items", "lines", "output"];
|
|
84
|
+
/**
|
|
85
|
+
* Walks a parsed engine response and applies filterAndDedupe to the first
|
|
86
|
+
* recognized log-bearing array. Mutates a clone — the input is not modified.
|
|
87
|
+
*
|
|
88
|
+
* Returns a tuple of [shapedResult, summary] where shapedResult is the same
|
|
89
|
+
* shape as input but with the array replaced and a `_filter` summary
|
|
90
|
+
* appended at the same level so the model can see what was hidden.
|
|
91
|
+
*/
|
|
92
|
+
export function shapeEngineLogResponse(raw, opts = {}) {
|
|
93
|
+
if (!raw || typeof raw !== "object")
|
|
94
|
+
return { result: raw, summary: null };
|
|
95
|
+
const cloned = JSON.parse(JSON.stringify(raw));
|
|
96
|
+
const summary = applyToFirstArray(cloned, opts);
|
|
97
|
+
return { result: cloned, summary };
|
|
98
|
+
}
|
|
99
|
+
function applyToFirstArray(node, opts) {
|
|
100
|
+
if (!node || typeof node !== "object")
|
|
101
|
+
return null;
|
|
102
|
+
if (Array.isArray(node)) {
|
|
103
|
+
for (const item of node) {
|
|
104
|
+
if (item && typeof item === "object") {
|
|
105
|
+
const inner = applyToFirstArray(item, opts);
|
|
106
|
+
if (inner)
|
|
107
|
+
return inner;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const obj = node;
|
|
113
|
+
for (const key of ENTRY_KEYS) {
|
|
114
|
+
const arr = obj[key];
|
|
115
|
+
if (Array.isArray(arr) && arr.length > 0 && typeof arr[0] === "object") {
|
|
116
|
+
const { entries, summary } = filterAndDedupe(arr, opts);
|
|
117
|
+
obj[key] = entries;
|
|
118
|
+
obj["_filter"] = summary;
|
|
119
|
+
return summary;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const v of Object.values(obj)) {
|
|
123
|
+
if (v && typeof v === "object") {
|
|
124
|
+
const inner = applyToFirstArray(v, opts);
|
|
125
|
+
if (inner)
|
|
126
|
+
return inner;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|