deepdebug-local-agent 0.3.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.
Files changed (50) hide show
  1. package/.dockerignore +24 -0
  2. package/.idea/deepdebug-local-agent.iml +12 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/Dockerfile +46 -0
  6. package/cloudbuild.yaml +42 -0
  7. package/index.js +42 -0
  8. package/mcp-server.js +533 -0
  9. package/package.json +22 -0
  10. package/src/ai-engine.js +861 -0
  11. package/src/analyzers/config-analyzer.js +446 -0
  12. package/src/analyzers/controller-analyzer.js +429 -0
  13. package/src/analyzers/dto-analyzer.js +455 -0
  14. package/src/detectors/build-tool-detector.js +0 -0
  15. package/src/detectors/framework-detector.js +91 -0
  16. package/src/detectors/language-detector.js +89 -0
  17. package/src/detectors/multi-project-detector.js +191 -0
  18. package/src/detectors/service-detector.js +244 -0
  19. package/src/detectors.js +30 -0
  20. package/src/exec-utils.js +215 -0
  21. package/src/fs-utils.js +34 -0
  22. package/src/git/base-git-provider.js +384 -0
  23. package/src/git/git-provider-registry.js +110 -0
  24. package/src/git/github-provider.js +502 -0
  25. package/src/mcp-http-server.js +313 -0
  26. package/src/patch/patch-engine.js +339 -0
  27. package/src/patch-manager.js +816 -0
  28. package/src/patch.js +607 -0
  29. package/src/patch_bkp.js +154 -0
  30. package/src/ports.js +69 -0
  31. package/src/routes/workspace.route.js +528 -0
  32. package/src/runtimes/base-runtime.js +290 -0
  33. package/src/runtimes/java/gradle-runtime.js +378 -0
  34. package/src/runtimes/java/java-integrations.js +339 -0
  35. package/src/runtimes/java/maven-runtime.js +418 -0
  36. package/src/runtimes/node/node-integrations.js +247 -0
  37. package/src/runtimes/node/npm-runtime.js +466 -0
  38. package/src/runtimes/node/yarn-runtime.js +354 -0
  39. package/src/runtimes/runtime-registry.js +256 -0
  40. package/src/server-local.js +576 -0
  41. package/src/server.js +4565 -0
  42. package/src/utils/environment-diagnostics.js +666 -0
  43. package/src/utils/exec-utils.js +264 -0
  44. package/src/utils/fs-utils.js +218 -0
  45. package/src/workspace/detect-port.js +176 -0
  46. package/src/workspace/file-reader.js +54 -0
  47. package/src/workspace/git-client.js +0 -0
  48. package/src/workspace/process-manager.js +619 -0
  49. package/src/workspace/scanner.js +72 -0
  50. package/src/workspace-manager.js +172 -0
