deepdebug-local-agent 0.3.7 → 0.3.9
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/analyzers/config-analyzer.js +446 -0
- package/analyzers/controller-analyzer.js +429 -0
- package/analyzers/dto-analyzer.js +455 -0
- package/detectors/build-tool-detector.js +0 -0
- package/detectors/framework-detector.js +91 -0
- package/detectors/language-detector.js +89 -0
- package/detectors/multi-project-detector.js +191 -0
- package/detectors/service-detector.js +244 -0
- package/detectors.js +30 -0
- package/exec-utils.js +215 -0
- package/fs-utils.js +34 -0
- package/mcp-http-server.js +313 -0
- package/package.json +1 -1
- package/patch.js +607 -0
- package/ports.js +69 -0
- package/server.js +1 -138
- package/workspace/detect-port.js +176 -0
- package/workspace/file-reader.js +54 -0
- package/workspace/git-client.js +0 -0
- package/workspace/process-manager.js +619 -0
- package/workspace/scanner.js +72 -0
- package/workspace-manager.js +172 -0
package/fs-utils.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
export const stat = promisify(fs.stat);
|
|
6
|
+
export const readdir = promisify(fs.readdir);
|
|
7
|
+
export const readFile = promisify(fs.readFile);
|
|
8
|
+
export const writeFile = promisify(fs.writeFile);
|
|
9
|
+
export const access = promisify(fs.access);
|
|
10
|
+
|
|
11
|
+
export async function exists(p) {
|
|
12
|
+
try { await access(p, fs.constants.F_OK); return true; }
|
|
13
|
+
catch { return false; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function listRecursive(root, { maxFiles = 5000, includeHidden = false } = {}) {
|
|
17
|
+
const results = [];
|
|
18
|
+
async function walk(dir) {
|
|
19
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
if (!includeHidden && e.name.startsWith(".")) continue;
|
|
22
|
+
const full = path.join(dir, e.name);
|
|
23
|
+
if (e.isDirectory()) {
|
|
24
|
+
results.push({ type: "dir", path: path.relative(root, full) });
|
|
25
|
+
await walk(full);
|
|
26
|
+
} else {
|
|
27
|
+
results.push({ type: "file", path: path.relative(root, full) });
|
|
28
|
+
if (results.length >= maxFiles) return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
await walk(root);
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { readFile, writeFile, listRecursive, exists } from "./fs-utils.js";
|
|
5
|
+
import { detectProject } from "./detectors.js";
|
|
6
|
+
import { applyUnifiedDiff } from "./patch.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MCP HTTP Bridge — Expõe as MCP tools como REST API.
|
|
10
|
+
*
|
|
11
|
+
* O Gateway Java chama estes endpoints para obter contexto do código
|
|
12
|
+
* ANTES de enviar o prompt ao Claude.
|
|
13
|
+
*
|
|
14
|
+
* Port: 5056 (separado do Express principal na 5055)
|
|
15
|
+
*
|
|
16
|
+
* Isto é mais prático que stdio MCP para comunicação Gateway ↔ Agent,
|
|
17
|
+
* e mantém compatibilidade com MCP tools para uso futuro com Claude Code.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const IGNORE_DIRS = ["node_modules", ".git", "target", "build", "dist", ".idea", "__pycache__", "vendor", ".gradle"];
|
|
21
|
+
|
|
22
|
+
export function startMCPHttpServer(workspaceManager, port = 5056) {
|
|
23
|
+
const app = express();
|
|
24
|
+
app.use(express.json({ limit: "50mb" }));
|
|
25
|
+
|
|
26
|
+
// ========================================
|
|
27
|
+
// Health
|
|
28
|
+
// ========================================
|
|
29
|
+
app.get("/health", (_req, res) => {
|
|
30
|
+
res.json({
|
|
31
|
+
status: "ok",
|
|
32
|
+
server: "deepdebug-mcp",
|
|
33
|
+
workspaces: workspaceManager.count,
|
|
34
|
+
openWorkspaces: workspaceManager.list().map(w => ({
|
|
35
|
+
id: w.id, root: w.root, language: w.projectInfo?.language
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ========================================
|
|
41
|
+
// POST /mcp/open — Open workspace
|
|
42
|
+
// ========================================
|
|
43
|
+
app.post("/mcp/open", async (req, res) => {
|
|
44
|
+
const { workspaceId, root } = req.body;
|
|
45
|
+
if (!workspaceId || !root) {
|
|
46
|
+
return res.status(400).json({ error: "workspaceId and root are required" });
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const ws = await workspaceManager.open(workspaceId, root);
|
|
50
|
+
res.json({ ok: true, workspace: ws });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
res.status(400).json({ error: e.message });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ========================================
|
|
57
|
+
// POST /mcp/close — Close workspace
|
|
58
|
+
// ========================================
|
|
59
|
+
app.post("/mcp/close", (req, res) => {
|
|
60
|
+
const { workspaceId } = req.body;
|
|
61
|
+
const closed = workspaceManager.close(workspaceId);
|
|
62
|
+
res.json({ ok: closed });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ========================================
|
|
66
|
+
// GET /mcp/workspaces — List all open
|
|
67
|
+
// ========================================
|
|
68
|
+
app.get("/mcp/workspaces", (_req, res) => {
|
|
69
|
+
res.json({ workspaces: workspaceManager.list() });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ========================================
|
|
73
|
+
// POST /mcp/read-file
|
|
74
|
+
// ========================================
|
|
75
|
+
app.post("/mcp/read-file", async (req, res) => {
|
|
76
|
+
const { workspaceId, path: filePath } = req.body;
|
|
77
|
+
try {
|
|
78
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
79
|
+
const fullPath = path.resolve(root, filePath);
|
|
80
|
+
if (!fullPath.startsWith(root)) {
|
|
81
|
+
return res.status(403).json({ error: "Path outside workspace" });
|
|
82
|
+
}
|
|
83
|
+
if (!(await exists(fullPath))) {
|
|
84
|
+
return res.status(404).json({ error: `File not found: ${filePath}` });
|
|
85
|
+
}
|
|
86
|
+
const content = await readFile(fullPath, "utf8");
|
|
87
|
+
res.json({
|
|
88
|
+
ok: true,
|
|
89
|
+
path: filePath,
|
|
90
|
+
content,
|
|
91
|
+
size: Buffer.byteLength(content),
|
|
92
|
+
lines: content.split("\n").length,
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {
|
|
95
|
+
res.status(400).json({ error: e.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ========================================
|
|
100
|
+
// POST /mcp/read-files — Batch read
|
|
101
|
+
// ========================================
|
|
102
|
+
app.post("/mcp/read-files", async (req, res) => {
|
|
103
|
+
const { workspaceId, paths } = req.body;
|
|
104
|
+
if (!paths || !Array.isArray(paths)) {
|
|
105
|
+
return res.status(400).json({ error: "paths array required" });
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
109
|
+
const results = {};
|
|
110
|
+
|
|
111
|
+
for (const filePath of paths.slice(0, 20)) { // Max 20 files
|
|
112
|
+
const fullPath = path.resolve(root, filePath);
|
|
113
|
+
if (!fullPath.startsWith(root)) continue;
|
|
114
|
+
try {
|
|
115
|
+
const content = await readFile(fullPath, "utf8");
|
|
116
|
+
results[filePath] = { ok: true, content, lines: content.split("\n").length };
|
|
117
|
+
} catch {
|
|
118
|
+
results[filePath] = { ok: false, error: "File not found" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.json({ ok: true, files: results, count: Object.keys(results).length });
|
|
123
|
+
} catch (e) {
|
|
124
|
+
res.status(400).json({ error: e.message });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ========================================
|
|
129
|
+
// POST /mcp/list-directory
|
|
130
|
+
// ========================================
|
|
131
|
+
app.post("/mcp/list-directory", async (req, res) => {
|
|
132
|
+
const { workspaceId, path: dirPath = "", maxFiles = 500 } = req.body;
|
|
133
|
+
try {
|
|
134
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
135
|
+
const targetDir = dirPath ? path.resolve(root, dirPath) : root;
|
|
136
|
+
|
|
137
|
+
if (!targetDir.startsWith(root)) {
|
|
138
|
+
return res.status(403).json({ error: "Path outside workspace" });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const files = await listRecursive(targetDir, { maxFiles });
|
|
142
|
+
const filtered = files.filter(f => !IGNORE_DIRS.some(ig => f.path.includes(ig)));
|
|
143
|
+
|
|
144
|
+
res.json({
|
|
145
|
+
ok: true,
|
|
146
|
+
root: dirPath || "/",
|
|
147
|
+
totalItems: filtered.length,
|
|
148
|
+
items: filtered,
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
res.status(400).json({ error: e.message });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ========================================
|
|
156
|
+
// POST /mcp/search-code
|
|
157
|
+
// ========================================
|
|
158
|
+
app.post("/mcp/search-code", async (req, res) => {
|
|
159
|
+
const { workspaceId, query, filePattern = "", maxResults = 50 } = req.body;
|
|
160
|
+
if (!query) return res.status(400).json({ error: "query required" });
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
164
|
+
const results = await grepWorkspace(root, query, filePattern, maxResults);
|
|
165
|
+
res.json({ ok: true, query, matches: results.length, results });
|
|
166
|
+
} catch (e) {
|
|
167
|
+
res.status(400).json({ error: e.message });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ========================================
|
|
172
|
+
// POST /mcp/execute-command
|
|
173
|
+
// ========================================
|
|
174
|
+
app.post("/mcp/execute-command", async (req, res) => {
|
|
175
|
+
const { workspaceId, command, timeout = 120 } = req.body;
|
|
176
|
+
if (!command) return res.status(400).json({ error: "command required" });
|
|
177
|
+
|
|
178
|
+
// Security
|
|
179
|
+
const BLOCKED = ["rm -rf /", "mkfs", "dd if=/dev", ":(){ :|:"];
|
|
180
|
+
if (BLOCKED.some(b => command.includes(b))) {
|
|
181
|
+
return res.status(403).json({ error: "Command blocked" });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
186
|
+
const result = await execInWorkspace(root, command, timeout);
|
|
187
|
+
res.json({ ok: result.exitCode === 0, ...result });
|
|
188
|
+
} catch (e) {
|
|
189
|
+
res.status(400).json({ error: e.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ========================================
|
|
194
|
+
// POST /mcp/project-info
|
|
195
|
+
// ========================================
|
|
196
|
+
app.post("/mcp/project-info", async (req, res) => {
|
|
197
|
+
const { workspaceId } = req.body;
|
|
198
|
+
try {
|
|
199
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
200
|
+
const projectInfo = await detectProject(root);
|
|
201
|
+
const files = await listRecursive(root, { maxFiles: 200 });
|
|
202
|
+
const filtered = files.filter(f => !IGNORE_DIRS.some(ig => f.path.includes(ig)));
|
|
203
|
+
|
|
204
|
+
const sourceExts = { java: 0, js: 0, ts: 0, py: 0, go: 0, cs: 0 };
|
|
205
|
+
filtered.forEach(f => {
|
|
206
|
+
if (f.type !== "file") return;
|
|
207
|
+
const ext = f.path.split(".").pop();
|
|
208
|
+
if (ext in sourceExts) sourceExts[ext]++;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
res.json({
|
|
212
|
+
ok: true,
|
|
213
|
+
...projectInfo,
|
|
214
|
+
root,
|
|
215
|
+
totalFiles: filtered.length,
|
|
216
|
+
sourceFiles: sourceExts,
|
|
217
|
+
topDirectories: [...new Set(filtered
|
|
218
|
+
.filter(f => f.type === "dir")
|
|
219
|
+
.map(f => f.path.split("/")[0])
|
|
220
|
+
)].slice(0, 25),
|
|
221
|
+
});
|
|
222
|
+
} catch (e) {
|
|
223
|
+
res.status(400).json({ error: e.message });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ========================================
|
|
228
|
+
// POST /mcp/apply-patch
|
|
229
|
+
// ========================================
|
|
230
|
+
app.post("/mcp/apply-patch", async (req, res) => {
|
|
231
|
+
const { workspaceId, diff } = req.body;
|
|
232
|
+
if (!diff) return res.status(400).json({ error: "diff required" });
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const root = workspaceManager.resolveRoot(workspaceId);
|
|
236
|
+
const result = await applyUnifiedDiff(root, diff);
|
|
237
|
+
res.json({ ok: true, ...result });
|
|
238
|
+
} catch (e) {
|
|
239
|
+
res.status(400).json({ error: e.message });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ========================================
|
|
244
|
+
// START SERVER
|
|
245
|
+
// ========================================
|
|
246
|
+
app.listen(port, () => {
|
|
247
|
+
console.log(`🔌 MCP HTTP Bridge listening on http://localhost:${port}`);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return app;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ========================================
|
|
254
|
+
// HELPERS
|
|
255
|
+
// ========================================
|
|
256
|
+
|
|
257
|
+
function grepWorkspace(root, query, filePattern, maxResults) {
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
const args = [
|
|
260
|
+
"-r", "-n",
|
|
261
|
+
...IGNORE_DIRS.flatMap(d => ["--exclude-dir", d]),
|
|
262
|
+
];
|
|
263
|
+
if (filePattern) args.push("--include", filePattern);
|
|
264
|
+
args.push("-m", String(maxResults), query, ".");
|
|
265
|
+
|
|
266
|
+
const child = spawn("grep", args, { cwd: root });
|
|
267
|
+
let stdout = "";
|
|
268
|
+
|
|
269
|
+
child.stdout.on("data", d => stdout += d.toString());
|
|
270
|
+
const timer = setTimeout(() => { try { child.kill(); } catch {} }, 15000);
|
|
271
|
+
|
|
272
|
+
child.on("close", () => {
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
if (!stdout.trim()) return resolve([]);
|
|
275
|
+
|
|
276
|
+
const results = stdout.trim().split("\n").slice(0, maxResults).map(line => {
|
|
277
|
+
// Format: ./path/to/file.java:42: code line content
|
|
278
|
+
const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
|
|
279
|
+
if (match) {
|
|
280
|
+
return { file: match[1], line: parseInt(match[2]), content: match[3].trim() };
|
|
281
|
+
}
|
|
282
|
+
return { raw: line };
|
|
283
|
+
});
|
|
284
|
+
resolve(results);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function execInWorkspace(root, command, timeoutSec) {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const child = spawn("sh", ["-c", command], { cwd: root });
|
|
292
|
+
let stdout = "";
|
|
293
|
+
let stderr = "";
|
|
294
|
+
|
|
295
|
+
child.stdout.on("data", d => stdout += d.toString());
|
|
296
|
+
child.stderr.on("data", d => stderr += d.toString());
|
|
297
|
+
|
|
298
|
+
const timer = setTimeout(() => {
|
|
299
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
300
|
+
resolve({ exitCode: -1, stdout, stderr, timedOut: true });
|
|
301
|
+
}, timeoutSec * 1000);
|
|
302
|
+
|
|
303
|
+
child.on("close", (code) => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
// Truncate long outputs
|
|
306
|
+
const maxLen = 10000;
|
|
307
|
+
if (stdout.length > maxLen) {
|
|
308
|
+
stdout = stdout.substring(0, maxLen / 2) + "\n...(truncated)...\n" + stdout.substring(stdout.length - maxLen / 2);
|
|
309
|
+
}
|
|
310
|
+
resolve({ exitCode: code, stdout, stderr: stderr.substring(0, 5000), timedOut: false });
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
package/package.json
CHANGED