deepdebug-local-agent 1.0.10 β†’ 1.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepdebug-local-agent",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "DeepDebug Local Agent - AI-powered code debugging assistant",
5
5
  "private": false,
6
6
  "type": "module",
@@ -6,10 +6,87 @@ import os from "os";
6
6
  const router = express.Router();
7
7
 
8
8
  // πŸ”Ή Caminho global do workspace (inicializado com fallback padrΓ£o)
9
+ // βœ… FIX: Read WORKSPACE_ROOT (set by install.js Docker) OR WORKSPACE_BASE (legacy)
9
10
  let workspacePath =
11
+ process.env.WORKSPACE_ROOT ||
10
12
  process.env.WORKSPACE_BASE ||
11
13
  path.join(os.homedir(), "Documents", "Projects");
12
14
 
15
+ // πŸ†• Hostβ†’Docker path translation
16
+ // When Gateway sends host paths (C:\Users\willi\Projects\my-app),
17
+ // we need to translate them to Docker paths (/workspace/my-app)
18
+ const WORKSPACE_MOUNT = process.env.WORKSPACE_ROOT || "/workspace";
19
+ const HOST_PROJECTS_PATH = process.env.HOST_PROJECTS_PATH || null;
20
+
21
+ console.log(`πŸ“‚ [workspace_route] Initialized with workspacePath: ${workspacePath}`);
22
+ console.log(`πŸ“‚ [workspace_route] WORKSPACE_MOUNT: ${WORKSPACE_MOUNT}`);
23
+ console.log(`πŸ“‚ [workspace_route] HOST_PROJECTS_PATH: ${HOST_PROJECTS_PATH || 'not set'}`);
24
+
25
+ /**
26
+ * Translates a host-side path to the Docker container path.
27
+ *
28
+ * Examples:
29
+ * C:\Users\willi\Documents\Projects\pure-core-ms β†’ /workspace/pure-core-ms
30
+ * /Users/macintosh/IdeaProjects/pure-core-ms β†’ /workspace/pure-core-ms
31
+ * /workspace/pure-core-ms β†’ /workspace/pure-core-ms (unchanged)
32
+ * src/main/java/... β†’ src/main/java/... (relative, unchanged)
33
+ */
34
+ function translateToDockerPath(hostPath) {
35
+ if (!hostPath) return workspacePath;
36
+
37
+ // Normalize path separators (Windows β†’ Unix)
38
+ let normalized = hostPath.replace(/\\/g, '/');
39
+
40
+ // Already a Docker path
41
+ if (normalized.startsWith('/workspace')) {
42
+ return normalized;
43
+ }
44
+
45
+ // Relative path (like src/main/java/...) - not a workspace root
46
+ if (!path.isAbsolute(normalized) && !normalized.match(/^[A-Za-z]:/)) {
47
+ return normalized;
48
+ }
49
+
50
+ // If we know the host projects root, strip it
51
+ if (HOST_PROJECTS_PATH) {
52
+ const normalizedHost = HOST_PROJECTS_PATH.replace(/\\/g, '/').replace(/\/$/, '');
53
+ if (normalized.toLowerCase().startsWith(normalizedHost.toLowerCase())) {
54
+ const relative = normalized.substring(normalizedHost.length).replace(/^\//, '');
55
+ const dockerPath = path.posix.join(WORKSPACE_MOUNT, relative);
56
+ console.log(`πŸ“‚ [translate] ${hostPath} β†’ ${dockerPath} (via HOST_PROJECTS_PATH)`);
57
+ return dockerPath;
58
+ }
59
+ }
60
+
61
+ // Smart detection: look for common project root patterns
62
+ const projectRootPatterns = [
63
+ /.*[/\\](Documents[/\\]Projects)[/\\]/i,
64
+ /.*[/\\](IdeaProjects)[/\\]/i,
65
+ /.*[/\\](source[/\\]repos)[/\\]/i,
66
+ /.*[/\\](Projects)[/\\]/i,
67
+ /.*[/\\](repos)[/\\]/i,
68
+ /.*[/\\](dev)[/\\]/i,
69
+ /.*[/\\](code)[/\\]/i,
70
+ ];
71
+
72
+ for (const pattern of projectRootPatterns) {
73
+ const match = normalized.match(pattern);
74
+ if (match) {
75
+ const afterRoot = normalized.substring(match.index + match[0].length);
76
+ const dockerPath = path.posix.join(WORKSPACE_MOUNT, afterRoot);
77
+ console.log(`πŸ“‚ [translate] ${hostPath} β†’ ${dockerPath} (via pattern: ${match[1]})`);
78
+ return dockerPath;
79
+ }
80
+ }
81
+
82
+ // Last resort: extract the last folder name
83
+ const parts = normalized.replace(/\/$/, '').split('/');
84
+ const projectName = parts[parts.length - 1];
85
+ const dockerPath = path.posix.join(WORKSPACE_MOUNT, projectName);
86
+ console.log(`πŸ“‚ [translate] ${hostPath} β†’ ${dockerPath} (via last folder name)`);
87
+ return dockerPath;
88
+ }
89
+
13
90
  // --------------------------------------------------
14
91
  // 🧩 1️⃣ GET /workspace/info
15
92
  // Retorna informaΓ§Γ΅es sobre o workspace atual
@@ -22,6 +99,11 @@ router.get("/info", async (req, res) => {
22
99
  isDirectory: fs.existsSync(workspacePath)
23
100
  ? fs.lstatSync(workspacePath).isDirectory()
24
101
  : false,
102
+ env: {
103
+ WORKSPACE_ROOT: process.env.WORKSPACE_ROOT || null,
104
+ WORKSPACE_BASE: process.env.WORKSPACE_BASE || null,
105
+ HOST_PROJECTS_PATH: process.env.HOST_PROJECTS_PATH || null,
106
+ }
25
107
  });
26
108
  } catch (err) {
27
109
  console.error("❌ Error reading workspace info:", err);
@@ -29,20 +111,246 @@ router.get("/info", async (req, res) => {
29
111
  }
30
112
  });
