godot-daedalus_backend 1.0.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/README.md +101 -0
- package/bin/godot-daedalus-backend.js +4 -0
- package/bin/godot-daedalus-mcp.js +4 -0
- package/bin/godot-daedalus-terminal-mcp.js +4 -0
- package/bin/run-tsx-entry.js +26 -0
- package/package.json +54 -0
- package/scripts/deepseek-tokenizer-server.py +54 -0
- package/src/app-paths.ts +36 -0
- package/src/main.ts +21 -0
- package/src/mcp/content-length-protocol.ts +68 -0
- package/src/mcp/custom-mcp-config-store.ts +397 -0
- package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
- package/src/mcp/godot-editor-bridge.ts +307 -0
- package/src/mcp/godot-mcp-server.ts +3484 -0
- package/src/mcp/godot-paths.ts +151 -0
- package/src/mcp/godot-project-settings.ts +233 -0
- package/src/mcp/godot-tool-registration.ts +46 -0
- package/src/mcp/mcp-config.ts +48 -0
- package/src/mcp/mcp-host.ts +393 -0
- package/src/mcp/mcp-session.ts +81 -0
- package/src/mcp/terminal-mcp-server.ts +576 -0
- package/src/mcp/tscn-tools.ts +302 -0
- package/src/mcp/types.ts +12 -0
- package/src/ping-client.ts +24 -0
- package/src/prompts/registry.ts +97 -0
- package/src/prompts/templates/backend-helper.md +25 -0
- package/src/prompts/templates/gdscript-reviewer.md +19 -0
- package/src/prompts/templates/godot-assistant.md +225 -0
- package/src/prompts/templates/scene-architect.md +15 -0
- package/src/prompts/templates/session-compressor.md +33 -0
- package/src/protocol/schema.ts +486 -0
- package/src/protocol/types.ts +77 -0
- package/src/providers/deepseek-agent.ts +1014 -0
- package/src/providers/deepseek-client.ts +114 -0
- package/src/providers/deepseek-dsml-tools.ts +90 -0
- package/src/providers/deepseek-loose-tools.ts +450 -0
- package/src/providers/provider-config-store.ts +164 -0
- package/src/server/client-session.ts +93 -0
- package/src/server/request-dispatcher.ts +74 -0
- package/src/server/response-helpers.ts +33 -0
- package/src/server/send-json.ts +8 -0
- package/src/server/websocket-server.ts +3997 -0
- package/src/session/session-compressor.ts +68 -0
- package/src/session/session-store.ts +669 -0
- package/src/skills/registry.ts +180 -0
- package/src/skills/templates/backend-helper.md +12 -0
- package/src/skills/templates/file-creator.md +14 -0
- package/src/skills/templates/gdscript-review.md +12 -0
- package/src/skills/templates/godot-project-init.md +29 -0
- package/src/skills/templates/scene-builder.md +12 -0
- package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
- package/src/tokens/model-profiles.ts +38 -0
- package/src/tokens/token-counter-factory.ts +52 -0
- package/src/tokens/token-counter.ts +22 -0
- package/src/tools/approval-gateway.ts +111 -0
- package/src/tools/llm-tools.ts +1415 -0
- package/src/tools/tool-dispatcher.ts +147 -0
- package/src/tools/tool-event-describer.ts +387 -0
- package/src/tools/tool-idempotency.ts +373 -0
- package/src/tools/tool-policy-table.ts +61 -0
- package/src/tools/tool-policy.ts +73 -0
- package/src/workflow/llm-planner.ts +407 -0
- package/src/workflow/planner.ts +201 -0
- package/src/workflow/runner.ts +141 -0
- package/src/workflow/types.ts +69 -0
- package/src/workspace/registry.ts +104 -0
- package/src/workspace/types.ts +7 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
|
|
3
|
+
export const GODOT_EDITOR_SERVER_ID: string = "godot_editor";
|
|
4
|
+
|
|
5
|
+
const EDITOR_TOOL_TIMEOUT_MS: number = 30_000;
|
|
6
|
+
const EDITOR_CONTEXT_STALE_MS: number = 15_000;
|
|
7
|
+
|
|
8
|
+
type JsonObject = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
type ToolTextResult = {
|
|
11
|
+
content: Array<{
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PendingEditorToolCall = {
|
|
18
|
+
resolve: (result: unknown) => void;
|
|
19
|
+
reject: (error: Error) => void;
|
|
20
|
+
timeout: NodeJS.Timeout;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function jsonTextResult(value: unknown): ToolTextResult {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: JSON.stringify(value, null, 2)
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createEditorUnavailableResult(): ToolTextResult {
|
|
35
|
+
return jsonTextResult({
|
|
36
|
+
ok: false,
|
|
37
|
+
error: {
|
|
38
|
+
code: "editor_unavailable",
|
|
39
|
+
message: "Godot editor client is not connected or has not reported live context yet."
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSocketOpen(socket: WebSocket | undefined): socket is WebSocket {
|
|
45
|
+
return socket !== undefined && socket.readyState === WebSocket.OPEN;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class GodotEditorBridge {
|
|
49
|
+
private socket?: WebSocket | undefined;
|
|
50
|
+
private context: JsonObject = {};
|
|
51
|
+
private updatedAtMs: number = 0;
|
|
52
|
+
private pendingToolCalls: Map<string, PendingEditorToolCall> = new Map();
|
|
53
|
+
|
|
54
|
+
attachSocket(socket: WebSocket): void {
|
|
55
|
+
this.socket = socket;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
detachSocket(socket: WebSocket): void {
|
|
59
|
+
if (this.socket !== socket) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.socket = undefined;
|
|
64
|
+
this.context = {};
|
|
65
|
+
this.updatedAtMs = 0;
|
|
66
|
+
|
|
67
|
+
for (const [callId, pending] of this.pendingToolCalls.entries()) {
|
|
68
|
+
clearTimeout(pending.timeout);
|
|
69
|
+
pending.reject(new Error(`editor_unavailable: editor disconnected before tool result (${callId})`));
|
|
70
|
+
}
|
|
71
|
+
this.pendingToolCalls.clear();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateContext(context: JsonObject): void {
|
|
75
|
+
this.context = {
|
|
76
|
+
...context,
|
|
77
|
+
online: true
|
|
78
|
+
};
|
|
79
|
+
this.updatedAtMs = Date.now();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handleToolResult(callId: string, ok: boolean, result: unknown, error: unknown): boolean {
|
|
83
|
+
const pending: PendingEditorToolCall | undefined = this.pendingToolCalls.get(callId);
|
|
84
|
+
if (pending === undefined) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.pendingToolCalls.delete(callId);
|
|
89
|
+
clearTimeout(pending.timeout);
|
|
90
|
+
|
|
91
|
+
if (ok) {
|
|
92
|
+
pending.resolve(result ?? { ok: true });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const message: string = typeof error === "string" && error.length > 0
|
|
97
|
+
? error
|
|
98
|
+
: "Godot editor tool failed";
|
|
99
|
+
pending.reject(new Error(message));
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isOnline(): boolean {
|
|
104
|
+
return isSocketOpen(this.socket) && this.updatedAtMs > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async refreshFilesystem(changedPaths: string[]): Promise<unknown | null> {
|
|
108
|
+
if (!this.isOnline()) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return await this.requestEditorTool("refresh_filesystem", {
|
|
113
|
+
changedPaths,
|
|
114
|
+
scanSources: true
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
listTools() {
|
|
119
|
+
return {
|
|
120
|
+
tools: [
|
|
121
|
+
{
|
|
122
|
+
name: "get_context",
|
|
123
|
+
description: "返回 Godot 编辑器在线状态、当前场景、选择节点、脚本选区、文件系统选择和上下文新鲜度。",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {},
|
|
127
|
+
required: []
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "get_selected_nodes",
|
|
132
|
+
description: "读取当前在线 Godot 编辑器中的多个选中节点摘要。",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {},
|
|
136
|
+
required: []
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "inspect_node",
|
|
141
|
+
description: "检查在线 Godot 编辑器当前未保存状态里的指定节点结构。",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
scenePath: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "可选场景路径;为空时使用当前打开场景。"
|
|
148
|
+
},
|
|
149
|
+
nodePath: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "相对当前场景根节点的 NodePath,例如 '.'、'CanvasLayer/Button'。"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
required: ["nodePath"]
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "apply_scene_patch",
|
|
159
|
+
description: "在在线 Godot 编辑器中应用场景 patch,使用 EditorUndoRedoManager 形成一个可撤销动作。",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
title: {
|
|
164
|
+
type: "string",
|
|
165
|
+
description: "UndoRedo 动作标题。"
|
|
166
|
+
},
|
|
167
|
+
scenePath: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "可选场景路径;为空时使用当前打开场景。"
|
|
170
|
+
},
|
|
171
|
+
saveAfter: {
|
|
172
|
+
type: "boolean",
|
|
173
|
+
description: "提交动作后是否保存当前场景,默认 true。"
|
|
174
|
+
},
|
|
175
|
+
operations: {
|
|
176
|
+
type: "array",
|
|
177
|
+
minItems: 1,
|
|
178
|
+
maxItems: 50,
|
|
179
|
+
items: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
type: {
|
|
183
|
+
type: "string",
|
|
184
|
+
enum: ["set_property", "add_node", "rename_node", "attach_script", "connect_signal"]
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
additionalProperties: true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
required: ["operations"]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
listResources() {
|
|
199
|
+
return {
|
|
200
|
+
resources: [
|
|
201
|
+
{
|
|
202
|
+
uri: "godot-editor://context",
|
|
203
|
+
name: "Godot Editor Live Context",
|
|
204
|
+
mimeType: "application/json"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
readResource(uri: string) {
|
|
211
|
+
if (uri !== "godot-editor://context") {
|
|
212
|
+
throw new Error(`Unknown godot_editor resource: ${uri}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
contents: [
|
|
217
|
+
{
|
|
218
|
+
uri,
|
|
219
|
+
mimeType: "application/json",
|
|
220
|
+
text: JSON.stringify(this.createContextSnapshot(), null, 2)
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async callTool(name: string, args: JsonObject): Promise<ToolTextResult> {
|
|
227
|
+
if (name === "get_context") {
|
|
228
|
+
return jsonTextResult(this.createContextSnapshot());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (name === "get_selected_nodes") {
|
|
232
|
+
if (!this.isOnline()) {
|
|
233
|
+
return createEditorUnavailableResult();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return jsonTextResult({
|
|
237
|
+
ok: true,
|
|
238
|
+
selectedNodes: this.context.selectedNodes ?? [],
|
|
239
|
+
context: this.createContextSnapshot()
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (name !== "inspect_node" && name !== "apply_scene_patch") {
|
|
244
|
+
throw new Error(`Unknown godot_editor tool: ${name}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!this.isOnline()) {
|
|
248
|
+
return createEditorUnavailableResult();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result: unknown = await this.requestEditorTool(name, args);
|
|
252
|
+
return jsonTextResult({
|
|
253
|
+
ok: true,
|
|
254
|
+
result
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private createContextSnapshot(): JsonObject {
|
|
259
|
+
const ageMs: number | null = this.updatedAtMs > 0 ? Date.now() - this.updatedAtMs : null;
|
|
260
|
+
const online: boolean = this.isOnline();
|
|
261
|
+
return {
|
|
262
|
+
online,
|
|
263
|
+
stale: ageMs === null || ageMs > EDITOR_CONTEXT_STALE_MS,
|
|
264
|
+
updatedAt: this.updatedAtMs > 0 ? new Date(this.updatedAtMs).toISOString() : null,
|
|
265
|
+
ageMs,
|
|
266
|
+
context: online ? this.context : {},
|
|
267
|
+
error: online ? null : "editor_unavailable"
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private requestEditorTool(toolName: string, args: JsonObject): Promise<unknown> {
|
|
272
|
+
if (!isSocketOpen(this.socket)) {
|
|
273
|
+
return Promise.reject(new Error("editor_unavailable: Godot editor client is not connected"));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const callId: string = `editor-tool-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
277
|
+
return new Promise<unknown>((resolve, reject): void => {
|
|
278
|
+
const timeout: NodeJS.Timeout = setTimeout((): void => {
|
|
279
|
+
this.pendingToolCalls.delete(callId);
|
|
280
|
+
reject(new Error(`editor_tool_timeout: ${toolName}`));
|
|
281
|
+
}, EDITOR_TOOL_TIMEOUT_MS);
|
|
282
|
+
|
|
283
|
+
this.pendingToolCalls.set(callId, {
|
|
284
|
+
resolve,
|
|
285
|
+
reject,
|
|
286
|
+
timeout
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
this.socket?.send(JSON.stringify({
|
|
291
|
+
type: "event",
|
|
292
|
+
id: callId,
|
|
293
|
+
event: "editor.tool.requested",
|
|
294
|
+
data: {
|
|
295
|
+
callId,
|
|
296
|
+
toolName,
|
|
297
|
+
args
|
|
298
|
+
}
|
|
299
|
+
}));
|
|
300
|
+
} catch (error: unknown) {
|
|
301
|
+
this.pendingToolCalls.delete(callId);
|
|
302
|
+
clearTimeout(timeout);
|
|
303
|
+
reject(error instanceof Error ? error : new Error("editor_tool_send_failed"));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|