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
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Godot Daedalus Backend
|
|
2
|
+
|
|
3
|
+
Godot Daedalus Backend is the TypeScript runtime service for the Godot Daedalus editor plugin. It provides the WebSocket/RPC backend, DeepSeek chat integration, approval-gated Godot project tools, session persistence, MCP host support, and Godot MCP servers.
|
|
4
|
+
|
|
5
|
+
This npm package is a source-runtime package. It publishes the TypeScript source and small JavaScript bin launchers, then runs the source through `tsx` at runtime. It does not publish compiled `dist/` output.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 20.19 or newer.
|
|
10
|
+
- npm.
|
|
11
|
+
- Godot 4.x for Godot validation and editor workflows.
|
|
12
|
+
- Optional Python 3 for the DeepSeek tokenizer helper.
|
|
13
|
+
|
|
14
|
+
The Python tokenizer bridge is included as `scripts/deepseek-tokenizer-server.py`. The tokenizer model files are intentionally not included in the npm package because they can be large. If you want exact tokenizer counting, install the Python dependency and point the backend at your tokenizer directory:
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
pip install tokenizers
|
|
18
|
+
$env:PYTHON_CMD = "python"
|
|
19
|
+
$env:DEEPSEEK_TOKENIZER_DIR = "D:\path\to\tokenizer"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If Python or tokenizer files are unavailable, the backend can still run; token counting may fall back to the non-Python path used by the runtime.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
Global install:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
npm install -g godot-daedalus_backend
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Local project install:
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
npm install godot-daedalus_backend
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Run The WebSocket Backend
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
$env:PORT = "8080"
|
|
42
|
+
godot-daedalus-backend
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The Godot plugin connects to this backend over WebSocket. Provider configuration, including the DeepSeek API key, is normally saved from the plugin settings UI.
|
|
46
|
+
|
|
47
|
+
## Run The Godot MCP Server
|
|
48
|
+
|
|
49
|
+
The standalone Godot MCP server requires a Godot project path:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
$env:GODOT_PROJECT_PATH = "D:\GodotProjects\example"
|
|
53
|
+
godot-daedalus-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Run The Terminal MCP Server
|
|
57
|
+
|
|
58
|
+
The terminal MCP server exposes guarded verification commands, including Godot headless checks. Configure the project and Godot executable first:
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
$env:GODOT_PROJECT_PATH = "D:\GodotProjects\example"
|
|
62
|
+
$env:GODOT_EXECUTABLE_PATH = "D:\Godot_v4.7-stable_win64.exe\Godot_v4.7-stable_win64.exe"
|
|
63
|
+
godot-daedalus-terminal-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```powershell
|
|
69
|
+
npm install
|
|
70
|
+
npm run typecheck
|
|
71
|
+
npm test
|
|
72
|
+
npm run pack:check
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Useful scripts:
|
|
76
|
+
|
|
77
|
+
- `npm start`: run the WebSocket backend from source.
|
|
78
|
+
- `npm run mcp`: run the Godot MCP server from source.
|
|
79
|
+
- `npm run terminal:mcp`: run the terminal MCP server from source.
|
|
80
|
+
- `npm run ping`: run the local ping client.
|
|
81
|
+
- `npm run pack:check`: inspect the npm package contents without publishing.
|
|
82
|
+
|
|
83
|
+
## Published Files
|
|
84
|
+
|
|
85
|
+
The npm package uses the `files` whitelist in `package.json`. Runtime users receive:
|
|
86
|
+
|
|
87
|
+
- `bin/`
|
|
88
|
+
- `src/`
|
|
89
|
+
- `scripts/deepseek-tokenizer-server.py`
|
|
90
|
+
- `README.md`
|
|
91
|
+
- `package.json`
|
|
92
|
+
|
|
93
|
+
Tests, local agent instructions, generated tarballs, environment files, `node_modules/`, and tokenizer model files are not published.
|
|
94
|
+
|
|
95
|
+
Before publishing:
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
npm test
|
|
99
|
+
npm publish --dry-run
|
|
100
|
+
npm publish
|
|
101
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export function runTsxEntry(entryRelativePath) {
|
|
6
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const entryPath = resolve(packageRoot, entryRelativePath);
|
|
8
|
+
const child = spawn(process.execPath, ["--import", "tsx", entryPath, ...process.argv.slice(2)], {
|
|
9
|
+
cwd: packageRoot,
|
|
10
|
+
env: process.env,
|
|
11
|
+
stdio: "inherit"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
child.on("error", (error) => {
|
|
15
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
child.on("exit", (code, signal) => {
|
|
20
|
+
if (signal !== null) {
|
|
21
|
+
console.error(`Process exited from signal ${signal}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
process.exit(code ?? 1);
|
|
25
|
+
});
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "godot-daedalus_backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript backend for the Godot Daedalus editor assistant plugin.",
|
|
5
|
+
"main": "./src/main.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"godot-daedalus-backend": "bin/godot-daedalus-backend.js",
|
|
8
|
+
"godot-daedalus-mcp": "bin/godot-daedalus-mcp.js",
|
|
9
|
+
"godot-daedalus-terminal-mcp": "bin/godot-daedalus-terminal-mcp.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"src/",
|
|
14
|
+
"scripts/deepseek-tokenizer-server.py",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "tsx src/main.ts",
|
|
19
|
+
"dev": "tsx src/main.ts",
|
|
20
|
+
"ping": "tsx src/ping-client.ts",
|
|
21
|
+
"mcp": "tsx src/mcp/godot-mcp-server.ts",
|
|
22
|
+
"terminal:mcp": "tsx src/mcp/terminal-mcp-server.ts",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "npm run typecheck && node --import tsx --test \"tests/*.test.ts\"",
|
|
25
|
+
"pack:check": "npm pack --dry-run",
|
|
26
|
+
"prepublishOnly": "npm test"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.19"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"godot",
|
|
33
|
+
"editor-plugin",
|
|
34
|
+
"mcp",
|
|
35
|
+
"websocket",
|
|
36
|
+
"deepseek"
|
|
37
|
+
],
|
|
38
|
+
"author": "LuYingYiLong",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"type": "module",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
+
"keytar": "^7.9.0",
|
|
44
|
+
"openai": "^6.45.0",
|
|
45
|
+
"tsx": "^4.22.4",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"ws": "^8.21.0",
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^26.0.1",
|
|
52
|
+
"@types/ws": "^8.18.1"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""DeepSeek tokenizer JSON-line server.
|
|
2
|
+
|
|
3
|
+
Usage: python scripts/deepseek-tokenizer-server.py <tokenizer_dir>
|
|
4
|
+
|
|
5
|
+
Reads JSON objects from stdin, one per line: {"id": 1, "text": "..."}
|
|
6
|
+
Writes JSON responses to stdout: {"id": 1, "tokens": 42}
|
|
7
|
+
Sends {"ready": true} on startup.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from tokenizers import Tokenizer
|
|
14
|
+
|
|
15
|
+
if hasattr(sys.stdin, "reconfigure"):
|
|
16
|
+
sys.stdin.reconfigure(encoding="utf-8")
|
|
17
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
18
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
if len(sys.argv) < 2:
|
|
22
|
+
print(json.dumps({"error": "Usage: deepseek-tokenizer-server.py <tokenizer_dir>"}), flush=True)
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
tokenizer_dir = Path(sys.argv[1])
|
|
26
|
+
tokenizer_path = tokenizer_dir / "tokenizer.json"
|
|
27
|
+
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
|
28
|
+
print(json.dumps({"ready": True}), flush=True)
|
|
29
|
+
|
|
30
|
+
for line in sys.stdin:
|
|
31
|
+
line = line.strip()
|
|
32
|
+
if not line:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
request = json.loads(line)
|
|
37
|
+
except json.JSONDecodeError as e:
|
|
38
|
+
print(json.dumps({"error": f"Invalid JSON: {e}"}), flush=True)
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
request_id = request.get("id")
|
|
42
|
+
text = request.get("text", "")
|
|
43
|
+
if not isinstance(text, str):
|
|
44
|
+
print(json.dumps({"id": request_id, "error": "text must be a string"}), flush=True)
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
encoded = tokenizer.encode(text)
|
|
49
|
+
print(json.dumps({"id": request_id, "tokens": len(encoded.ids)}), flush=True)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(json.dumps({"id": request_id, "error": str(e)}), flush=True)
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
package/src/app-paths.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
const APP_DIR_NAME: string = ".godot_daedalus";
|
|
4
|
+
|
|
5
|
+
export function getAppDataDir(): string {
|
|
6
|
+
const windowsAppData: string | undefined = process.env.APPDATA;
|
|
7
|
+
if (!windowsAppData || windowsAppData.trim().length === 0) {
|
|
8
|
+
throw new Error("APPDATA is not configured");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return join(windowsAppData, APP_DIR_NAME);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getDefaultWorkspaceConfigPath(): string {
|
|
15
|
+
return join(getAppDataDir(), "config", "workspaces.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getProviderConfigPath(): string {
|
|
19
|
+
return join(getAppDataDir(), "config", "provider.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getMcpServersConfigPath(): string {
|
|
23
|
+
return join(getAppDataDir(), "config", "mcp-servers.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getDefaultSessionsDir(): string {
|
|
27
|
+
return join(getAppDataDir(), "data", "sessions");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getDefaultArchivedSessionsDir(): string {
|
|
31
|
+
return join(getAppDataDir(), "data", "archived_sessions");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getToolExecutionLedgerPath(): string {
|
|
35
|
+
return join(getAppDataDir(), "data", "tool-executions.jsonl");
|
|
36
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createServer } from "./server/websocket-server.js";
|
|
2
|
+
import { McpHost } from "./mcp/mcp-host.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PORT: number = 8080;
|
|
5
|
+
const portText: string = process.env.PORT ?? String(DEFAULT_PORT);
|
|
6
|
+
const port: number = Number.parseInt(portText, 10);
|
|
7
|
+
|
|
8
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
9
|
+
throw new Error(`Invalid PORT: ${portText}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const mcpHost: McpHost = new McpHost();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await mcpHost.connectAll();
|
|
16
|
+
} catch (error: unknown) {
|
|
17
|
+
console.warn("MCP host failed to connect:", error instanceof Error ? error.message : error);
|
|
18
|
+
console.warn("Server will start without MCP support");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
createServer(port, mcpHost);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
const HEADER_SEPARATOR: string = "\r\n\r\n";
|
|
4
|
+
|
|
5
|
+
export function encodeContentLengthMessage(payload: unknown): Buffer {
|
|
6
|
+
const body: string = JSON.stringify(payload);
|
|
7
|
+
const bodyBuffer: Buffer = Buffer.from(body, "utf8");
|
|
8
|
+
const header: string = `Content-Length: ${bodyBuffer.byteLength}\r\n\r\n`;
|
|
9
|
+
return Buffer.concat([Buffer.from(header, "ascii"), bodyBuffer]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ContentLengthMessageParser {
|
|
13
|
+
private buffer: Buffer = Buffer.alloc(0);
|
|
14
|
+
|
|
15
|
+
push(chunk: Buffer): unknown[] {
|
|
16
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
17
|
+
const messages: unknown[] = [];
|
|
18
|
+
|
|
19
|
+
while (true) {
|
|
20
|
+
const headerEnd: number = this.buffer.indexOf(HEADER_SEPARATOR);
|
|
21
|
+
if (headerEnd < 0) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const headerText: string = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
26
|
+
const contentLength: number | null = parseContentLength(headerText);
|
|
27
|
+
if (contentLength === null) {
|
|
28
|
+
throw new Error("Missing Content-Length header");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const bodyStart: number = headerEnd + Buffer.byteLength(HEADER_SEPARATOR, "ascii");
|
|
32
|
+
const bodyEnd: number = bodyStart + contentLength;
|
|
33
|
+
if (this.buffer.byteLength < bodyEnd) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bodyText: string = this.buffer.subarray(bodyStart, bodyEnd).toString("utf8");
|
|
38
|
+
messages.push(JSON.parse(bodyText) as unknown);
|
|
39
|
+
this.buffer = this.buffer.subarray(bodyEnd);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return messages;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseContentLength(headerText: string): number | null {
|
|
47
|
+
const lines: string[] = headerText.split("\r\n");
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const separatorIndex: number = line.indexOf(":");
|
|
50
|
+
if (separatorIndex < 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const name: string = line.slice(0, separatorIndex).trim().toLowerCase();
|
|
55
|
+
if (name !== "content-length") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const value: string = line.slice(separatorIndex + 1).trim();
|
|
60
|
+
if (!/^\d+$/.test(value)) {
|
|
61
|
+
throw new Error(`Invalid Content-Length header: ${value}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Number.parseInt(value, 10);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|