package/mcp-server.js ADDED
@@ -0,0 +1,533 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import path from "path";
9
+ import { spawn } from "child_process";
10
+ import { exists, listRecursive, readFile, writeFile, stat } from "./fs-utils.js";
11
+ import { detectProject, readText } from "./detectors.js";
12
+ import { applyUnifiedDiff } from "./patch.js";
13
+
14
+ /**
15
+ * DeepDebug MCP Server
16
+ *
17
+ * Expõe as capacidades do Local Agent via Model Context Protocol.
18
+ * Permite que o Claude AI aceda ao código real dos workspaces
19
+ * antes de gerar patches.
20
+ *
21
+ * Tools:
22
+ * 1. read_file — Lê ficheiro do workspace
23
+ * 2. write_file — Escreve ficheiro no workspace
24
+ * 3. list_directory — Lista ficheiros/dirs
25
+ * 4. search_code — Grep no workspace
26
+ * 5. execute_command — Corre comando (compile, test)
27
+ * 6. get_project_info — Language, framework, deps
28
+ * 7. apply_patch — Aplica unified diff
29
+ * 8. get_workspace_status — Status de todos os workspaces
30
+ */
31
+
32
+ /** @type {import('./workspace-manager.js').WorkspaceManager} */
33
+ let workspaceManager = null;
34
+
35
+ /**
36
+ * Inicializa o MCP Server com referência ao WorkspaceManager.
37
+ * @param {import('./workspace-manager.js').WorkspaceManager} wsManager
38
+ */
39
+ export function createMCPServer(wsManager) {
40
+ workspaceManager = wsManager;
41
+
42
+ const server = new Server(
43
+ {
44
+ name: "deepdebug-local-agent",
45
+ version: "1.0.0",
46
+ },
47
+ {
48
+ capabilities: {
49
+ tools: {},
50
+ },
51
+ }
52
+ );
53
+
54
+ // ============================================
55
+ // LIST TOOLS
56
+ // ============================================
57
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
58
+ tools: [
59
+ {
60
+ name: "read_file",
61
+ description: "Read the contents of a file from a workspace. Returns the full file content as text. Use this to understand existing code before generating patches.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ workspaceId: { type: "string", description: "Workspace ID (optional, uses default if not provided)" },
66
+ path: { type: "string", description: "Relative file path (e.g. 'src/main/java/com/example/UserService.java')" },
67
+ },
68
+ required: ["path"],
69
+ },
70
+ },
71
+ {
72
+ name: "write_file",
73
+ description: "Write content to a file in a workspace. Creates the file if it doesn't exist. Use with caution.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ workspaceId: { type: "string", description: "Workspace ID" },
78
+ path: { type: "string", description: "Relative file path" },
79
+ content: { type: "string", description: "File content to write" },
80
+ },
81
+ required: ["path", "content"],
82
+ },
83
+ },
84
+ {
85
+ name: "list_directory",
86
+ description: "List files and directories in a workspace path. Returns file tree with types (file/dir). Useful to understand project structure.",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ workspaceId: { type: "string", description: "Workspace ID" },
91
+ path: { type: "string", description: "Relative directory path (empty or '.' for root)" },
92
+ maxFiles: { type: "number", description: "Max files to return (default 500)", default: 500 },
93
+ },
94
+ required: [],
95
+ },
96
+ },
97
+ {
98
+ name: "search_code",
99
+ description: "Search for text patterns in workspace files using grep. Returns matching lines with file paths and line numbers. Use to find related code, usages, and dependencies.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ workspaceId: { type: "string", description: "Workspace ID" },
104
+ query: { type: "string", description: "Search pattern (text or regex)" },
105
+ filePattern: { type: "string", description: "File glob pattern (e.g. '*.java', '*.ts')", default: "*" },
106
+ maxResults: { type: "number", description: "Max results (default 50)", default: 50 },
107
+ },
108
+ required: ["query"],
109
+ },
110
+ },
111
+ {
112
+ name: "execute_command",
113
+ description: "Execute a shell command in the workspace directory. Use for compilation, testing, and validation. Returns stdout, stderr, and exit code.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ workspaceId: { type: "string", description: "Workspace ID" },
118
+ command: { type: "string", description: "Command to execute (e.g. 'mvn compile', 'npm test')" },
119
+ timeoutMs: { type: "number", description: "Timeout in ms (default 120000)", default: 120000 },
120
+ },
121
+ required: ["command"],
122
+ },
123
+ },
124
+ {
125
+ name: "get_project_info",
126
+ description: "Get project metadata: programming language, framework, build tool, and detected port. Use to understand the tech stack before generating fixes.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ workspaceId: { type: "string", description: "Workspace ID" },
131
+ },
132
+ required: [],
133
+ },
134
+ },
135
+ {
136
+ name: "apply_patch",
137
+ description: "Apply a unified diff patch to a file in the workspace. The diff must be in standard unified diff format. Returns the patched file path and size.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ workspaceId: { type: "string", description: "Workspace ID" },
142
+ diff: { type: "string", description: "Unified diff content" },
143
+ },
144
+ required: ["diff"],
145
+ },
146
+ },
147
+ {
148
+ name: "get_workspace_status",
149
+ description: "Get status of all open workspaces, or a specific one. Returns workspace IDs, roots, project info, and last access times.",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ workspaceId: { type: "string", description: "Specific workspace ID (optional, returns all if not provided)" },
154
+ },
155
+ required: [],
156
+ },
157
+ },
158
+ ],
159
+ }));
160
+
161
+ // ============================================
162
+ // CALL TOOL — Route to handler
163
+ // ============================================
164
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
165
+ const { name, arguments: args } = request.params;
166
+
167
+ try {
168
+ switch (name) {
169
+ case "read_file":
170
+ return await handleReadFile(args);
171
+ case "write_file":
172
+ return await handleWriteFile(args);
173
+ case "list_directory":
174
+ return await handleListDirectory(args);
175
+ case "search_code":
176
+ return await handleSearchCode(args);
177
+ case "execute_command":
178
+ return await handleExecuteCommand(args);
179
+ case "get_project_info":
180
+ return await handleGetProjectInfo(args);
181
+ case "apply_patch":
182
+ return await handleApplyPatch(args);
183
+ case "get_workspace_status":
184
+ return await handleGetWorkspaceStatus(args);
185
+ default:
186
+ return errorResponse(`Unknown tool: ${name}`);
187
+ }
188
+ } catch (err) {
189
+ console.error(`❌ MCP tool error [${name}]:`, err.message);
190
+ return errorResponse(err.message);
191
+ }
192
+ });
193
+
194
+ return server;
195
+ }
196
+
197
+ // ============================================
198
+ // TOOL HANDLERS
199
+ // ============================================
200
+
201
+ async function handleReadFile(args) {
202
+ const root = workspaceManager.resolveRoot(args.workspaceId);
203
+ const filePath = args.path;
204
+
205
+ // Security: prevent path traversal
206
+ const full = path.resolve(root, filePath);
207
+ if (!full.startsWith(root)) {
208
+ return errorResponse("Path traversal not allowed");
209
+ }
210
+
211
+ if (!(await exists(full))) {
212
+ return errorResponse(`File not found: ${filePath}`);
213
+ }
214
+
215
+ const fileStats = await stat(full);
216
+ // Limit: 1MB
217
+ if (fileStats.size > 1024 * 1024) {
218
+ return errorResponse(`File too large: ${(fileStats.size / 1024).toFixed(0)}KB (max 1MB)`);
219
+ }
220
+
221
+ const content = await readFile(full, "utf8");
222
+
223
+ return textResponse(
224
+ `File: ${filePath}\nSize: ${fileStats.size} bytes\nLines: ${content.split("\n").length}\n\n${content}`
225
+ );
226
+ }
227
+
228
+ async function handleWriteFile(args) {
229
+ const root = workspaceManager.resolveRoot(args.workspaceId);
230
+ const filePath = args.path;
231
+ const content = args.content;
232
+
233
+ const full = path.resolve(root, filePath);
234
+ if (!full.startsWith(root)) {
235
+ return errorResponse("Path traversal not allowed");
236
+ }
237
+
238
+ // Ensure directory exists
239
+ const dir = path.dirname(full);
240
+ const fs = await import("fs");
241
+ await fs.promises.mkdir(dir, { recursive: true });
242
+
243
+ await writeFile(full, content, "utf8");
244
+
245
+ return textResponse(`Written: ${filePath} (${Buffer.byteLength(content, "utf8")} bytes)`);
246
+ }
247
+
248
+ async function handleListDirectory(args) {
249
+ const root = workspaceManager.resolveRoot(args.workspaceId);
250
+ const dirPath = args.path || ".";
251
+ const maxFiles = args.maxFiles || 500;
252
+
253
+ const full = path.resolve(root, dirPath);
254
+ if (!full.startsWith(root)) {
255
+ return errorResponse("Path traversal not allowed");
256
+ }
257
+
258
+ const entries = await listRecursive(full, { maxFiles });
259
+
260
+ // Format as tree-like output
261
+ const lines = entries.map(e =>
262
+ `${e.type === "dir" ? "📁" : "📄"} ${e.path}`
263
+ );
264
+
265
+ return textResponse(
266
+ `Directory: ${dirPath}\nEntries: ${entries.length}${entries.length >= maxFiles ? " (truncated)" : ""}\n\n${lines.join("\n")}`
267
+ );
268
+ }
269
+
270
+ async function handleSearchCode(args) {
271
+ const root = workspaceManager.resolveRoot(args.workspaceId);
272
+ const query = args.query;
273
+ const filePattern = args.filePattern || "*";
274
+ const maxResults = args.maxResults || 50;
275
+
276
+ return new Promise((resolve) => {
277
+ // Use grep with recursive search, excluding common dirs
278
+ const grepArgs = [
279
+ "-rn", // recursive + line numbers
280
+ "--include", filePattern, // file pattern
281
+ "-m", String(maxResults), // max matches per file
282
+ "--exclude-dir=node_modules",
283
+ "--exclude-dir=.git",
284
+ "--exclude-dir=target",
285
+ "--exclude-dir=build",
286
+ "--exclude-dir=dist",
287
+ "--exclude-dir=.idea",
288
+ "--exclude-dir=__pycache__",
289
+ query,
290
+ "."
291
+ ];
292
+
293
+ const child = spawn("grep", grepArgs, { cwd: root, shell: false });
294
+ let stdout = "";
295
+ let stderr = "";
296
+
297
+ child.stdout.on("data", (d) => stdout += d.toString());
298
+ child.stderr.on("data", (d) => stderr += d.toString());
299
+
300
+ const timer = setTimeout(() => {
301
+ try { child.kill("SIGKILL"); } catch {}
302
+ }, 10000);
303
+
304
+ child.on("close", (code) => {
305
+ clearTimeout(timer);
306
+
307
+ if (code > 1) {
308
+ resolve(errorResponse(`Search failed: ${stderr.trim()}`));
309
+ return;
310
+ }
311
+
312
+ const lines = stdout.trim().split("\n").filter(Boolean);
313
+ const truncated = lines.length >= maxResults;
314
+
315
+ resolve(textResponse(
316
+ `Search: "${query}" (pattern: ${filePattern})\nResults: ${lines.length}${truncated ? " (truncated)" : ""}\n\n${lines.join("\n")}`
317
+ ));
318
+ });
319
+ });
320
+ }
321
+
322
+ async function handleExecuteCommand(args) {
323
+ const root = workspaceManager.resolveRoot(args.workspaceId);
324
+ const command = args.command;
325
+ const timeoutMs = args.timeoutMs || 120000;
326
+
327
+ // Security: block dangerous commands
328
+ const blocked = ["rm -rf /", "rm -rf ~", "mkfs", "dd if=", "> /dev/"];
329
+ for (const b of blocked) {
330
+ if (command.includes(b)) {
331
+ return errorResponse(`Blocked command: ${b}`);
332
+ }
333
+ }
334
+
335
+ return new Promise((resolve) => {
336
+ const child = spawn("sh", ["-c", command], { cwd: root });
337
+ let stdout = "";
338
+ let stderr = "";
339
+
340
+ child.stdout.on("data", (d) => stdout += d.toString());
341
+ child.stderr.on("data", (d) => stderr += d.toString());
342
+
343
+ const timer = setTimeout(() => {
344
+ try { child.kill("SIGKILL"); } catch {}
345
+ resolve(textResponse(
346
+ `Command: ${command}\nStatus: TIMEOUT (${timeoutMs}ms)\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`
347
+ ));
348
+ }, timeoutMs);
349
+
350
+ child.on("close", (code) => {
351
+ clearTimeout(timer);
352
+ resolve(textResponse(
353
+ `Command: ${command}\nExit code: ${code}\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`
354
+ ));
355
+ });
356
+ });
357
+ }
358
+
359
+ async function handleGetProjectInfo(args) {
360
+ const root = workspaceManager.resolveRoot(args.workspaceId);
361
+ const projectInfo = await detectProject(root);
362
+
363
+ // Try to read key config files for more context
364
+ let dependencies = "";
365
+ try {
366
+ if (projectInfo.language === "java" && projectInfo.buildTool === "maven") {
367
+ const pom = await readFile(path.join(root, "pom.xml"), "utf8");
368
+ // Extract just the dependencies section (trimmed)
369
+ const depsMatch = pom.match(/<dependencies>([\s\S]*?)<\/dependencies>/);
370
+ dependencies = depsMatch ? depsMatch[1].trim() : "No dependencies section found";
371
+ } else if (projectInfo.language === "node") {
372
+ const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
373
+ dependencies = JSON.stringify({
374
+ dependencies: pkg.dependencies || {},
375
+ devDependencies: pkg.devDependencies || {}
376
+ }, null, 2);
377
+ } else if (projectInfo.language === "python") {
378
+ try {
379
+ dependencies = await readFile(path.join(root, "requirements.txt"), "utf8");
380
+ } catch {
381
+ try {
382
+ dependencies = await readFile(path.join(root, "pyproject.toml"), "utf8");
383
+ } catch {
384
+ dependencies = "No dependency file found";
385
+ }
386
+ }
387
+ }
388
+ } catch {
389
+ dependencies = "Could not read dependencies";
390
+ }
391
+
392
+ return textResponse(
393
+ `Project Info:\n` +
394
+ ` Language: ${projectInfo.language}\n` +
395
+ ` Build Tool: ${projectInfo.buildTool}\n` +
396
+ ` Marker File: ${projectInfo.marker}\n` +
397
+ ` Root: ${root}\n\n` +
398
+ `Dependencies:\n${dependencies}`
399
+ );
400
+ }
401
+
402
+ async function handleApplyPatch(args) {
403
+ const root = workspaceManager.resolveRoot(args.workspaceId);
404
+ const diff = args.diff;
405
+
406
+ const result = await applyUnifiedDiff(root, diff);
407
+
408
+ return textResponse(
409
+ `Patch applied successfully!\n` +
410
+ ` Target: ${result.target}\n` +
411
+ ` Size: ${result.bytes} bytes`
412
+ );
413
+ }
414
+
415
+ async function handleGetWorkspaceStatus(args) {
416
+ if (args.workspaceId) {
417
+ const ws = workspaceManager.get(args.workspaceId);
418
+ if (!ws) return errorResponse(`Workspace not found: ${args.workspaceId}`);
419
+ return textResponse(JSON.stringify(ws, null, 2));
420
+ }
421
+
422
+ const all = workspaceManager.list();
423
+ return textResponse(
424
+ `Open workspaces: ${all.length}\nDefault: ${workspaceManager.defaultWorkspaceId || "none"}\n\n` +
425
+ JSON.stringify(all, null, 2)
426
+ );
427
+ }
428
+
429
+ // ============================================
430
+ // HELPERS
431
+ // ============================================
432
+
433
+ function textResponse(text) {
434
+ return {
435
+ content: [{ type: "text", text }],
436
+ };
437
+ }
438
+
439
+ function errorResponse(message) {
440
+ return {
441
+ content: [{ type: "text", text: `ERROR: ${message}` }],
442
+ isError: true,
443
+ };
444
+ }
445
+
446
+ // ============================================
447
+ // HTTP/SSE TRANSPORT — Para o Gateway chamar
448
+ // ============================================
449
+
450
+ /**
451
+ * Inicia o MCP Server com transporte SSE numa porta HTTP.
452
+ * O Gateway liga-se via HTTP POST com SSE.
453
+ */
454
+ export async function startMCPHttpServer(wsManager, port = 5056) {
455
+ const express = (await import("express")).default;
456
+ const app = express();
457
+
458
+ const server = createMCPServer(wsManager);
459
+
460
+ // Map para guardar transportes activos
461
+ const transports = new Map();
462
+
463
+ // SSE endpoint — o client liga-se aqui
464
+ app.get("/sse", (req, res) => {
465
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
466
+ console.log(`🔌 MCP SSE connection: ${sessionId}`);
467
+
468
+ const transport = new SSEServerTransport(`/messages/${sessionId}`, res);
469
+ transports.set(sessionId, transport);
470
+
471
+ server.connect(transport).catch(err => {
472
+ console.error(`❌ MCP transport error: ${err.message}`);
473
+ });
474
+
475
+ req.on("close", () => {
476
+ console.log(`🔌 MCP SSE disconnected: ${sessionId}`);
477
+ transports.delete(sessionId);
478
+ });
479
+ });
480
+
481
+ // Message endpoint — o client envia mensagens aqui
482
+ app.post("/messages/:sessionId", express.json(), (req, res) => {
483
+ const { sessionId } = req.params;
484
+ const transport = transports.get(sessionId);
485
+ if (!transport) {
486
+ return res.status(404).json({ error: "Session not found" });
487
+ }
488
+ transport.handlePostMessage(req, res);
489
+ });
490
+
491
+ // Health check
492
+ app.get("/health", (_req, res) => {
493
+ res.json({
494
+ status: "ok",
495
+ protocol: "mcp",
496
+ workspaces: wsManager.count,
497
+ defaultWorkspace: wsManager.defaultWorkspaceId
498
+ });
499
+ });
500
+
501
+ // Simple REST wrapper — para o Gateway chamar tools directamente via HTTP
502
+ // Isto é mais simples que MCP SSE para integração Java
503
+ app.post("/tool/:toolName", express.json(), async (req, res) => {
504
+ const { toolName } = req.params;
505
+ const args = req.body || {};
506
+
507
+ try {
508
+ const result = await server.callTool({ name: toolName, arguments: args });
509
+ res.json(result);
510
+ } catch (err) {
511
+ res.status(500).json({ error: err.message });
512
+ }
513
+ });
514
+
515
+ // List available tools
516
+ app.get("/tools", async (_req, res) => {
517
+ try {
518
+ const result = await server.listTools();
519
+ res.json(result);
520
+ } catch (err) {
521
+ res.status(500).json({ error: err.message });
522
+ }
523
+ });
524
+
525
+ app.listen(port, () => {
526
+ console.log(`🧠 DeepDebug MCP Server listening on http://localhost:${port}`);
527
+ console.log(` SSE: http://localhost:${port}/sse`);
528
+ console.log(` REST: http://localhost:${port}/tool/{toolName}`);
529
+ console.log(` Tools: http://localhost:${port}/tools`);
530
+ });
531
+
532
+ return { server, app };
533
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "deepdebug-local-agent",
3
+ "version": "0.3.1",
4
+ "type": "module",
5
+ "main": "src/server.js",
6
+ "scripts": {
7
+ "start": "node src/server.js",
8
+ "dev": "NODE_ENV=development node src/server.js",
9
+ "mcp": "node src/mcp-server.js"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.26.0",
13
+ "body-parser": "^1.20.3",
14
+ "cors": "^2.8.5",
15
+ "express": "^4.19.2",
16
+ "js-yaml": "^4.1.0",
17
+ "pidusage": "^3.0.2",
18
+ "properties-reader": "^2.3.0",
19
+ "strip-ansi": "^7.1.0",
20
+ "unidiff": "^1.0.2"
21
+ }
22
+ }