31
113
 
114
+ // --------------------------------------------------
115
+ // πŸ†• POST /workspace/open
116
+ // Gateway Java (LocalAgentClient.ensureActiveWorkspaceOpen) calls this
117
+ //
118
+ // TWO MODES:
119
+ // 1. LOCAL MODE: { "root": "/host/path/to/project" }
120
+ // β†’ Translates host paths to Docker container paths (for Docker local)
121
+ //
122
+ // 2. CLOUD MODE: { "repoUrl": "https://github.com/org/repo", "token": "ghp_xxx", "branch": "main" }
123
+ // β†’ Clones the repo via git (for Cloud Run β€” no local filesystem)
124
+ // β†’ Caches in /tmp/workspaces/{repoName} to avoid re-cloning
125
+ // --------------------------------------------------
126
+
127
+ // Git clone cache directory (survives between requests in same Cloud Run instance)
128
+ const WORKSPACE_CACHE = process.env.WORKSPACE_CACHE || '/tmp/workspaces';
129
+
130
+ /**
131
+ * Execute a shell command and return { stdout, stderr, exitCode }
132
+ */
133
+ function execAsync(cmd, options = {}) {
134
+ const { execSync } = require('child_process');
135
+ try {
136
+ const stdout = execSync(cmd, {
137
+ encoding: 'utf8',
138
+ timeout: options.timeout || 120000,
139
+ stdio: ['pipe', 'pipe', 'pipe'],
140
+ ...options
141
+ });
142
+ return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
143
+ } catch (err) {
144
+ return {
145
+ stdout: err.stdout ? err.stdout.toString().trim() : '',
146
+ stderr: err.stderr ? err.stderr.toString().trim() : err.message,
147
+ exitCode: err.status || 1
148
+ };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Clone or update a git repository.
154
+ * Returns the local path where the repo is checked out.
155
+ */
156
+ async function ensureRepoCloned(repoUrl, token, branch = 'main') {
157
+ // Extract repo name from URL
158
+ let repoName = repoUrl.split('/').pop().replace('.git', '');
159
+ // Include org for uniqueness: org/repo β†’ org_repo
160
+ const urlParts = repoUrl.replace(/\.git$/, '').split('/');
161
+ if (urlParts.length >= 2) {
162
+ const org = urlParts[urlParts.length - 2];
163
+ repoName = `${org}_${repoName}`;
164
+ }
165
+
166
+ const clonePath = path.join(WORKSPACE_CACHE, repoName);
167
+
168
+ // Ensure cache directory exists
169
+ if (!fs.existsSync(WORKSPACE_CACHE)) {
170
+ fs.mkdirSync(WORKSPACE_CACHE, { recursive: true });
171
+ console.log(`πŸ“ Created workspace cache: ${WORKSPACE_CACHE}`);
172
+ }
173
+
174
+ // Build authenticated URL if token provided
175
+ const authUrl = token
176
+ ? repoUrl.replace('https://', `https://x-access-token:${token}@`)
177
+ : repoUrl;
178
+
179
+ if (fs.existsSync(path.join(clonePath, '.git'))) {
180
+ // Already cloned β†’ git fetch + checkout + pull
181
+ console.log(`πŸ“‚ [git] Repo already cloned: ${clonePath}, updating...`);
182
+
183
+ // Fetch latest
184
+ let result = execAsync(`git fetch origin`, { cwd: clonePath });
185
+ if (result.exitCode !== 0) {
186
+ // Try with token in remote URL
187
+ execAsync(`git remote set-url origin "${authUrl}"`, { cwd: clonePath });
188
+ result = execAsync(`git fetch origin`, { cwd: clonePath });
189
+ }
190
+
191
+ // Checkout correct branch
192
+ execAsync(`git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${branch}`, { cwd: clonePath });
193
+
194
+ // Reset to remote (discard any local patches from previous fixes)
195
+ execAsync(`git reset --hard origin/${branch}`, { cwd: clonePath });
196
+
197
+ console.log(`βœ… [git] Updated to latest ${branch}: ${clonePath}`);
198
+ } else {
199
+ // Fresh clone
200
+ console.log(`πŸ“‚ [git] Cloning ${repoUrl} (branch: ${branch})...`);
201
+
202
+ const result = execAsync(
203
+ `git clone --branch ${branch} --single-branch --depth 50 "${authUrl}" "${clonePath}"`,
204
+ { timeout: 180000 } // 3 minutes for large repos
205
+ );
206
+
207
+ if (result.exitCode !== 0) {
208
+ throw new Error(`Git clone failed: ${result.stderr}`);
209
+ }
210
+
211
+ // Configure git user for future commits
212
+ execAsync(`git config user.email "deepdebug-ai@deepdebug.ai"`, { cwd: clonePath });
213
+ execAsync(`git config user.name "DeepDebug AI"`, { cwd: clonePath });
214
+
215
+ // Store the authenticated remote URL for future push
216
+ if (token) {
217
+ execAsync(`git remote set-url origin "${authUrl}"`, { cwd: clonePath });
218
+ }
219
+
220
+ console.log(`βœ… [git] Cloned successfully: ${clonePath}`);
221
+ }
222
+
223
+ return clonePath;
224
+ }
225
+
226
+ router.post("/open", async (req, res) => {
227
+ try {
228
+ const { root, workspacePath: wpPath, repoUrl, token, branch = 'main' } = req.body;
229
+
230
+ // ==========================================
231
+ // CLOUD MODE: Git clone
232
+ // ==========================================
233
+ if (repoUrl) {
234
+ console.log(`πŸ“‚ [/workspace/open] CLOUD MODE β€” cloning ${repoUrl} (branch: ${branch})`);
235
+
236
+ try {
237
+ const clonePath = await ensureRepoCloned(repoUrl, token, branch);
238
+
239
+ // Detect project type
240
+ const files = fs.readdirSync(clonePath);
241
+ let language = "unknown", buildTool = "unknown", marker = null;
242
+ if (files.includes("pom.xml")) { language = "java"; buildTool = "maven"; marker = "pom.xml"; }
243
+ else if (files.includes("build.gradle") || files.includes("build.gradle.kts")) { language = "java"; buildTool = "gradle"; marker = "build.gradle"; }
244
+ else if (files.includes("package.json")) { language = "node"; buildTool = "npm"; marker = "package.json"; }
245
+ else if (files.includes("requirements.txt") || files.includes("setup.py")) { language = "python"; buildTool = "pip"; marker = "requirements.txt"; }
246
+ else if (files.includes("go.mod")) { language = "go"; buildTool = "go"; marker = "go.mod"; }
247
+ else if (files.some(f => f.endsWith(".csproj"))) { language = "csharp"; buildTool = "dotnet"; marker = ".csproj"; }
248
+
249
+ workspacePath = clonePath;
250
+ console.log(`βœ… [/workspace/open] Cloud workspace set to: ${workspacePath} (${language}/${buildTool})`);
251
+
252
+ return res.json({
253
+ ok: true,
254
+ root: workspacePath,
255
+ mode: "cloud-clone",
256
+ meta: { language, buildTool, marker },
257
+ port: null
258
+ });
259
+ } catch (gitErr) {
260
+ console.error(`❌ [/workspace/open] Git clone failed:`, gitErr.message);
261
+ return res.status(500).json({
262
+ ok: false,
263
+ error: `Git clone failed: ${gitErr.message}`,
264
+ hint: "Check that the repository URL and token are correct."
265
+ });
266
+ }
267
+ }
268
+
269
+ // ==========================================
270
+ // LOCAL MODE: Path-based (Docker local)
271
+ // ==========================================
272
+ const requestedPath = root || wpPath;
273
+
274
+ if (!requestedPath) {
275
+ return res.status(400).json({ error: "Missing 'root', 'workspacePath', or 'repoUrl' in body" });
276
+ }
277
+
278
+ // Translate host path to docker path if needed
279
+ let resolvedPath = translateToDockerPath(requestedPath);
280
+ resolvedPath = path.resolve(resolvedPath);
281
+
282
+ console.log(`πŸ“‚ [/workspace/open] LOCAL MODE β€” Requested: ${requestedPath}`);
283
+ console.log(`πŸ“‚ [/workspace/open] Resolved: ${resolvedPath}`);
284
+
285
+ if (!fs.existsSync(resolvedPath) || !fs.lstatSync(resolvedPath).isDirectory()) {
286
+ // If translated path doesn't exist, try the raw path as fallback
287
+ const rawResolved = path.resolve(requestedPath.replace(/\\/g, '/'));
288
+ if (fs.existsSync(rawResolved) && fs.lstatSync(rawResolved).isDirectory()) {
289
+ resolvedPath = rawResolved;
290
+ console.log(`πŸ“‚ [/workspace/open] Fallback to raw path: ${resolvedPath}`);
291
+ } else {
292
+ // List what IS in /workspace to help debug
293
+ let availableDirs = [];
294
+ try {
295
+ if (fs.existsSync(WORKSPACE_MOUNT)) {
296
+ availableDirs = fs.readdirSync(WORKSPACE_MOUNT, { withFileTypes: true })
297
+ .filter(d => d.isDirectory())
298
+ .map(d => d.name);
299
+ }
300
+ } catch (e) {}
301
+
302
+ console.error(`❌ [/workspace/open] Directory not found: ${resolvedPath}`);
303
+ return res.status(400).json({
304
+ error: `Directory not found: ${resolvedPath}`,
305
+ requestedPath, resolvedPath,
306
+ availableProjects: availableDirs,
307
+ hint: "The workspace path might not be mounted in Docker. Check docker -v mount."
308
+ });
309
+ }
310
+ }
311
+
312
+ // Detect project type
313
+ const files = fs.readdirSync(resolvedPath);
314
+ let language = "unknown", buildTool = "unknown", marker = null;
315
+ if (files.includes("pom.xml")) { language = "java"; buildTool = "maven"; marker = "pom.xml"; }
316
+ else if (files.includes("build.gradle") || files.includes("build.gradle.kts")) { language = "java"; buildTool = "gradle"; marker = "build.gradle"; }
317
+ else if (files.includes("package.json")) { language = "node"; buildTool = "npm"; marker = "package.json"; }
318
+ else if (files.includes("requirements.txt") || files.includes("setup.py")) { language = "python"; buildTool = "pip"; marker = "requirements.txt"; }
319
+ else if (files.includes("go.mod")) { language = "go"; buildTool = "go"; marker = "go.mod"; }
320
+
321
+ workspacePath = resolvedPath;
322
+ console.log(`βœ… [/workspace/open] Local workspace set to: ${workspacePath} (${language}/${buildTool})`);
323
+
324
+ return res.json({
325
+ ok: true,
326
+ root: workspacePath,
327
+ mode: "local",
328
+ meta: { language, buildTool, marker },
329
+ port: null
330
+ });
331
+ } catch (err) {
332
+ console.error("❌ [/workspace/open] Error:", err);
333
+ return res.status(500).json({ error: err.message });
334
+ }
335
+ });
336
+
32
337
  // --------------------------------------------------
33
338
  // 🧩 2️⃣ POST /workspace
34
339
  // Atualiza o diretΓ³rio de workspace selecionado pelo usuΓ‘rio
340
+ // (legacy endpoint - kept for backward compatibility)
35
341
  // --------------------------------------------------
36
342
  router.post("/", async (req, res) => {
37
343
  try {
38
344
  console.log(workspacePath)
39
- const { workspacePath: newPath } = req.body;
40
- if (!newPath) {
41
- return res.status(400).json({ error: "Missing workspacePath" });
345
+ const { workspacePath: newPath, root } = req.body;
346
+ const requestedPath = newPath || root;
347
+ if (!requestedPath) {
348
+ return res.status(400).json({ error: "Missing workspacePath or root" });
42
349
  }
43
350
 
44
- // πŸ”Ή Resolve o caminho completo (garante formato absoluto)
45
- const resolvedPath = path.resolve(newPath);
351
+ // Translate host path if in Docker
352
+ let resolvedPath = translateToDockerPath(requestedPath);
353
+ resolvedPath = path.resolve(resolvedPath);
46
354
 
47
355
  if (!fs.existsSync(resolvedPath) || !fs.lstatSync(resolvedPath).isDirectory()) {
48
356
  return res
@@ -78,9 +386,27 @@ router.get("/files", async (req, res) => {
78
386
  for (const entry of entries) {
79
387
  const fullPath = path.join(dir, entry.name);
80
388
  if (entry.isDirectory()) {
81
- walk(fullPath, depth + 1);
82
- } else if (entry.isFile() && entry.name.endsWith(".java")) {
83
- files.push(fullPath);
389
+ if (
390
+ !["node_modules", ".git", "target", "build", ".idea"].includes(
391
+ entry.name
392
+ )
393
+ ) {
394
+ walk(fullPath, depth + 1);
395
+ }
396
+ } else if (
397
+ entry.name.endsWith(".java") ||
398
+ entry.name.endsWith(".js") ||
399
+ entry.name.endsWith(".ts") ||
400
+ entry.name.endsWith(".py") ||
401
+ entry.name.endsWith(".go") ||
402
+ entry.name.endsWith(".cs") ||
403
+ entry.name.endsWith(".xml") ||
404
+ entry.name.endsWith(".yml") ||
405
+ entry.name.endsWith(".yaml") ||
406
+ entry.name.endsWith(".json") ||
407
+ entry.name.endsWith(".properties")
408
+ ) {
409
+ files.push(path.relative(workspacePath, fullPath));
84
410
  }
85
411
  }
86
412
  }
@@ -88,7 +414,7 @@ router.get("/files", async (req, res) => {
88
414
  walk(workspacePath);
89
415
  return res.json({ workspacePath, files });
90
416
  } catch (err) {
91
- console.error("❌ Error listing files:", err);
417
+ console.error("❌ Error listing workspace files:", err);
92
418
  return res.status(500).json({ error: err.message });
93
419
  }
94
420
  });
@@ -144,8 +470,7 @@ router.get("/file", async (req, res) => {
144
470
 
145
471
  // --------------------------------------------------
146
472
  // 🧩 5️⃣ POST /workspace/resolve
147
- // Resolve caminho absoluto do projeto pelo nome da pasta
148
- // (usado quando o dev escolhe o workspace no Setup Wizard)
473
+ // Resolve caminho relativo a absoluto (para exibiΓ§Γ£o no frontend)
149
474
  // --------------------------------------------------
150
475
  router.post("/resolve", async (req, res) => {
151
476
  try {
@@ -200,94 +525,62 @@ router.get("/detect", async (req, res) => {
200
525
  // ==========================================
201
526
 
202
527
  // --------------------------------------------------
203
- // 🧩 7️⃣ POST /workspace/search-file
204
- // Busca recursiva por nome de arquivo no workspace
205
- // Usado pelo InvestigationModeService para encontrar arquivos por hint
528
+ // 🧩 7️⃣ GET /workspace/search-file
529
+ // Busca um arquivo pelo nome dentro do workspace
206
530
  // --------------------------------------------------
207
- router.post("/search-file", async (req, res) => {
531
+ router.get("/search-file", async (req, res) => {
208
532
  try {
209
- const { fileName } = req.body;
210
-
533
+ const { fileName } = req.query;
211
534
  if (!fileName) {
212
535
  return res.status(400).json({ error: "Missing fileName" });
213
536
  }
214
537
 
215
538
  if (!workspacePath || !fs.existsSync(workspacePath)) {
216
- return res.status(400).json({ error: "Workspace not configured or invalid" });
539
+ return res.status(400).json({ error: "Workspace not configured" });
217
540
  }
218
541
 
219
542
  console.log(`πŸ” [search-file] Searching for: ${fileName} in ${workspacePath}`);
220
543
 
221
544
  let foundPath = null;
222
- const matches = [];
223
-
224
- // FunΓ§Γ£o de busca recursiva
225
- function searchRecursive(dir, depth = 0) {
226
- if (depth > 10 || foundPath) return; // Limitar profundidade
545
+ const alternatives = [];
227
546
 
547
+ function searchFile(dir, depth = 0) {
548
+ if (depth > 10 || foundPath) return;
228
549
  try {
229
550
  const entries = fs.readdirSync(dir, { withFileTypes: true });
230
-
231
551
  for (const entry of entries) {
232
- // Ignorar diretΓ³rios comuns
233
- if (entry.name.startsWith('.') ||
234
- entry.name === 'node_modules' ||
235
- entry.name === 'target' ||
236
- entry.name === 'build' ||
237
- entry.name === '.git') {
238
- continue;
239
- }
240
-
552
+ if (foundPath) return;
241
553
  const fullPath = path.join(dir, entry.name);
242
-
243
554
  if (entry.isDirectory()) {
244
- searchRecursive(fullPath, depth + 1);
245
- } else if (entry.isFile()) {
246
- // Match exato ou parcial
247
- if (entry.name === fileName ||
248
- entry.name.toLowerCase() === fileName.toLowerCase()) {
555
+ if (!["node_modules", ".git", "target", "build", ".idea", ".gradle"].includes(entry.name)) {
556
+ searchFile(fullPath, depth + 1);
557
+ }
558
+ } else {
559
+ // Exact match
560
+ if (entry.name === fileName || entry.name === path.basename(fileName)) {
249
561
  foundPath = path.relative(workspacePath, fullPath);
250
- matches.push({
251
- path: foundPath,
252
- matchType: 'exact'
253
- });
254
- } else if (entry.name.includes(fileName.replace('.java', ''))) {
255
- matches.push({
562
+ }
563
+ // Partial match for alternatives
564
+ else if (entry.name.toLowerCase().includes(path.basename(fileName).toLowerCase().replace('.java', '').replace('.js', ''))) {
565
+ alternatives.push({
256
566
  path: path.relative(workspacePath, fullPath),
257
- matchType: 'partial'
567
+ name: entry.name
258
568
  });
259
569
  }
260
570
  }
261
571
  }
262
- } catch (err) {
263
- // Ignorar erros de permissΓ£o
264
- console.warn(`⚠️ Cannot read directory: ${dir}`);
265
- }
572
+ } catch (e) { /* ignore permission errors */ }
266
573
  }
267
574
 
268
- searchRecursive(workspacePath);
575
+ searchFile(workspacePath);
269
576
 
270
577
  if (foundPath) {
271
578
  console.log(`βœ… [search-file] Found: ${foundPath}`);
272
- return res.json({
273
- found: true,
274
- path: foundPath,
275
- matches: matches.slice(0, 10) // Limitar matches
276
- });
277
- }
278
-
279
- // Se nΓ£o encontrou match exato, retornar matches parciais
280
- if (matches.length > 0) {
281
- console.log(`πŸ”Ά [search-file] Partial matches found: ${matches.length}`);
282
- return res.json({
283
- found: true,
284
- path: matches[0].path,
285
- matches: matches.slice(0, 10)
286
- });
579
+ return res.json({ found: true, path: foundPath, alternatives: alternatives.slice(0, 5) });
287
580
  }
288
581
 
289
- console.log(`❌ [search-file] Not found: ${fileName}`);
290
- return res.json({ found: false, matches: [] });
582
+ console.log(`⚠️ [search-file] Not found: ${fileName} (${alternatives.length} alternatives)`);
583
+ return res.json({ found: false, path: null, alternatives: alternatives.slice(0, 10) });
291
584
 
292
585
  } catch (err) {
293
586
  console.error("❌ Error searching file:", err);
@@ -296,189 +589,129 @@ router.post("/search-file", async (req, res) => {
296
589
  });
297
590
 
298
591
  // --------------------------------------------------
299
- // 🧩 8️⃣ GET /workspace/recent-files
300
- // Lista arquivos recentemente modificados
301
- // Usado pelo InvestigationModeService como ΓΊltima estratΓ©gia
592
+ // 🧩 8️⃣ POST /workspace/search
593
+ // Busca por conteΓΊdo dentro dos arquivos do workspace
302
594
  // --------------------------------------------------
303
- router.get("/recent-files", async (req, res) => {
595
+ router.post("/search", async (req, res) => {
304
596
  try {
305
- const limit = parseInt(req.query.limit) || 10;
597
+ const { query, filePattern, maxResults = 20 } = req.body;
598
+ if (!query) {
599
+ return res.status(400).json({ error: "Missing 'query' in body" });
600
+ }
306
601
 
307
602
  if (!workspacePath || !fs.existsSync(workspacePath)) {
308
- return res.status(400).json({ error: "Workspace not configured or invalid" });
603
+ return res.status(400).json({ error: "Workspace not configured" });
309
604
  }
310
605
 
311
- console.log(`πŸ• [recent-files] Getting ${limit} recent files from ${workspacePath}`);
312
-
313
- const allFiles = [];
606
+ console.log(`πŸ” [search] Searching for: "${query}" in ${workspacePath}`);
314
607
 
315
- // Coletar todos os arquivos com data de modificaΓ§Γ£o
316
- function collectFiles(dir, depth = 0) {
317
- if (depth > 5) return; // Limitar profundidade
608
+ const results = [];
609
+ const extensions = ['.java', '.js', '.ts', '.py', '.go', '.cs', '.xml', '.yml', '.yaml', '.json', '.properties', '.html', '.css'];
318
610
 
611
+ function searchContent(dir, depth = 0) {
612
+ if (depth > 8 || results.length >= maxResults) return;
319
613
  try {
320
614
  const entries = fs.readdirSync(dir, { withFileTypes: true });
321
-
322
615
  for (const entry of entries) {
323
- // Ignorar diretΓ³rios comuns
324
- if (entry.name.startsWith('.') ||
325
- entry.name === 'node_modules' ||
326
- entry.name === 'target' ||
327
- entry.name === 'build' ||
328
- entry.name === '.git') {
329
- continue;
330
- }
331
-
616
+ if (results.length >= maxResults) return;
332
617
  const fullPath = path.join(dir, entry.name);
333
-
334
618
  if (entry.isDirectory()) {
335
- collectFiles(fullPath, depth + 1);
336
- } else if (entry.isFile() &&
337
- (entry.name.endsWith('.java') ||
338
- entry.name.endsWith('.js') ||
339
- entry.name.endsWith('.ts'))) {
340
- try {
341
- const stats = fs.statSync(fullPath);
342
- allFiles.push({
343
- path: path.relative(workspacePath, fullPath),
344
- name: entry.name,
345
- modified: stats.mtime,
346
- size: stats.size
347
- });
348
- } catch (e) {
349
- // Ignorar arquivos que nΓ£o podem ser lidos
619
+ if (!["node_modules", ".git", "target", "build", ".idea", ".gradle", "__pycache__"].includes(entry.name)) {
620
+ searchContent(fullPath, depth + 1);
350
621
  }
622
+ } else if (extensions.some(ext => entry.name.endsWith(ext))) {
623
+ if (filePattern && !entry.name.match(new RegExp(filePattern, 'i'))) continue;
624
+ try {
625
+ const content = fs.readFileSync(fullPath, 'utf8');
626
+ const lines = content.split('\n');
627
+ for (let i = 0; i < lines.length; i++) {
628
+ if (lines[i].includes(query)) {
629
+ results.push({
630
+ file: path.relative(workspacePath, fullPath),
631
+ line: i + 1,
632
+ content: lines[i].trim().substring(0, 200),
633
+ context: lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join('\n')
634
+ });
635
+ if (results.length >= maxResults) return;
636
+ }
637
+ }
638
+ } catch (e) { /* ignore read errors */ }
351
639
  }
352
640
  }
353
- } catch (err) {
354
- // Ignorar erros de permissΓ£o
355
- }
641
+ } catch (e) { /* ignore permission errors */ }
356
642
  }
357
643
 
358
- collectFiles(workspacePath);
359
-
360
- // Ordenar por data de modificaΓ§Γ£o (mais recente primeiro)
361
- allFiles.sort((a, b) => b.modified - a.modified);
362
-
363
- // Retornar apenas os N mais recentes
364
- const recentFiles = allFiles.slice(0, limit);
644
+ searchContent(workspacePath);
365
645
 
366
- console.log(`βœ… [recent-files] Returning ${recentFiles.length} files`);
367
-
368
- return res.json({
369
- workspacePath,
370
- total: allFiles.length,
371
- files: recentFiles
372
- });
646
+ console.log(`βœ… [search] Found ${results.length} matches`);
647
+ return res.json({ query, results, total: results.length });
373
648
 
374
649
  } catch (err) {
375
- console.error("❌ Error getting recent files:", err);
650
+ console.error("❌ Error searching:", err);
376
651
  return res.status(500).json({ error: err.message });
377
652
  }
378
653
  });
379
654
 
380
655
  // --------------------------------------------------
381
656
  // 🧩 9️⃣ GET /workspace/scan
382
- // Escaneia workspace completo com metadados
383
- // Usado pelo SmartFileDetectionService para anΓ‘lise com AI
657
+ // Lista TODOS os arquivos do workspace (para list_files tool)
384
658
  // --------------------------------------------------
385
659
  router.get("/scan", async (req, res) => {
386
660
  try {
387
661
  if (!workspacePath || !fs.existsSync(workspacePath)) {
388
- return res.status(400).json({ error: "Workspace not configured or invalid" });
662
+ return res.status(400).json({ error: "Workspace not configured" });
389
663
  }
390
664
 
391
- console.log(`πŸ“‚ [scan] Scanning workspace: ${workspacePath}`);
665
+ const { directory, pattern, maxDepth = 5 } = req.query;
666
+ const scanRoot = directory ? path.join(workspacePath, directory) : workspacePath;
392
667
 
393
- const files = [];
394
- const languages = new Set();
395
- let totalFiles = 0;
396
-
397
- // Mapeamento de extensΓ£o para linguagem
398
- const extensionMap = {
399
- '.java': 'Java',
400
- '.js': 'JavaScript',
401
- '.ts': 'TypeScript',
402
- '.py': 'Python',
403
- '.go': 'Go',
404
- '.rs': 'Rust',
405
- '.kt': 'Kotlin',
406
- '.scala': 'Scala',
407
- '.rb': 'Ruby',
408
- '.php': 'PHP',
409
- '.cs': 'C#',
410
- '.cpp': 'C++',
411
- '.c': 'C'
412
- };
668
+ if (!fs.existsSync(scanRoot)) {
669
+ return res.status(404).json({ error: `Directory not found: ${directory || '/'}` });
670
+ }
671
+
672
+ console.log(`πŸ“‚ [scan] Scanning: ${scanRoot} (maxDepth: ${maxDepth})`);
413
673
 
414
- function scanRecursive(dir, depth = 0) {
415
- if (depth > 6) return; // Limitar profundidade
674
+ const files = [];
675
+ const dirs = [];
416
676
 
677
+ function scan(dir, depth = 0) {
678
+ if (depth > parseInt(maxDepth)) return;
417
679
  try {
418
680
  const entries = fs.readdirSync(dir, { withFileTypes: true });
419
-
420
681
  for (const entry of entries) {
421
- // Ignorar diretΓ³rios comuns
422
- if (entry.name.startsWith('.') ||
423
- entry.name === 'node_modules' ||
424
- entry.name === 'target' ||
425
- entry.name === 'build' ||
426
- entry.name === '.git' ||
427
- entry.name === '__pycache__') {
428
- continue;
429
- }
430
-
431
682
  const fullPath = path.join(dir, entry.name);
432
-
683
+ const relativePath = path.relative(workspacePath, fullPath);
433
684
  if (entry.isDirectory()) {
434
- scanRecursive(fullPath, depth + 1);
435
- } else if (entry.isFile()) {
436
- const ext = path.extname(entry.name).toLowerCase();
437
-
438
- // Verificar se Γ© arquivo de cΓ³digo
439
- if (extensionMap[ext]) {
440
- languages.add(extensionMap[ext]);
441
- totalFiles++;
442
-
443
- // Limitar arquivos retornados para nΓ£o estourar memΓ³ria
444
- if (files.length < 200) {
445
- try {
446
- const stats = fs.statSync(fullPath);
447
- files.push({
448
- path: path.relative(workspacePath, fullPath),
449
- name: entry.name,
450
- language: extensionMap[ext],
451
- size: stats.size,
452
- modified: stats.mtime
453
- });
454
- } catch (e) {
455
- // Ignorar
456
- }
685
+ if (!["node_modules", ".git", "target", "build", ".idea", ".gradle", "__pycache__", ".next"].includes(entry.name)) {
686
+ dirs.push(relativePath);
687
+ scan(fullPath, depth + 1);
688
+ }
689
+ } else {
690
+ if (pattern) {
691
+ if (entry.name.match(new RegExp(pattern, 'i'))) {
692
+ files.push(relativePath);
457
693
  }
694
+ } else {
695
+ files.push(relativePath);
458
696
  }
459
697
  }
460
698
  }
461
- } catch (err) {
462
- // Ignorar erros de permissΓ£o
463
- }
699
+ } catch (e) { /* ignore */ }
464
700
  }
465
701
 
466
- scanRecursive(workspacePath);
467
-
468
- console.log(`βœ… [scan] Found ${totalFiles} files in ${languages.size} languages`);
702
+ scan(scanRoot);
469
703
 
470
704
  return res.json({
705
+ root: directory || '/',
471
706
  workspacePath,
472
- files,
473
- metadata: {
474
- totalFiles,
475
- languages: Array.from(languages),
476
- scannedAt: new Date().toISOString()
477
- }
707
+ files: files.slice(0, 500),
708
+ directories: dirs,
709
+ totalFiles: files.length,
710
+ truncated: files.length > 500
478
711
  });
479
712
 
480
713
  } catch (err) {
481
- console.error("❌ Error scanning workspace:", err);
714
+ console.error("❌ Error scanning:", err);
482
715
  return res.status(500).json({ error: err.message });
483
716
  }
484
717
  });
@@ -508,7 +741,9 @@ router.get("/file-content", async (req, res) => {
508
741
  if (!fs.existsSync(fullPath)) {
509
742
  return res.status(404).json({
510
743
  error: "File not found",
511
- path: requestedPath
744
+ path: requestedPath,
745
+ resolvedPath: fullPath,
746
+ workspacePath,
512
747
  });
513
748
  }
514
749
 
package/src/server.js CHANGED
@@ -717,8 +717,92 @@ app.get("/health", (_req, res) => {
717
717
 
718
718
  /** Define/abre o workspace local */
719
719
  app.post("/workspace/open", async (req, res) => {
720
- const { root, workspaceId } = req.body || {};
721
- if (!root) return res.status(400).json({ error: "root is required" });
720
+ const { root, workspaceId, repoUrl, token, branch = "main" } = req.body || {};
721
+
722
+ // ==========================================
723
+ // CLOUD MODE: Git clone (when Gateway sends repoUrl)
724
+ // ==========================================
725
+ if (repoUrl) {
726
+ console.log(`πŸ“‚ [/workspace/open] CLOUD MODE β€” cloning ${repoUrl} (branch: ${branch})`);
727
+
728
+ try {
729
+ // Extract repo name: https://github.com/org/repo β†’ org_repo
730
+ let repoName = repoUrl.split('/').pop().replace('.git', '');
731
+ const urlParts = repoUrl.replace(/\.git$/, '').split('/');
732
+ if (urlParts.length >= 2) {
733
+ const org = urlParts[urlParts.length - 2];
734
+ repoName = `${org}_${repoName}`;
735
+ }
736
+
737
+ const cacheDir = process.env.WORKSPACE_CACHE || '/tmp/workspaces';
738
+ const clonePath = path.join(cacheDir, repoName);
739
+
740
+ // Ensure cache directory exists
741
+ await fsPromises.mkdir(cacheDir, { recursive: true });
742
+
743
+ // Build authenticated URL
744
+ const authUrl = token
745
+ ? repoUrl.replace('https://', `https://x-access-token:${token}@`)
746
+ : repoUrl;
747
+
748
+ const gitDir = path.join(clonePath, '.git');
749
+ const alreadyCloned = await exists(gitDir);
750
+
751
+ if (alreadyCloned) {
752
+ console.log(`πŸ”„ [cloud] Repo exists: ${clonePath}, updating...`);
753
+ // Update remote URL with fresh token
754
+ await execAsync(`git remote set-url origin "${authUrl}"`, { cwd: clonePath }).catch(() => {});
755
+ await execAsync(`git fetch origin`, { cwd: clonePath, timeout: 120000 });
756
+ // Checkout correct branch and reset to remote (discard previous patches)
757
+ await execAsync(`git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${branch}`, { cwd: clonePath }).catch(() => {});
758
+ await execAsync(`git reset --hard origin/${branch}`, { cwd: clonePath });
759
+ console.log(`βœ… [cloud] Updated to latest ${branch}`);
760
+ } else {
761
+ console.log(`πŸ”½ [cloud] Cloning ${repoUrl} (branch: ${branch})...`);
762
+ await execAsync(
763
+ `git clone --branch ${branch} --single-branch --depth 50 "${authUrl}" "${clonePath}"`,
764
+ { timeout: 300000 }
765
+ );
766
+ // Configure git user for future commits
767
+ await execAsync(`git config user.email "deepdebug-ai@deepdebug.ai"`, { cwd: clonePath });
768
+ await execAsync(`git config user.name "DeepDebug AI"`, { cwd: clonePath });
769
+ console.log(`βœ… [cloud] Cloned successfully: ${clonePath}`);
770
+ }
771
+
772
+ // Set as active workspace
773
+ WORKSPACE_ROOT = clonePath;
774
+ const wsId = workspaceId || "default";
775
+ try { await wsManager.open(wsId, clonePath); } catch (err) {
776
+ console.warn(`⚠️ WorkspaceManager.open failed (non-fatal): ${err.message}`);
777
+ }
778
+
779
+ const meta = await detectProject(clonePath);
780
+ const port = await detectPort(clonePath);
781
+
782
+ return res.json({
783
+ ok: true,
784
+ root: WORKSPACE_ROOT,
785
+ workspaceId: wsId,
786
+ mode: "cloud-clone",
787
+ meta,
788
+ port
789
+ });
790
+
791
+ } catch (gitErr) {
792
+ console.error(`❌ [cloud] Git clone failed:`, gitErr.message);
793
+ const hint = gitErr.message.includes('Authentication') || gitErr.message.includes('could not read')
794
+ ? "Authentication failed. Check token and repo URL."
795
+ : gitErr.message.includes('not found') || gitErr.message.includes('does not exist')
796
+ ? "Repository not found. Check the URL."
797
+ : "Clone failed. Ensure the URL is accessible.";
798
+ return res.status(500).json({ ok: false, error: gitErr.message, hint });
799
+ }
800
+ }
801
+
802
+ // ==========================================
803
+ // LOCAL MODE: Path-based (existing behavior)
804
+ // ==========================================
805
+ if (!root) return res.status(400).json({ error: "root or repoUrl is required" });
722
806
  const abs = path.resolve(root);
723
807
  if (!(await exists(abs))) return res.status(404).json({ error: "path not found" });
724
808
 
@@ -734,7 +818,7 @@ app.post("/workspace/open", async (req, res) => {
734
818
 
735
819
  const meta = await detectProject(WORKSPACE_ROOT);
736
820
  const port = await detectPort(WORKSPACE_ROOT);
737
- res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, meta, port });
821
+ res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, mode: "local", meta, port });
738
822
  });
739
823
 
740
824
  /**