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.
Files changed (67) hide show
  1. package/README.md +101 -0
  2. package/bin/godot-daedalus-backend.js +4 -0
  3. package/bin/godot-daedalus-mcp.js +4 -0
  4. package/bin/godot-daedalus-terminal-mcp.js +4 -0
  5. package/bin/run-tsx-entry.js +26 -0
  6. package/package.json +54 -0
  7. package/scripts/deepseek-tokenizer-server.py +54 -0
  8. package/src/app-paths.ts +36 -0
  9. package/src/main.ts +21 -0
  10. package/src/mcp/content-length-protocol.ts +68 -0
  11. package/src/mcp/custom-mcp-config-store.ts +397 -0
  12. package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
  13. package/src/mcp/godot-editor-bridge.ts +307 -0
  14. package/src/mcp/godot-mcp-server.ts +3484 -0
  15. package/src/mcp/godot-paths.ts +151 -0
  16. package/src/mcp/godot-project-settings.ts +233 -0
  17. package/src/mcp/godot-tool-registration.ts +46 -0
  18. package/src/mcp/mcp-config.ts +48 -0
  19. package/src/mcp/mcp-host.ts +393 -0
  20. package/src/mcp/mcp-session.ts +81 -0
  21. package/src/mcp/terminal-mcp-server.ts +576 -0
  22. package/src/mcp/tscn-tools.ts +302 -0
  23. package/src/mcp/types.ts +12 -0
  24. package/src/ping-client.ts +24 -0
  25. package/src/prompts/registry.ts +97 -0
  26. package/src/prompts/templates/backend-helper.md +25 -0
  27. package/src/prompts/templates/gdscript-reviewer.md +19 -0
  28. package/src/prompts/templates/godot-assistant.md +225 -0
  29. package/src/prompts/templates/scene-architect.md +15 -0
  30. package/src/prompts/templates/session-compressor.md +33 -0
  31. package/src/protocol/schema.ts +486 -0
  32. package/src/protocol/types.ts +77 -0
  33. package/src/providers/deepseek-agent.ts +1014 -0
  34. package/src/providers/deepseek-client.ts +114 -0
  35. package/src/providers/deepseek-dsml-tools.ts +90 -0
  36. package/src/providers/deepseek-loose-tools.ts +450 -0
  37. package/src/providers/provider-config-store.ts +164 -0
  38. package/src/server/client-session.ts +93 -0
  39. package/src/server/request-dispatcher.ts +74 -0
  40. package/src/server/response-helpers.ts +33 -0
  41. package/src/server/send-json.ts +8 -0
  42. package/src/server/websocket-server.ts +3997 -0
  43. package/src/session/session-compressor.ts +68 -0
  44. package/src/session/session-store.ts +669 -0
  45. package/src/skills/registry.ts +180 -0
  46. package/src/skills/templates/backend-helper.md +12 -0
  47. package/src/skills/templates/file-creator.md +14 -0
  48. package/src/skills/templates/gdscript-review.md +12 -0
  49. package/src/skills/templates/godot-project-init.md +29 -0
  50. package/src/skills/templates/scene-builder.md +12 -0
  51. package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
  52. package/src/tokens/model-profiles.ts +38 -0
  53. package/src/tokens/token-counter-factory.ts +52 -0
  54. package/src/tokens/token-counter.ts +22 -0
  55. package/src/tools/approval-gateway.ts +111 -0
  56. package/src/tools/llm-tools.ts +1415 -0
  57. package/src/tools/tool-dispatcher.ts +147 -0
  58. package/src/tools/tool-event-describer.ts +387 -0
  59. package/src/tools/tool-idempotency.ts +373 -0
  60. package/src/tools/tool-policy-table.ts +61 -0
  61. package/src/tools/tool-policy.ts +73 -0
  62. package/src/workflow/llm-planner.ts +407 -0
  63. package/src/workflow/planner.ts +201 -0
  64. package/src/workflow/runner.ts +141 -0
  65. package/src/workflow/types.ts +69 -0
  66. package/src/workspace/registry.ts +104 -0
  67. package/src/workspace/types.ts +7 -0
