gopeak 2.1.0 → 2.2.1

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.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * WebSocket connection for real-time communication with Godot
3
+ */
4
+ let ws = null;
5
+ let wsConnected = false;
6
+ const pendingRequests = new Map();
7
+ let requestId = 0;
8
+ const actionListeners = [];
9
+ export function onActionEvent(callback) {
10
+ actionListeners.push(callback);
11
+ }
12
+ function handleActionEvent(msg) {
13
+ for (const listener of actionListeners) {
14
+ try {
15
+ listener(msg);
16
+ }
17
+ catch (err) {
18
+ console.error('[visualizer] Action listener error:', err);
19
+ }
20
+ }
21
+ }
22
+ export function connectWebSocket() {
23
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
24
+ ws = new WebSocket(`${protocol}//${window.location.host}/visualizer`);
25
+ ws.onopen = () => {
26
+ wsConnected = true;
27
+ console.log('[visualizer] WebSocket connected');
28
+ };
29
+ ws.onclose = () => {
30
+ wsConnected = false;
31
+ console.log('[visualizer] WebSocket disconnected, reconnecting...');
32
+ setTimeout(connectWebSocket, 2000);
33
+ };
34
+ ws.onerror = (err) => {
35
+ console.error('[visualizer] WebSocket error:', err);
36
+ };
37
+ ws.onmessage = (event) => {
38
+ try {
39
+ const msg = JSON.parse(event.data);
40
+ if (msg.type === 'action_event' && !msg.id) {
41
+ handleActionEvent(msg);
42
+ return;
43
+ }
44
+ if (msg.id && pendingRequests.has(msg.id)) {
45
+ const { resolve, reject } = pendingRequests.get(msg.id);
46
+ pendingRequests.delete(msg.id);
47
+ if (msg.error) {
48
+ reject(new Error(msg.error));
49
+ }
50
+ else {
51
+ resolve(msg.result || msg);
52
+ }
53
+ }
54
+ }
55
+ catch (err) {
56
+ console.error('[visualizer] Failed to parse message:', err);
57
+ }
58
+ };
59
+ }
60
+ export function sendCommand(command, args) {
61
+ return new Promise((resolve, reject) => {
62
+ if (!wsConnected || !ws) {
63
+ reject(new Error('WebSocket not connected'));
64
+ return;
65
+ }
66
+ const id = ++requestId;
67
+ pendingRequests.set(id, { resolve, reject });
68
+ ws.send(JSON.stringify({
69
+ type: 'visualizer_command',
70
+ id,
71
+ command,
72
+ args
73
+ }));
74
+ // Timeout after 30 seconds
75
+ setTimeout(() => {
76
+ if (pendingRequests.has(id)) {
77
+ pendingRequests.delete(id);
78
+ reject(new Error('Request timeout'));
79
+ }
80
+ }, 30000);
81
+ });
82
+ }
83
+ export function isConnected() {
84
+ return wsConnected;
85
+ }
@@ -0,0 +1,375 @@
1
+ import { exec, execSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { WebSocket } from 'ws';
6
+ import { createScriptFile, deleteFunction, findUsages, modifyFunction, modifySignal, modifyVariable, refreshMap, resolvePath, } from './gdscript_parser.js';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ let wss = null;
9
+ let currentProjectPath = null;
10
+ let currentBridge = null;
11
+ const DEFAULT_VISUALIZER_HTML = `<!doctype html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8" />
15
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
16
+ <title>Godot MCP Visualizer</title>
17
+ </head>
18
+ <body>
19
+ <h1>Godot MCP Visualizer</h1>
20
+ <p>Run the map_project tool to load visualization data.</p>
21
+ </body>
22
+ </html>`;
23
+ const handleVisualizerConnectionRef = (socket) => {
24
+ handleVisualizerConnection(socket);
25
+ };
26
+ const onToolStart = (event) => {
27
+ broadcastToVisualizer({
28
+ type: 'tool_event',
29
+ event: 'start',
30
+ ...event,
31
+ });
32
+ };
33
+ const onToolEnd = (event) => {
34
+ broadcastToVisualizer({
35
+ type: 'tool_event',
36
+ event: 'end',
37
+ ...event,
38
+ });
39
+ };
40
+ const onGodotConnected = (event) => {
41
+ broadcastToVisualizer({
42
+ type: 'connection_event',
43
+ event: 'godot_connected',
44
+ ...event,
45
+ });
46
+ };
47
+ const onGodotDisconnected = () => {
48
+ broadcastToVisualizer({
49
+ type: 'connection_event',
50
+ event: 'godot_disconnected',
51
+ });
52
+ };
53
+ export function setProjectPath(projectPath) {
54
+ currentProjectPath = projectPath;
55
+ const snapshot = readGitSnapshot(projectPath);
56
+ if (snapshot) {
57
+ lastGitSnapshotByProject.set(projectPath, snapshot);
58
+ }
59
+ else {
60
+ lastGitSnapshotByProject.delete(projectPath);
61
+ }
62
+ }
63
+ export async function serveVisualization(projectData, bridge) {
64
+ if (wss) {
65
+ wss.off('connection', handleVisualizerConnectionRef);
66
+ }
67
+ detachBridgeHandlers();
68
+ const htmlPath = path.join(__dirname, 'visualizer.html');
69
+ let html;
70
+ try {
71
+ html = fs.readFileSync(htmlPath, 'utf-8');
72
+ }
73
+ catch {
74
+ throw new Error(`Visualizer HTML template not found at ${htmlPath}`);
75
+ }
76
+ const dataJson = JSON.stringify(projectData);
77
+ html = html.replace('"%%PROJECT_DATA%%"', dataJson);
78
+ bridge.setVisualizerHtml(html);
79
+ currentBridge = bridge;
80
+ wss = bridge.getVisualizerWss();
81
+ if (!wss) {
82
+ throw new Error('Visualizer WebSocket server is not initialized. Start Godot bridge first.');
83
+ }
84
+ wss.off('connection', handleVisualizerConnectionRef);
85
+ wss.on('connection', handleVisualizerConnectionRef);
86
+ bridge.on('tool_start', onToolStart);
87
+ bridge.on('tool_end', onToolEnd);
88
+ bridge.on('godot_connected', onGodotConnected);
89
+ bridge.on('godot_disconnected', onGodotDisconnected);
90
+ const url = `http://localhost:${bridge.getStatus().port}`;
91
+ console.error(`[visualizer] Serving at ${url}`);
92
+ openBrowser(url);
93
+ return url;
94
+ }
95
+ function handleVisualizerConnection(ws) {
96
+ console.error('[visualizer] Browser connected via WebSocket');
97
+ if (currentBridge) {
98
+ ws.send(JSON.stringify({
99
+ type: 'godot_status',
100
+ status: currentBridge.getStatus(),
101
+ }));
102
+ }
103
+ ws.on('message', async (data) => {
104
+ try {
105
+ const message = JSON.parse(data.toString());
106
+ const result = await handleInternalCommand(message);
107
+ ws.send(JSON.stringify({ id: message.id, ...result }));
108
+ }
109
+ catch (error) {
110
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
111
+ ws.send(JSON.stringify({ error: errMsg }));
112
+ }
113
+ });
114
+ ws.on('close', () => {
115
+ console.error('[visualizer] Browser disconnected');
116
+ });
117
+ }
118
+ function parseDiffHunks(diffText) {
119
+ const hunks = [];
120
+ let additions = 0;
121
+ let deletions = 0;
122
+ const hunkRegex = /^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
123
+ const diffLines = diffText.split('\n');
124
+ let currentHunk = null;
125
+ for (const line of diffLines) {
126
+ const match = line.match(hunkRegex);
127
+ if (match) {
128
+ if (currentHunk) {
129
+ hunks.push(currentHunk);
130
+ }
131
+ const start = parseInt(match[1], 10);
132
+ const count = match[2] ? parseInt(match[2], 10) : 1;
133
+ currentHunk = { startLine: start, endLine: start + count - 1, header: match[3].trim(), lines: [] };
134
+ }
135
+ else if (currentHunk) {
136
+ currentHunk.lines.push(line);
137
+ if (line.startsWith('+') && !line.startsWith('+++')) {
138
+ additions++;
139
+ }
140
+ if (line.startsWith('-') && !line.startsWith('---')) {
141
+ deletions++;
142
+ }
143
+ }
144
+ }
145
+ if (currentHunk) {
146
+ hunks.push(currentHunk);
147
+ }
148
+ return { hunks, additions, deletions };
149
+ }
150
+ function runDiffCommand(command, cwd) {
151
+ try {
152
+ return execSync(command, { cwd, encoding: 'utf-8', timeout: 5000 });
153
+ }
154
+ catch (error) {
155
+ const stdout = error.stdout;
156
+ if (typeof stdout === 'string') {
157
+ return stdout;
158
+ }
159
+ if (Buffer.isBuffer(stdout)) {
160
+ return stdout.toString('utf-8');
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+ function quoteForShell(value) {
166
+ return `"${value.replace(/[\\"$`]/g, '\\$&')}"`;
167
+ }
168
+ const actionLog = [];
169
+ const MAX_ACTION_LOG = 100;
170
+ const lastGitSnapshotByProject = new Map();
171
+ function appendActionEntry(entry) {
172
+ actionLog.push(entry);
173
+ if (actionLog.length > MAX_ACTION_LOG) {
174
+ actionLog.shift();
175
+ }
176
+ if (!wss) {
177
+ return;
178
+ }
179
+ const msg = JSON.stringify({ type: 'action_event', entry });
180
+ wss.clients.forEach((c) => {
181
+ if (c.readyState === WebSocket.OPEN) {
182
+ c.send(msg);
183
+ }
184
+ });
185
+ }
186
+ function parseGitStatusLine(xy) {
187
+ if (xy === '??') {
188
+ return 'untracked';
189
+ }
190
+ if (xy.includes('A')) {
191
+ return 'added';
192
+ }
193
+ return 'modified';
194
+ }
195
+ function readGitSnapshot(projectPath) {
196
+ const gitDir = path.join(projectPath, '.git');
197
+ if (!fs.existsSync(gitDir)) {
198
+ return null;
199
+ }
200
+ try {
201
+ const raw = runDiffCommand('git status --porcelain', projectPath);
202
+ const snapshot = new Map();
203
+ for (const line of raw.split('\n')) {
204
+ if (!line.trim()) {
205
+ continue;
206
+ }
207
+ const xy = line.substring(0, 2);
208
+ const filePath = line.substring(3).trim();
209
+ const actualPath = filePath.includes(' -> ') ? filePath.split(' -> ')[1] : filePath;
210
+ const resFilePath = 'res://' + actualPath;
211
+ snapshot.set(resFilePath, parseGitStatusLine(xy));
212
+ }
213
+ return snapshot;
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
219
+ function recordExternalChanges(projectPath) {
220
+ const previous = lastGitSnapshotByProject.get(projectPath);
221
+ const current = readGitSnapshot(projectPath);
222
+ if (!current) {
223
+ lastGitSnapshotByProject.delete(projectPath);
224
+ return;
225
+ }
226
+ if (!previous) {
227
+ lastGitSnapshotByProject.set(projectPath, current);
228
+ return;
229
+ }
230
+ for (const [filePath, status] of current) {
231
+ const prevStatus = previous.get(filePath);
232
+ if (prevStatus === status) {
233
+ continue;
234
+ }
235
+ appendActionEntry({
236
+ ts: new Date().toISOString(),
237
+ command: 'external_change_detected',
238
+ filePath,
239
+ details: {
240
+ path: filePath,
241
+ status,
242
+ },
243
+ reason: 'Detected during refresh',
244
+ });
245
+ }
246
+ lastGitSnapshotByProject.set(projectPath, current);
247
+ }
248
+ const MUTATION_COMMANDS = new Set([
249
+ 'create_script_file',
250
+ 'modify_variable',
251
+ 'modify_signal',
252
+ 'modify_function',
253
+ 'modify_function_delete',
254
+ ]);
255
+ const COMMAND_MAP = {
256
+ refresh_map: (pp, args) => {
257
+ const result = refreshMap(pp, args);
258
+ if (result.ok) {
259
+ recordExternalChanges(pp);
260
+ }
261
+ return result;
262
+ },
263
+ create_script_file: (pp, args) => createScriptFile(pp, args),
264
+ modify_variable: (pp, args) => modifyVariable(pp, args),
265
+ modify_signal: (pp, args) => modifySignal(pp, args),
266
+ modify_function: (pp, args) => modifyFunction(pp, args),
267
+ modify_function_delete: (pp, args) => deleteFunction(pp, args),
268
+ find_usages: (pp, args) => findUsages(pp, args),
269
+ get_action_log: (_pp, _args) => ({ ok: true, entries: actionLog }),
270
+ get_file_diff: (pp, args) => {
271
+ try {
272
+ const requestPath = args.path || '';
273
+ if (!requestPath) {
274
+ return { ok: false, error: 'Missing required argument: path' };
275
+ }
276
+ const absolutePath = resolvePath(pp, requestPath);
277
+ const quotedAbsolutePath = quoteForShell(absolutePath);
278
+ let diffText = runDiffCommand(`git diff HEAD -- ${quotedAbsolutePath}`, pp);
279
+ if (diffText.trim() === '' && fs.existsSync(absolutePath)) {
280
+ const relPath = path.relative(pp, absolutePath);
281
+ const quotedRelPath = quoteForShell(relPath);
282
+ const untracked = runDiffCommand(`git ls-files --others --exclude-standard -- ${quotedRelPath}`, pp);
283
+ if (untracked.trim() !== '') {
284
+ diffText = runDiffCommand(`git diff --no-index /dev/null ${quotedAbsolutePath}`, pp);
285
+ }
286
+ }
287
+ const parsed = parseDiffHunks(diffText);
288
+ return {
289
+ ok: true,
290
+ diff: {
291
+ path: requestPath,
292
+ hunks: parsed.hunks,
293
+ summary: {
294
+ additions: parsed.additions,
295
+ deletions: parsed.deletions,
296
+ },
297
+ },
298
+ };
299
+ }
300
+ catch (error) {
301
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
302
+ return { ok: false, error: errMsg };
303
+ }
304
+ },
305
+ };
306
+ async function handleInternalCommand(message) {
307
+ const { command, args } = message;
308
+ if (!currentProjectPath) {
309
+ return { ok: false, error: 'No project path set. Call map_project first.' };
310
+ }
311
+ console.error(`[visualizer] Internal command: ${command}`);
312
+ const handler = COMMAND_MAP[command];
313
+ if (handler) {
314
+ try {
315
+ const result = handler(currentProjectPath, args);
316
+ if (result.ok && MUTATION_COMMANDS.has(command)) {
317
+ appendActionEntry({
318
+ ts: new Date().toISOString(),
319
+ command,
320
+ filePath: args.path || '',
321
+ details: { ...args },
322
+ reason: args.reason || undefined,
323
+ });
324
+ }
325
+ return result;
326
+ }
327
+ catch (error) {
328
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
329
+ return { ok: false, error: errMsg };
330
+ }
331
+ }
332
+ return { ok: false, error: `Unknown command: ${command}` };
333
+ }
334
+ export function stopVisualizationServer() {
335
+ if (wss) {
336
+ wss.off('connection', handleVisualizerConnectionRef);
337
+ wss = null;
338
+ }
339
+ if (currentBridge) {
340
+ currentBridge.setVisualizerHtml(DEFAULT_VISUALIZER_HTML);
341
+ detachBridgeHandlers();
342
+ currentBridge = null;
343
+ console.error('[visualizer] Server detached from unified bridge');
344
+ }
345
+ }
346
+ function detachBridgeHandlers() {
347
+ if (!currentBridge) {
348
+ return;
349
+ }
350
+ currentBridge.off('tool_start', onToolStart);
351
+ currentBridge.off('tool_end', onToolEnd);
352
+ currentBridge.off('godot_connected', onGodotConnected);
353
+ currentBridge.off('godot_disconnected', onGodotDisconnected);
354
+ }
355
+ function broadcastToVisualizer(message) {
356
+ if (!wss) {
357
+ return;
358
+ }
359
+ const payload = JSON.stringify(message);
360
+ wss.clients.forEach((client) => {
361
+ if (client.readyState === WebSocket.OPEN) {
362
+ client.send(payload);
363
+ }
364
+ });
365
+ }
366
+ function openBrowser(url) {
367
+ const cmd = process.platform === 'darwin' ? 'open'
368
+ : process.platform === 'win32' ? 'start'
369
+ : 'xdg-open';
370
+ exec(`${cmd} ${url}`, (err) => {
371
+ if (err) {
372
+ console.error(`[visualizer] Could not open browser: ${err.message}`);
373
+ }
374
+ });
375
+ }