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,164 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import keytar from "keytar";
|
|
4
|
+
import { getProviderConfigPath } from "../app-paths.js";
|
|
5
|
+
|
|
6
|
+
const KEYTAR_SERVICE: string = "Godot Daedalus";
|
|
7
|
+
const DEEPSEEK_ACCOUNT: string = "deepseek_api_key";
|
|
8
|
+
|
|
9
|
+
export type ProviderConfigInput = {
|
|
10
|
+
provider: "deepseek";
|
|
11
|
+
apiKey?: string | undefined;
|
|
12
|
+
model?: string | undefined;
|
|
13
|
+
baseUrl?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type StoredProviderConfig = {
|
|
17
|
+
provider: "deepseek";
|
|
18
|
+
model?: string | undefined;
|
|
19
|
+
baseUrl?: string | undefined;
|
|
20
|
+
keyStorage: "keytar";
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ProviderConfigWithSecret = StoredProviderConfig & {
|
|
25
|
+
apiKey?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ProviderConfigStatus = {
|
|
29
|
+
provider: "deepseek";
|
|
30
|
+
configured: boolean;
|
|
31
|
+
model: string | null;
|
|
32
|
+
baseUrl: string | null;
|
|
33
|
+
apiKeyMasked: string | null;
|
|
34
|
+
keyStorage: "keytar";
|
|
35
|
+
configPath: string;
|
|
36
|
+
updatedAt: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function normalizeOptionalString(value: string | undefined): string | undefined {
|
|
40
|
+
const trimmed: string | undefined = value?.trim();
|
|
41
|
+
return trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function maskApiKey(apiKey: string | null): string | null {
|
|
45
|
+
if (apiKey === null || apiKey.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (apiKey.length <= 8) {
|
|
50
|
+
return "********";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `${apiKey.slice(0, 3)}...${apiKey.slice(-4)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readStoredProviderConfig(): Promise<StoredProviderConfig | null> {
|
|
57
|
+
const filePath: string = getProviderConfigPath();
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const raw: string = await readFile(filePath, "utf8");
|
|
61
|
+
const parsed: unknown = JSON.parse(raw);
|
|
62
|
+
|
|
63
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const record: Record<string, unknown> = parsed as Record<string, unknown>;
|
|
68
|
+
if (record.provider !== "deepseek") {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config: StoredProviderConfig = {
|
|
73
|
+
provider: "deepseek",
|
|
74
|
+
keyStorage: "keytar",
|
|
75
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : ""
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (typeof record.model === "string" && record.model.trim().length > 0) {
|
|
79
|
+
config.model = record.model.trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof record.baseUrl === "string" && record.baseUrl.trim().length > 0) {
|
|
83
|
+
config.baseUrl = record.baseUrl.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return config;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function writeStoredProviderConfig(config: StoredProviderConfig): Promise<void> {
|
|
93
|
+
const filePath: string = getProviderConfigPath();
|
|
94
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
95
|
+
await writeFile(filePath, JSON.stringify(config, null, 2), "utf8");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function saveProviderConfig(input: ProviderConfigInput): Promise<ProviderConfigStatus> {
|
|
99
|
+
const apiKey: string | undefined = normalizeOptionalString(input.apiKey);
|
|
100
|
+
if (apiKey !== undefined) {
|
|
101
|
+
await keytar.setPassword(KEYTAR_SERVICE, DEEPSEEK_ACCOUNT, apiKey);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const existing: StoredProviderConfig | null = await readStoredProviderConfig();
|
|
105
|
+
const config: StoredProviderConfig = {
|
|
106
|
+
provider: "deepseek",
|
|
107
|
+
keyStorage: "keytar",
|
|
108
|
+
updatedAt: new Date().toISOString()
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const model: string | undefined = normalizeOptionalString(input.model) ?? existing?.model;
|
|
112
|
+
const baseUrl: string | undefined = normalizeOptionalString(input.baseUrl) ?? existing?.baseUrl;
|
|
113
|
+
|
|
114
|
+
if (model !== undefined) {
|
|
115
|
+
config.model = model;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (baseUrl !== undefined) {
|
|
119
|
+
config.baseUrl = baseUrl;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await writeStoredProviderConfig(config);
|
|
123
|
+
return getProviderConfigStatus();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function loadProviderConfigWithSecret(): Promise<ProviderConfigWithSecret | null> {
|
|
127
|
+
const stored: StoredProviderConfig | null = await readStoredProviderConfig();
|
|
128
|
+
const apiKey: string | null = await keytar.getPassword(KEYTAR_SERVICE, DEEPSEEK_ACCOUNT);
|
|
129
|
+
|
|
130
|
+
if (stored === null && apiKey === null) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
provider: "deepseek",
|
|
136
|
+
keyStorage: "keytar",
|
|
137
|
+
updatedAt: stored?.updatedAt ?? "",
|
|
138
|
+
model: stored?.model,
|
|
139
|
+
baseUrl: stored?.baseUrl,
|
|
140
|
+
apiKey: apiKey ?? undefined
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function getProviderConfigStatus(): Promise<ProviderConfigStatus> {
|
|
145
|
+
const stored: StoredProviderConfig | null = await readStoredProviderConfig();
|
|
146
|
+
const apiKey: string | null = await keytar.getPassword(KEYTAR_SERVICE, DEEPSEEK_ACCOUNT);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
provider: "deepseek",
|
|
150
|
+
configured: apiKey !== null,
|
|
151
|
+
model: stored?.model ?? null,
|
|
152
|
+
baseUrl: stored?.baseUrl ?? null,
|
|
153
|
+
apiKeyMasked: maskApiKey(apiKey),
|
|
154
|
+
keyStorage: "keytar",
|
|
155
|
+
configPath: getProviderConfigPath(),
|
|
156
|
+
updatedAt: stored?.updatedAt ?? null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function clearProviderConfig(): Promise<ProviderConfigStatus> {
|
|
161
|
+
await keytar.deletePassword(KEYTAR_SERVICE, DEEPSEEK_ACCOUNT);
|
|
162
|
+
await rm(getProviderConfigPath(), { force: true });
|
|
163
|
+
return getProviderConfigStatus();
|
|
164
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AiChatParams, ChatMessage, ModelProfile } from "../protocol/types.js";
|
|
2
|
+
import type { DeepSeekAgentContinuation } from "../providers/deepseek-agent.js";
|
|
3
|
+
import type { DeepSeekChatOptions } from "../providers/deepseek-client.js";
|
|
4
|
+
import type { SessionMetadata } from "../session/session-store.js";
|
|
5
|
+
import { ApprovalGateway } from "../tools/approval-gateway.js";
|
|
6
|
+
import type { SkillId } from "../skills/registry.js";
|
|
7
|
+
import { getDefaultModelProfile } from "../tokens/model-profiles.js";
|
|
8
|
+
import type { WorkspaceConfig } from "../workspace/types.js";
|
|
9
|
+
import type { WorkflowRunState } from "../workflow/types.js";
|
|
10
|
+
|
|
11
|
+
export type PendingGuide = {
|
|
12
|
+
id: string;
|
|
13
|
+
clientGuideId: string;
|
|
14
|
+
text: string;
|
|
15
|
+
anchorRequestId?: string | undefined;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ThinkingEventBuffer = {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
requestId: string;
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type PendingAiContinuation = {
|
|
27
|
+
params: AiChatParams;
|
|
28
|
+
options: DeepSeekChatOptions;
|
|
29
|
+
continuation: DeepSeekAgentContinuation;
|
|
30
|
+
allowedToolNames?: readonly string[] | undefined;
|
|
31
|
+
userMessage: string;
|
|
32
|
+
requestId: string;
|
|
33
|
+
userCreatedAt: string;
|
|
34
|
+
stream: boolean;
|
|
35
|
+
workflowState?: WorkflowRunState | undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ClientSession = {
|
|
39
|
+
deepseekApiKey?: string | undefined;
|
|
40
|
+
deepseekModel?: string | undefined;
|
|
41
|
+
deepseekBaseUrl?: string | undefined;
|
|
42
|
+
godotExecutablePath?: string | undefined;
|
|
43
|
+
godotProjectPath?: string | undefined;
|
|
44
|
+
messages: ChatMessage[];
|
|
45
|
+
modelProfile: ModelProfile;
|
|
46
|
+
approvalGateway: ApprovalGateway;
|
|
47
|
+
activeSkillId?: SkillId | undefined;
|
|
48
|
+
activeWorkspace?: WorkspaceConfig | undefined;
|
|
49
|
+
sessionId?: string | undefined;
|
|
50
|
+
sessionTitle?: string | undefined;
|
|
51
|
+
summaryMessage?: ChatMessage | undefined;
|
|
52
|
+
summaryCoveredMessageCount?: number | undefined;
|
|
53
|
+
pendingAiContinuations: Map<string, PendingAiContinuation>;
|
|
54
|
+
thinkingEventBuffers: Map<string, ThinkingEventBuffer>;
|
|
55
|
+
activeAbortControllers: Map<string, AbortController>;
|
|
56
|
+
inFlightRequestIds: Set<string>;
|
|
57
|
+
completedRequestIds: Map<string, number>;
|
|
58
|
+
eventPersistQueue: Promise<void>;
|
|
59
|
+
pendingGuides: PendingGuide[];
|
|
60
|
+
fullSessionLoadPromise?: Promise<void> | undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function createClientSession(defaultWorkspace: WorkspaceConfig | undefined): ClientSession {
|
|
64
|
+
return {
|
|
65
|
+
messages: [],
|
|
66
|
+
modelProfile: getDefaultModelProfile(),
|
|
67
|
+
approvalGateway: new ApprovalGateway(),
|
|
68
|
+
activeWorkspace: defaultWorkspace,
|
|
69
|
+
pendingAiContinuations: new Map(),
|
|
70
|
+
thinkingEventBuffers: new Map(),
|
|
71
|
+
activeAbortControllers: new Map(),
|
|
72
|
+
inFlightRequestIds: new Set(),
|
|
73
|
+
completedRequestIds: new Map(),
|
|
74
|
+
pendingGuides: [],
|
|
75
|
+
eventPersistQueue: Promise.resolve()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function clearActiveSession(session: ClientSession): void {
|
|
80
|
+
session.sessionId = undefined;
|
|
81
|
+
session.sessionTitle = undefined;
|
|
82
|
+
session.messages = [];
|
|
83
|
+
session.fullSessionLoadPromise = undefined;
|
|
84
|
+
session.summaryMessage = undefined;
|
|
85
|
+
session.summaryCoveredMessageCount = undefined;
|
|
86
|
+
session.pendingGuides = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function applySessionMetadata(session: ClientSession, metadata: SessionMetadata): void {
|
|
90
|
+
session.sessionId = metadata.id;
|
|
91
|
+
session.sessionTitle = metadata.title;
|
|
92
|
+
session.activeSkillId = metadata.activeSkillId as SkillId | undefined;
|
|
93
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type WebSocket from "ws";
|
|
2
|
+
import type { ClientRequest } from "../protocol/types.js";
|
|
3
|
+
import type { McpHost } from "../mcp/mcp-host.js";
|
|
4
|
+
import type { ClientSession } from "./client-session.js";
|
|
5
|
+
|
|
6
|
+
export type RequestHandler = (
|
|
7
|
+
socket: WebSocket,
|
|
8
|
+
request: ClientRequest,
|
|
9
|
+
session: ClientSession,
|
|
10
|
+
mcpHost: McpHost
|
|
11
|
+
) => Promise<void> | void;
|
|
12
|
+
|
|
13
|
+
export const REQUEST_HANDLER_METHODS: readonly ClientRequest["method"][] = [
|
|
14
|
+
"ping",
|
|
15
|
+
"provider.configure",
|
|
16
|
+
"provider.config.get",
|
|
17
|
+
"provider.config.set",
|
|
18
|
+
"provider.config.clear",
|
|
19
|
+
"ai.chat",
|
|
20
|
+
"ai.next_step_hints",
|
|
21
|
+
"ai.cancel",
|
|
22
|
+
"prompt.list",
|
|
23
|
+
"skill.list",
|
|
24
|
+
"skill.activate",
|
|
25
|
+
"session.reset",
|
|
26
|
+
"session.info",
|
|
27
|
+
"session.create",
|
|
28
|
+
"session.open",
|
|
29
|
+
"session.timeline",
|
|
30
|
+
"session.list",
|
|
31
|
+
"session.archive",
|
|
32
|
+
"session.archived.list",
|
|
33
|
+
"session.archived.restore",
|
|
34
|
+
"session.archived.delete",
|
|
35
|
+
"session.save",
|
|
36
|
+
"session.delete",
|
|
37
|
+
"session.rename",
|
|
38
|
+
"session.compress",
|
|
39
|
+
"session.summary",
|
|
40
|
+
"session.guide.add",
|
|
41
|
+
"session.guide.update",
|
|
42
|
+
"session.guide.delete",
|
|
43
|
+
"mcp.listTools",
|
|
44
|
+
"mcp.callTool",
|
|
45
|
+
"mcp.listResources",
|
|
46
|
+
"mcp.readResource",
|
|
47
|
+
"mcp.config.list",
|
|
48
|
+
"mcp.config.add",
|
|
49
|
+
"mcp.config.remove",
|
|
50
|
+
"mcp.config.setEnabled",
|
|
51
|
+
"fileChange.create",
|
|
52
|
+
"fileChange.overwrite",
|
|
53
|
+
"fileChange.delete",
|
|
54
|
+
"approval.list",
|
|
55
|
+
"approval.mode.set",
|
|
56
|
+
"approval.approve",
|
|
57
|
+
"approval.reject",
|
|
58
|
+
"environment.configure",
|
|
59
|
+
"editor.context.update",
|
|
60
|
+
"editor.tool.result",
|
|
61
|
+
"workspace.list",
|
|
62
|
+
"workspace.select",
|
|
63
|
+
"workspace.info"
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
export const REQUEST_HANDLERS: ReadonlyMap<ClientRequest["method"], RequestHandler | null> = new Map(
|
|
67
|
+
REQUEST_HANDLER_METHODS.map((method: ClientRequest["method"]): [ClientRequest["method"], RequestHandler | null] => [method, null])
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export function assertKnownRequestMethod(method: ClientRequest["method"]): void {
|
|
71
|
+
if (!REQUEST_HANDLERS.has(method)) {
|
|
72
|
+
throw new Error(`Request method is missing dispatcher registration: ${method}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type WebSocket from "ws";
|
|
2
|
+
import type { ServerEvent } from "../protocol/types.js";
|
|
3
|
+
import { sendJson } from "./send-json.js";
|
|
4
|
+
|
|
5
|
+
export function sendOk(socket: WebSocket, id: string, result: unknown): void {
|
|
6
|
+
sendJson(socket, {
|
|
7
|
+
type: "response",
|
|
8
|
+
id,
|
|
9
|
+
ok: true,
|
|
10
|
+
result
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sendError(socket: WebSocket, id: string, code: string, message: string): void {
|
|
15
|
+
sendJson(socket, {
|
|
16
|
+
type: "response",
|
|
17
|
+
id,
|
|
18
|
+
ok: false,
|
|
19
|
+
error: {
|
|
20
|
+
code,
|
|
21
|
+
message
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sendEvent(socket: WebSocket, id: string, event: ServerEvent["event"], data: unknown): void {
|
|
27
|
+
sendJson(socket, {
|
|
28
|
+
type: "event",
|
|
29
|
+
id,
|
|
30
|
+
event,
|
|
31
|
+
data
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import type { ServerEvent, ServerResponse } from "../protocol/types.js";
|
|
3
|
+
|
|
4
|
+
export function sendJson(socket: WebSocket, message: ServerResponse | ServerEvent): void {
|
|
5
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
6
|
+
socket.send(JSON.stringify(message));
|
|
7
|
+
}
|
|
8
|
+
}
|