@@ -0,0 +1,397 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import keytar from "keytar";
5
+ import { getMcpServersConfigPath } from "../app-paths.js";
6
+ import type { WorkspaceConfig } from "../workspace/types.js";
7
+ import type { McpServerConfig } from "./types.js";
8
+
9
+ const KEYTAR_SERVICE: string = "Godot Daedalus";
10
+ const MCP_SECRET_PREFIX: string = "mcp";
11
+ const MAX_CUSTOM_MCP_SERVERS: number = 24;
12
+ const MAX_ARGUMENTS: number = 64;
13
+ const MAX_SECRET_NAMES: number = 64;
14
+
15
+ export type CustomMcpTransport = "stdio" | "http";
16
+
17
+ export type CustomMcpServerInput = {
18
+ name: string;
19
+ description?: string | undefined;
20
+ transport: CustomMcpTransport;
21
+ enabled?: boolean | undefined;
22
+ command?: string | undefined;
23
+ args?: string[] | undefined;
24
+ env?: Record<string, string> | undefined;
25
+ url?: string | undefined;
26
+ headers?: Record<string, string> | undefined;
27
+ };
28
+
29
+ export type StoredCustomMcpServerConfig = {
30
+ id: string;
31
+ name: string;
32
+ description: string;
33
+ transport: CustomMcpTransport;
34
+ enabled: boolean;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ command?: string | undefined;
38
+ args?: string[] | undefined;
39
+ envNames?: string[] | undefined;
40
+ url?: string | undefined;
41
+ headerNames?: string[] | undefined;
42
+ };
43
+
44
+ export type CustomMcpServerSummary = {
45
+ id: string;
46
+ name: string;
47
+ description: string;
48
+ transport: CustomMcpTransport;
49
+ enabled: boolean;
50
+ createdAt: string;
51
+ updatedAt: string;
52
+ command: string | null;
53
+ args: string[];
54
+ envNames: string[];
55
+ envMasked: Record<string, string>;
56
+ url: string | null;
57
+ headerNames: string[];
58
+ headerMasked: Record<string, string>;
59
+ };
60
+
61
+ function normalizeText(value: string | undefined, maxLength: number): string {
62
+ const trimmed: string = value?.trim() ?? "";
63
+ return trimmed.slice(0, maxLength);
64
+ }
65
+
66
+ function slugify(value: string): string {
67
+ const slug: string = value
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9]+/g, "-")
70
+ .replace(/^-+|-+$/g, "")
71
+ .slice(0, 32);
72
+ return slug.length > 0 ? slug : "server";
73
+ }
74
+
75
+ function createServerId(name: string): string {
76
+ const hash: string = createHash("sha1")
77
+ .update(`${name}\n${randomUUID()}`)
78
+ .digest("hex")
79
+ .slice(0, 8);
80
+ return `custom-${slugify(name)}-${hash}`;
81
+ }
82
+
83
+ function normalizeArgs(args: string[] | undefined): string[] {
84
+ if (args === undefined) {
85
+ return [];
86
+ }
87
+
88
+ return args
89
+ .map((value: string): string => value.trim())
90
+ .filter((value: string): boolean => value.length > 0)
91
+ .slice(0, MAX_ARGUMENTS);
92
+ }
93
+
94
+ function isWindowsCmdCommand(command: string): boolean {
95
+ const normalizedCommand: string = command.replace(/\\/g, "/").toLowerCase();
96
+ const fileName: string = normalizedCommand.split("/").pop() ?? normalizedCommand;
97
+ return fileName === "cmd" || fileName === "cmd.exe";
98
+ }
99
+
100
+ function normalizeStdioArgsForCommand(command: string | undefined, args: string[]): string[] {
101
+ if (command === undefined || process.platform !== "win32" || !isWindowsCmdCommand(command) || args.length === 0) {
102
+ return args;
103
+ }
104
+
105
+ const firstArg: string = args[0]?.toLowerCase() ?? "";
106
+ if (firstArg === "/c" || firstArg === "/k") {
107
+ return args;
108
+ }
109
+
110
+ return ["/c", ...args];
111
+ }
112
+
113
+ function normalizeSecretRecord(value: Record<string, string> | undefined): Record<string, string> {
114
+ if (value === undefined) {
115
+ return {};
116
+ }
117
+
118
+ const result: Record<string, string> = {};
119
+ for (const [rawName, rawSecretValue] of Object.entries(value).slice(0, MAX_SECRET_NAMES)) {
120
+ const name: string = rawName.trim();
121
+ if (name.length === 0) {
122
+ continue;
123
+ }
124
+
125
+ result[name] = rawSecretValue;
126
+ }
127
+ return result;
128
+ }
129
+
130
+ function secretAccount(serverId: string, kind: "env" | "header", name: string): string {
131
+ return `${MCP_SECRET_PREFIX}:${serverId}:${kind}:${name}`;
132
+ }
133
+
134
+ function maskSecret(value: string | null): string {
135
+ if (value === null || value.length === 0) {
136
+ return "********";
137
+ }
138
+
139
+ if (value.length <= 8) {
140
+ return "********";
141
+ }
142
+
143
+ return `${value.slice(0, 2)}...${value.slice(-4)}`;
144
+ }
145
+
146
+ function isStoredCustomMcpServerConfig(value: unknown): value is StoredCustomMcpServerConfig {
147
+ if (value === null || typeof value !== "object") {
148
+ return false;
149
+ }
150
+
151
+ const record: Partial<StoredCustomMcpServerConfig> = value as Partial<StoredCustomMcpServerConfig>;
152
+ return typeof record.id === "string"
153
+ && record.id.startsWith("custom-")
154
+ && typeof record.name === "string"
155
+ && typeof record.description === "string"
156
+ && (record.transport === "stdio" || record.transport === "http")
157
+ && typeof record.enabled === "boolean"
158
+ && typeof record.createdAt === "string"
159
+ && typeof record.updatedAt === "string";
160
+ }
161
+
162
+ async function readStoredConfigs(): Promise<StoredCustomMcpServerConfig[]> {
163
+ try {
164
+ const raw: string = await readFile(getMcpServersConfigPath(), "utf8");
165
+ const parsed: unknown = JSON.parse(raw);
166
+ if (!Array.isArray(parsed)) {
167
+ return [];
168
+ }
169
+
170
+ return parsed.filter(isStoredCustomMcpServerConfig);
171
+ } catch {
172
+ return [];
173
+ }
174
+ }
175
+
176
+ async function writeStoredConfigs(configs: StoredCustomMcpServerConfig[]): Promise<void> {
177
+ const filePath: string = getMcpServersConfigPath();
178
+ await mkdir(dirname(filePath), { recursive: true });
179
+ await writeFile(filePath, JSON.stringify(configs, null, 2), "utf8");
180
+ }
181
+
182
+ async function saveSecrets(serverId: string, kind: "env" | "header", values: Record<string, string>): Promise<string[]> {
183
+ const names: string[] = [];
184
+ for (const [name, secretValue] of Object.entries(values)) {
185
+ await keytar.setPassword(KEYTAR_SERVICE, secretAccount(serverId, kind, name), secretValue);
186
+ names.push(name);
187
+ }
188
+ return names.sort();
189
+ }
190
+
191
+ async function loadSecrets(serverId: string, kind: "env" | "header", names: readonly string[] | undefined): Promise<Record<string, string>> {
192
+ const values: Record<string, string> = {};
193
+ for (const name of names ?? []) {
194
+ const value: string | null = await keytar.getPassword(KEYTAR_SERVICE, secretAccount(serverId, kind, name));
195
+ if (value !== null) {
196
+ values[name] = value;
197
+ }
198
+ }
199
+ return values;
200
+ }
201
+
202
+ async function deleteSecrets(serverId: string, kind: "env" | "header", names: readonly string[] | undefined): Promise<void> {
203
+ for (const name of names ?? []) {
204
+ await keytar.deletePassword(KEYTAR_SERVICE, secretAccount(serverId, kind, name));
205
+ }
206
+ }
207
+
208
+ async function createMaskedSecrets(serverId: string, kind: "env" | "header", names: readonly string[] | undefined): Promise<Record<string, string>> {
209
+ const result: Record<string, string> = {};
210
+ for (const name of names ?? []) {
211
+ const value: string | null = await keytar.getPassword(KEYTAR_SERVICE, secretAccount(serverId, kind, name));
212
+ result[name] = maskSecret(value);
213
+ }
214
+ return result;
215
+ }
216
+
217
+ function createStoredConfig(input: CustomMcpServerInput): StoredCustomMcpServerConfig {
218
+ const name: string = normalizeText(input.name, 80);
219
+ if (name.length === 0) {
220
+ throw new Error("MCP server name is required");
221
+ }
222
+
223
+ const now: string = new Date().toISOString();
224
+ const config: StoredCustomMcpServerConfig = {
225
+ id: createServerId(name),
226
+ name,
227
+ description: normalizeText(input.description, 300),
228
+ transport: input.transport,
229
+ enabled: input.enabled ?? true,
230
+ createdAt: now,
231
+ updatedAt: now
232
+ };
233
+
234
+ if (input.transport === "stdio") {
235
+ const command: string = normalizeText(input.command, 300);
236
+ if (command.length === 0) {
237
+ throw new Error("STDIO MCP server command is required");
238
+ }
239
+
240
+ config.command = command;
241
+ const args: string[] = normalizeStdioArgsForCommand(command, normalizeArgs(input.args));
242
+ if (args.length > 0) {
243
+ config.args = args;
244
+ }
245
+ return config;
246
+ }
247
+
248
+ const urlText: string = normalizeText(input.url, 1000);
249
+ if (urlText.length === 0) {
250
+ throw new Error("HTTP MCP server URL is required");
251
+ }
252
+
253
+ try {
254
+ const url: URL = new URL(urlText);
255
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
256
+ throw new Error("URL must use http or https");
257
+ }
258
+ } catch (error: unknown) {
259
+ throw new Error(error instanceof Error ? error.message : "Invalid HTTP MCP server URL");
260
+ }
261
+
262
+ config.url = urlText;
263
+ return config;
264
+ }
265
+
266
+ export async function addCustomMcpServerConfig(input: CustomMcpServerInput): Promise<CustomMcpServerSummary> {
267
+ const configs: StoredCustomMcpServerConfig[] = await readStoredConfigs();
268
+ if (configs.length >= MAX_CUSTOM_MCP_SERVERS) {
269
+ throw new Error(`Custom MCP server limit reached: ${MAX_CUSTOM_MCP_SERVERS}`);
270
+ }
271
+
272
+ const config: StoredCustomMcpServerConfig = createStoredConfig(input);
273
+ if (config.transport === "stdio") {
274
+ const env: Record<string, string> = normalizeSecretRecord(input.env);
275
+ const envNames: string[] = await saveSecrets(config.id, "env", env);
276
+ if (envNames.length > 0) {
277
+ config.envNames = envNames;
278
+ }
279
+ } else {
280
+ const headers: Record<string, string> = normalizeSecretRecord(input.headers);
281
+ const headerNames: string[] = await saveSecrets(config.id, "header", headers);
282
+ if (headerNames.length > 0) {
283
+ config.headerNames = headerNames;
284
+ }
285
+ }
286
+
287
+ configs.push(config);
288
+ await writeStoredConfigs(configs);
289
+ return createCustomMcpServerSummary(config);
290
+ }
291
+
292
+ export async function removeCustomMcpServerConfig(serverId: string): Promise<boolean> {
293
+ const configs: StoredCustomMcpServerConfig[] = await readStoredConfigs();
294
+ const index: number = configs.findIndex((config: StoredCustomMcpServerConfig): boolean => config.id === serverId);
295
+ if (index < 0) {
296
+ return false;
297
+ }
298
+
299
+ const [removed] = configs.splice(index, 1);
300
+ if (removed !== undefined) {
301
+ await deleteSecrets(removed.id, "env", removed.envNames);
302
+ await deleteSecrets(removed.id, "header", removed.headerNames);
303
+ }
304
+ await writeStoredConfigs(configs);
305
+ return true;
306
+ }
307
+
308
+ export async function setCustomMcpServerEnabled(serverId: string, enabled: boolean): Promise<boolean> {
309
+ const configs: StoredCustomMcpServerConfig[] = await readStoredConfigs();
310
+ const config: StoredCustomMcpServerConfig | undefined = configs.find((item: StoredCustomMcpServerConfig): boolean => item.id === serverId);
311
+ if (config === undefined) {
312
+ return false;
313
+ }
314
+
315
+ config.enabled = enabled;
316
+ config.updatedAt = new Date().toISOString();
317
+ await writeStoredConfigs(configs);
318
+ return true;
319
+ }
320
+
321
+ export async function listStoredCustomMcpServerConfigs(): Promise<StoredCustomMcpServerConfig[]> {
322
+ return readStoredConfigs();
323
+ }
324
+
325
+ export async function listCustomMcpServerSummaries(): Promise<CustomMcpServerSummary[]> {
326
+ const configs: StoredCustomMcpServerConfig[] = await readStoredConfigs();
327
+ const summaries: CustomMcpServerSummary[] = [];
328
+ for (const config of configs) {
329
+ summaries.push(await createCustomMcpServerSummary(config));
330
+ }
331
+ return summaries;
332
+ }
333
+
334
+ export async function buildCustomMcpServerConfigs(workspace: WorkspaceConfig): Promise<McpServerConfig[]> {
335
+ const configs: StoredCustomMcpServerConfig[] = await readStoredConfigs();
336
+ const result: McpServerConfig[] = [];
337
+
338
+ for (const config of configs) {
339
+ if (!config.enabled) {
340
+ continue;
341
+ }
342
+
343
+ if (config.transport === "stdio") {
344
+ const env: Record<string, string> = {
345
+ BACKEND_DIR: process.cwd(),
346
+ GODOT_PROJECT_PATH: workspace.rootPath
347
+ };
348
+ if (workspace.godotExecutablePath !== undefined) {
349
+ env.GODOT_EXECUTABLE_PATH = workspace.godotExecutablePath;
350
+ }
351
+
352
+ Object.assign(env, await loadSecrets(config.id, "env", config.envNames));
353
+ result.push({
354
+ id: config.id,
355
+ name: config.name,
356
+ description: config.description,
357
+ transport: "stdio",
358
+ command: config.command,
359
+ args: normalizeStdioArgsForCommand(config.command, config.args ?? []),
360
+ env,
361
+ custom: true
362
+ });
363
+ continue;
364
+ }
365
+
366
+ result.push({
367
+ id: config.id,
368
+ name: config.name,
369
+ description: config.description,
370
+ transport: "http",
371
+ url: config.url,
372
+ headers: await loadSecrets(config.id, "header", config.headerNames),
373
+ custom: true
374
+ });
375
+ }
376
+
377
+ return result;
378
+ }
379
+
380
+ async function createCustomMcpServerSummary(config: StoredCustomMcpServerConfig): Promise<CustomMcpServerSummary> {
381
+ return {
382
+ id: config.id,
383
+ name: config.name,
384
+ description: config.description,
385
+ transport: config.transport,
386
+ enabled: config.enabled,
387
+ createdAt: config.createdAt,
388
+ updatedAt: config.updatedAt,
389
+ command: config.command ?? null,
390
+ args: normalizeStdioArgsForCommand(config.command, config.args ?? []),
391
+ envNames: config.envNames ?? [],
392
+ envMasked: await createMaskedSecrets(config.id, "env", config.envNames),
393
+ url: config.url ?? null,
394
+ headerNames: config.headerNames ?? [],
395
+ headerMasked: await createMaskedSecrets(config.id, "header", config.headerNames)
396
+ };
397
+ }