deepdebug-local-agent 1.0.11 → 1.0.13

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.11",
3
+ "version": "1.0.13",
4
4
  "description": "DeepDebug Local Agent - AI-powered code debugging assistant",
5
5
  "private": false,
6
6
  "type": "module",
@@ -780,7 +780,6 @@ export class GitHubProvider extends BaseGitProvider {
780
780
  // - git@github.com:owner/repo.git
781
781
 
782
782
  let match = url.match(/github\.com[/:]([\w-]+)\/([\w-]+?)(?:\.git)?$/);
783
-
784
783
  if (match) {
785
784
  return {
786
785
  owner: match[1],
@@ -792,4 +791,4 @@ export class GitHubProvider extends BaseGitProvider {
792
791
  }
793
792
  }
794
793
 
795
- export default GitHubProvider;
794
+ export default GitHubProvider
package/src/server.js CHANGED
@@ -583,6 +583,14 @@ if (WORKSPACE_ROOT) {
583
583
  let DETECTED_SERVICES = [];
584
584
  const processManager = new ProcessManager();
585
585
  const wsManager = new WorkspaceManager();
586
+
587
+ function resolveWorkspaceRoot(req) {
588
+ const wsId = req.headers['x-workspace-id']
589
+ || (req.body && req.body.workspaceId)
590
+ || req.query.workspaceId;
591
+ if (wsId && wsManager.isOpen(wsId)) return wsManager.getRoot(wsId);
592
+ return WORKSPACE_ROOT;
593
+ }
586
594
  const MCP_PORT = process.env.MCP_PORT || 5056;
587
595
 
588
596
  // 🧠 Inicializar AI Vibe Coding Engine
@@ -702,7 +710,7 @@ function updateServiceStatus(serviceId, status) {
702
710
  }
703
711
 
704
712
  /** Health */
705
- app.get("/health", (_req, res) => {
713
+ app.get("/health", (req, res) => {
706
714
  res.json({
707
715
  status: "ok",
708
716
  workspace: WORKSPACE_ROOT || null,
@@ -741,9 +749,24 @@ app.post("/workspace/open", async (req, res) => {
741
749
  await fsPromises.mkdir(cacheDir, { recursive: true });
742
750
 
743
751
  // Build authenticated URL
744
- const authUrl = token
745
- ? repoUrl.replace('https://', `https://x-access-token:${token}@`)
746
- : repoUrl;
752
+ // Token formats:
753
+ // GitHub: TOKEN (uses x-access-token:TOKEN@)
754
+ // Bitbucket: x-token-auth:TOKEN or username:appPassword
755
+ // GitLab: oauth2:TOKEN
756
+ let authUrl;
757
+ if (token) {
758
+ // Strip any existing username@ from URL first
759
+ let cleanUrl = repoUrl.replace(/https:\/\/[^@]+@/, 'https://');
760
+ if (token.includes(':')) {
761
+ // Already in user:pass format (Bitbucket/GitLab)
762
+ authUrl = cleanUrl.replace('https://', `https://${token}@`);
763
+ } else {
764
+ // Plain token — GitHub style
765
+ authUrl = cleanUrl.replace('https://', `https://x-access-token:${token}@`);
766
+ }
767
+ } else {
768
+ authUrl = repoUrl;
769
+ }
747
770
 
748
771
  const gitDir = path.join(clonePath, '.git');
749
772
  const alreadyCloned = await exists(gitDir);
@@ -770,7 +793,10 @@ app.post("/workspace/open", async (req, res) => {
770
793
  }
771
794
 
772
795
  // Set as active workspace
773
- WORKSPACE_ROOT = clonePath;
796
+ // TENANT ISOLATION: Only set global for default
797
+ if (!workspaceId || workspaceId === "default") {
798
+ WORKSPACE_ROOT = clonePath;
799
+ }
774
800
  const wsId = workspaceId || "default";
775
801
  try { await wsManager.open(wsId, clonePath); } catch (err) {
776
802
  console.warn(`⚠️ WorkspaceManager.open failed (non-fatal): ${err.message}`);
@@ -806,7 +832,10 @@ app.post("/workspace/open", async (req, res) => {
806
832
  const abs = path.resolve(root);
807
833
  if (!(await exists(abs))) return res.status(404).json({ error: "path not found" });
808
834
 
809
- WORKSPACE_ROOT = abs;
835
+ // TENANT ISOLATION: Only set global for default
836
+ if (wsId === "default") {
837
+ WORKSPACE_ROOT = abs;
838
+ }
810
839
 
811
840
  // Registar no WorkspaceManager (multi-workspace support)
812
841
  const wsId = workspaceId || "default";
@@ -887,19 +916,21 @@ app.post("/workspace/clone", async (req, res) => {
887
916
  });
888
917
 
889
918
  /** Info do workspace */
890
- app.get("/workspace/info", async (_req, res) => {
891
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
892
- const meta = await detectProject(WORKSPACE_ROOT);
893
- const port = await detectPort(WORKSPACE_ROOT);
894
- res.json({ root: WORKSPACE_ROOT, meta, port });
919
+ app.get("/workspace/info", async (req, res) => {
920
+ const wsRoot = resolveWorkspaceRoot(req);
921
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
922
+ const meta = await detectProject(wsRoot);
923
+ const port = await detectPort(wsRoot);
924
+ res.json({ root: wsRoot, meta, port });
895
925
  });
896
926
 
897
927
  /** Scan completo do workspace */
898
- app.get("/workspace/scan", async (_req, res) => {
899
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
928
+ app.get("/workspace/scan", async (req, res) => {
929
+ const wsRoot = resolveWorkspaceRoot(req);
930
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
900
931
 
901
932
  try {
902
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
933
+ const scanner = new WorkspaceScanner(wsRoot);
903
934
  const structure = await scanner.scan();
904
935
  res.json(structure);
905
936
  } catch (err) {
@@ -908,17 +939,18 @@ app.get("/workspace/scan", async (_req, res) => {
908
939
  });
909
940
 
910
941
  /** Análise completa: language + framework */
911
- app.get("/workspace/analyze", async (_req, res) => {
912
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
942
+ app.get("/workspace/analyze", async (req, res) => {
943
+ const wsRoot = resolveWorkspaceRoot(req);
944
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
913
945
 
914
946
  try {
915
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
947
+ const scanner = new WorkspaceScanner(wsRoot);
916
948
  const structure = await scanner.scan();
917
949
 
918
950
  const languageDetector = new LanguageDetector(structure.files);
919
951
  const languageInfo = languageDetector.detect();
920
952
 
921
- const fileReader = new FileReader(WORKSPACE_ROOT);
953
+ const fileReader = new FileReader(wsRoot);
922
954
  const frameworkDetector = new FrameworkDetector(
923
955
  languageInfo.primary,
924
956
  structure.files,
@@ -927,7 +959,7 @@ app.get("/workspace/analyze", async (_req, res) => {
927
959
  const frameworkInfo = await frameworkDetector.detect();
928
960
 
929
961
  res.json({
930
- workspace: WORKSPACE_ROOT,
962
+ workspace: wsRoot,
931
963
  language: languageInfo,
932
964
  framework: frameworkInfo,
933
965
  stats: structure.metadata,
@@ -940,13 +972,14 @@ app.get("/workspace/analyze", async (_req, res) => {
940
972
 
941
973
  /** Lê conteúdo de arquivo específico */
942
974
  app.get("/workspace/file-content", async (req, res) => {
943
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
975
+ const wsRoot = resolveWorkspaceRoot(req);
976
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
944
977
 
945
978
  const { path: relativePath } = req.query;
946
979
  if (!relativePath) return res.status(400).json({ error: "path query param required" });
947
980
 
948
981
  try {
949
- const reader = new FileReader(WORKSPACE_ROOT);
982
+ const reader = new FileReader(wsRoot);
950
983
  const file = await reader.read(relativePath);
951
984
  res.json(file);
952
985
  } catch (err) {
@@ -956,7 +989,8 @@ app.get("/workspace/file-content", async (req, res) => {
956
989
 
957
990
  /** Lê múltiplos arquivos */
958
991
  app.post("/workspace/batch-read", async (req, res) => {
959
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
992
+ const wsRoot = resolveWorkspaceRoot(req);
993
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
960
994
 
961
995
  const { paths } = req.body || {};
962
996
  if (!paths || !Array.isArray(paths)) {
@@ -964,7 +998,7 @@ app.post("/workspace/batch-read", async (req, res) => {
964
998
  }
965
999
 
966
1000
  try {
967
- const reader = new FileReader(WORKSPACE_ROOT);
1001
+ const reader = new FileReader(wsRoot);
968
1002
  const files = await reader.readMultiple(paths);
969
1003
  res.json(files);
970
1004
  } catch (err) {
@@ -981,7 +1015,8 @@ app.post("/workspace/batch-read", async (req, res) => {
981
1015
  * Checks if a file exists in the workspace
982
1016
  */
983
1017
  app.get("/workspace/file-exists", async (req, res) => {
984
- if (!WORKSPACE_ROOT) {
1018
+ const wsRoot = resolveWorkspaceRoot(req);
1019
+ if (!wsRoot) {
985
1020
  return res.status(400).json({ error: "workspace not set" });
986
1021
  }
987
1022
 
@@ -991,7 +1026,7 @@ app.get("/workspace/file-exists", async (req, res) => {
991
1026
  }
992
1027
 
993
1028
  try {
994
- const fullPath = path.join(WORKSPACE_ROOT, relativePath);
1029
+ const fullPath = path.join(wsRoot, relativePath);
995
1030
  const fileExists = await exists(fullPath);
996
1031
 
997
1032
  console.log(`🔍 [file-exists] ${relativePath} -> ${fileExists ? 'EXISTS' : 'NOT FOUND'}`);
@@ -1013,7 +1048,8 @@ app.get("/workspace/file-exists", async (req, res) => {
1013
1048
  * Validates multiple file paths at once
1014
1049
  */
1015
1050
  app.post("/workspace/validate-paths", async (req, res) => {
1016
- if (!WORKSPACE_ROOT) {
1051
+ const wsRoot = resolveWorkspaceRoot(req);
1052
+ if (!wsRoot) {
1017
1053
  return res.status(400).json({ error: "workspace not set" });
1018
1054
  }
1019
1055
 
@@ -1025,7 +1061,7 @@ app.post("/workspace/validate-paths", async (req, res) => {
1025
1061
  try {
1026
1062
  const results = await Promise.all(
1027
1063
  pathList.map(async (relativePath) => {
1028
- const fullPath = path.join(WORKSPACE_ROOT, relativePath);
1064
+ const fullPath = path.join(wsRoot, relativePath);
1029
1065
  const fileExists = await exists(fullPath);
1030
1066
  return { path: relativePath, exists: fileExists };
1031
1067
  })
@@ -1056,7 +1092,8 @@ app.post("/workspace/validate-paths", async (req, res) => {
1056
1092
  * FIXED: Handle both string arrays and object arrays from listRecursive
1057
1093
  */
1058
1094
  app.post("/workspace/search-file", async (req, res) => {
1059
- if (!WORKSPACE_ROOT) {
1095
+ const wsRoot = resolveWorkspaceRoot(req);
1096
+ if (!wsRoot) {
1060
1097
  return res.status(400).json({ error: "workspace not set" });
1061
1098
  }
1062
1099
 
@@ -1068,7 +1105,7 @@ app.post("/workspace/search-file", async (req, res) => {
1068
1105
  try {
1069
1106
  console.log(`🔍 [search-file] Searching for: ${fileName}`);
1070
1107
 
1071
- const rawFiles = await listRecursive(WORKSPACE_ROOT, {
1108
+ const rawFiles = await listRecursive(wsRoot, {
1072
1109
  maxDepth: 15,
1073
1110
  includeHidden: false,
1074
1111
  extensions: null
@@ -1132,7 +1169,8 @@ app.post("/workspace/search-file", async (req, res) => {
1132
1169
  * FIXED: Handle both string arrays and object arrays from listRecursive
1133
1170
  */
1134
1171
  app.post("/workspace/search-by-content", async (req, res) => {
1135
- if (!WORKSPACE_ROOT) {
1172
+ const wsRoot = resolveWorkspaceRoot(req);
1173
+ if (!wsRoot) {
1136
1174
  return res.status(400).json({ error: "workspace not set" });
1137
1175
  }
1138
1176
 
@@ -1145,7 +1183,7 @@ app.post("/workspace/search-by-content", async (req, res) => {
1145
1183
  try {
1146
1184
  console.log(`🔍 [search-by-content] Searching for terms: ${terms.join(', ')}`);
1147
1185
 
1148
- const rawFiles = await listRecursive(WORKSPACE_ROOT, {
1186
+ const rawFiles = await listRecursive(wsRoot, {
1149
1187
  maxDepth: 15,
1150
1188
  includeHidden: false
1151
1189
  });
@@ -1171,7 +1209,7 @@ app.post("/workspace/search-by-content", async (req, res) => {
1171
1209
  if (results.length >= maxResults * 2) break;
1172
1210
 
1173
1211
  try {
1174
- const fullPath = path.join(WORKSPACE_ROOT, filePath);
1212
+ const fullPath = path.join(wsRoot, filePath);
1175
1213
  const content = await readFile(fullPath, 'utf8');
1176
1214
  const lines = content.split('\n');
1177
1215
 
@@ -1229,7 +1267,8 @@ app.post("/workspace/search-by-content", async (req, res) => {
1229
1267
  * FIXED: Handle both string arrays and object arrays from listRecursive
1230
1268
  */
1231
1269
  app.post("/workspace/find-field-definition", async (req, res) => {
1232
- if (!WORKSPACE_ROOT) {
1270
+ const wsRoot = resolveWorkspaceRoot(req);
1271
+ if (!wsRoot) {
1233
1272
  return res.status(400).json({ error: "workspace not set" });
1234
1273
  }
1235
1274
 
@@ -1242,7 +1281,7 @@ app.post("/workspace/find-field-definition", async (req, res) => {
1242
1281
  try {
1243
1282
  console.log(`🔍 [find-field] Searching for field: ${fieldName}`);
1244
1283
 
1245
- const rawFiles = await listRecursive(WORKSPACE_ROOT, {
1284
+ const rawFiles = await listRecursive(wsRoot, {
1246
1285
  maxDepth: 15,
1247
1286
  includeHidden: false
1248
1287
  });
@@ -1266,7 +1305,7 @@ app.post("/workspace/find-field-definition", async (req, res) => {
1266
1305
 
1267
1306
  for (const filePath of targetFiles) {
1268
1307
  try {
1269
- const fullPath = path.join(WORKSPACE_ROOT, filePath);
1308
+ const fullPath = path.join(wsRoot, filePath);
1270
1309
  const content = await readFile(fullPath, 'utf8');
1271
1310
  const lines = content.split('\n');
1272
1311
 
@@ -1339,17 +1378,18 @@ app.post("/workspace/find-field-definition", async (req, res) => {
1339
1378
  // ============================================
1340
1379
 
1341
1380
  /** Detecta serviços no workspace */
1342
- app.get("/workspace/services/detect", async (_req, res) => {
1343
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1381
+ app.get("/workspace/services/detect", async (req, res) => {
1382
+ const wsRoot = resolveWorkspaceRoot(req);
1383
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1344
1384
 
1345
1385
  try {
1346
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
1386
+ const scanner = new WorkspaceScanner(wsRoot);
1347
1387
  const structure = await scanner.scan();
1348
1388
 
1349
1389
  const languageDetector = new LanguageDetector(structure.files);
1350
1390
  const languageInfo = languageDetector.detect();
1351
1391
 
1352
- const fileReader = new FileReader(WORKSPACE_ROOT);
1392
+ const fileReader = new FileReader(wsRoot);
1353
1393
  const frameworkDetector = new FrameworkDetector(
1354
1394
  languageInfo.primary,
1355
1395
  structure.files,
@@ -1358,7 +1398,7 @@ app.get("/workspace/services/detect", async (_req, res) => {
1358
1398
  const frameworkInfo = await frameworkDetector.detect();
1359
1399
 
1360
1400
  const serviceDetector = new ServiceDetector(
1361
- WORKSPACE_ROOT,
1401
+ wsRoot,
1362
1402
  languageInfo,
1363
1403
  frameworkInfo
1364
1404
  );
@@ -1375,8 +1415,9 @@ app.get("/workspace/services/detect", async (_req, res) => {
1375
1415
  });
1376
1416
 
1377
1417
  /** Lista todos os serviços */
1378
- app.get("/workspace/services", (_req, res) => {
1379
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1418
+ app.get("/workspace/services", (req, res) => {
1419
+ const wsRoot = resolveWorkspaceRoot(req);
1420
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1380
1421
 
1381
1422
  // Atualizar status dos serviços com info do ProcessManager
1382
1423
  const servicesWithStatus = DETECTED_SERVICES.map(service => {
@@ -1392,7 +1433,8 @@ app.get("/workspace/services", (_req, res) => {
1392
1433
 
1393
1434
  /** Inicia um serviço */
1394
1435
  app.post("/workspace/services/:serviceId/start", async (req, res) => {
1395
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1436
+ const wsRoot = resolveWorkspaceRoot(req);
1437
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1396
1438
 
1397
1439
  const { serviceId } = req.params;
1398
1440
  const service = DETECTED_SERVICES.find(s => s.id === serviceId);
@@ -1418,7 +1460,8 @@ app.post("/workspace/services/:serviceId/start", async (req, res) => {
1418
1460
 
1419
1461
  /** Para um serviço */
1420
1462
  app.post("/workspace/services/:serviceId/stop", async (req, res) => {
1421
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1463
+ const wsRoot = resolveWorkspaceRoot(req);
1464
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1422
1465
 
1423
1466
  const { serviceId } = req.params;
1424
1467
 
@@ -1432,7 +1475,8 @@ app.post("/workspace/services/:serviceId/stop", async (req, res) => {
1432
1475
 
1433
1476
  /** Retorna status de um serviço */
1434
1477
  app.get("/workspace/services/:serviceId/status", (req, res) => {
1435
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1478
+ const wsRoot = resolveWorkspaceRoot(req);
1479
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1436
1480
 
1437
1481
  const { serviceId } = req.params;
1438
1482
  const status = processManager.getStatus(serviceId);
@@ -1442,7 +1486,8 @@ app.get("/workspace/services/:serviceId/status", (req, res) => {
1442
1486
 
1443
1487
  /** Retorna logs de um serviço */
1444
1488
  app.get("/workspace/services/:serviceId/logs", (req, res) => {
1445
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1489
+ const wsRoot = resolveWorkspaceRoot(req);
1490
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1446
1491
 
1447
1492
  const { serviceId } = req.params;
1448
1493
  const { limit = 100 } = req.query;
@@ -1457,7 +1502,8 @@ app.get("/workspace/services/:serviceId/logs", (req, res) => {
1457
1502
 
1458
1503
  /** Streaming de logs em tempo real (SSE) */
1459
1504
  app.get("/workspace/services/:serviceId/logs/stream", (req, res) => {
1460
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1505
+ const wsRoot = resolveWorkspaceRoot(req);
1506
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1461
1507
 
1462
1508
  const { serviceId } = req.params;
1463
1509
 
@@ -1492,18 +1538,20 @@ app.get("/workspace/services/:serviceId/logs/stream", (req, res) => {
1492
1538
  // ============================================
1493
1539
 
1494
1540
  app.get("/workspace/files", async (req, res) => {
1495
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1541
+ const wsRoot = resolveWorkspaceRoot(req);
1542
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1496
1543
  const { max = 5000 } = req.query;
1497
- const tree = await listRecursive(WORKSPACE_ROOT, { maxFiles: Number(max) });
1498
- res.json({ root: WORKSPACE_ROOT, count: tree.length, tree });
1544
+ const tree = await listRecursive(wsRoot, { maxFiles: Number(max) });
1545
+ res.json({ root: wsRoot, count: tree.length, tree });
1499
1546
  });
1500
1547
 
1501
1548
  app.get("/workspace/file", async (req, res) => {
1502
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1549
+ const wsRoot = resolveWorkspaceRoot(req);
1550
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1503
1551
  const rel = req.query.path;
1504
1552
  if (!rel) return res.status(400).json({ error: "path is required" });
1505
1553
  try {
1506
- const content = await readText(WORKSPACE_ROOT, rel);
1554
+ const content = await readText(wsRoot, rel);
1507
1555
  res.json({ path: rel, content });
1508
1556
  } catch (e) {
1509
1557
  res.status(404).json({ error: "file not found", details: String(e) });
@@ -1511,11 +1559,12 @@ app.get("/workspace/file", async (req, res) => {
1511
1559
  });
1512
1560
 
1513
1561
  app.post("/workspace/write", async (req, res) => {
1514
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1562
+ const wsRoot = resolveWorkspaceRoot(req);
1563
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1515
1564
  const { path: rel, content } = req.body || {};
1516
1565
  if (!rel) return res.status(400).json({ error: "path is required" });
1517
1566
  try {
1518
- await writeFile(path.join(WORKSPACE_ROOT, rel), content ?? "", "utf8");
1567
+ await writeFile(path.join(wsRoot, rel), content ?? "", "utf8");
1519
1568
  res.json({ ok: true, path: rel, bytes: Buffer.byteLength(content ?? "", "utf8") });
1520
1569
  } catch (e) {
1521
1570
  res.status(400).json({ error: "write failed", details: String(e) });
@@ -1526,13 +1575,14 @@ app.post("/workspace/write", async (req, res) => {
1526
1575
  // ✅ CORRECTED: /workspace/patch endpoint
1527
1576
  // ============================================
1528
1577
  app.post("/workspace/patch", async (req, res) => {
1529
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1578
+ const wsRoot = resolveWorkspaceRoot(req);
1579
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1530
1580
  const { diff, incidentId } = req.body || {};
1531
1581
  if (!diff) return res.status(400).json({ error: "diff is required" });
1532
1582
 
1533
1583
  try {
1534
1584
  console.log(`📝 Applying patch for incident: ${incidentId || 'unknown'}`);
1535
- const out = await applyUnifiedDiff(WORKSPACE_ROOT, diff);
1585
+ const out = await applyUnifiedDiff(wsRoot, diff);
1536
1586
 
1537
1587
  // ✅ CRITICAL FIX: Format response as expected by Gateway
1538
1588
  const response = {
@@ -1565,18 +1615,20 @@ app.post("/workspace/patch", async (req, res) => {
1565
1615
  }
1566
1616
  });
1567
1617
 
1568
- app.post("/workspace/test", async (_req, res) => {
1569
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1570
- const meta = await detectProject(WORKSPACE_ROOT);
1571
- const result = await compileAndTest({ language: meta.language, buildTool: meta.buildTool, cwd: WORKSPACE_ROOT });
1572
- res.json({ root: WORKSPACE_ROOT, meta, result });
1618
+ app.post("/workspace/test", async (req, res) => {
1619
+ const wsRoot = resolveWorkspaceRoot(req);
1620
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1621
+ const meta = await detectProject(wsRoot);
1622
+ const result = await compileAndTest({ language: meta.language, buildTool: meta.buildTool, cwd: wsRoot });
1623
+ res.json({ root: wsRoot, meta, result });
1573
1624
  });
1574
1625
 
1575
1626
  app.post("/workspace/run", async (req, res) => {
1576
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1627
+ const wsRoot = resolveWorkspaceRoot(req);
1628
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1577
1629
  const { cmd, args = [] } = req.body || {};
1578
1630
  if (!cmd) return res.status(400).json({ error: "cmd is required" });
1579
- const out = await run(cmd, args, WORKSPACE_ROOT, 5 * 60 * 1000);
1631
+ const out = await run(cmd, args, wsRoot, 5 * 60 * 1000);
1580
1632
  res.json(out);
1581
1633
  });
1582
1634
 
@@ -1639,17 +1691,18 @@ app.get("/workspace/test-local/state", async (req, res) => {
1639
1691
  * Compiles the project without starting server
1640
1692
  */
1641
1693
  app.post("/workspace/test-local/compile", async (req, res) => {
1642
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1694
+ const wsRoot = resolveWorkspaceRoot(req);
1695
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1643
1696
 
1644
1697
  try {
1645
1698
  console.log("🔨 [TEST-LOCAL] Starting compilation...");
1646
1699
  TEST_LOCAL_STATE.status = "compiling";
1647
1700
 
1648
- const meta = await detectProject(WORKSPACE_ROOT);
1701
+ const meta = await detectProject(wsRoot);
1649
1702
  const compileResult = await compileAndTest({
1650
1703
  language: meta.language,
1651
1704
  buildTool: meta.buildTool,
1652
- cwd: WORKSPACE_ROOT,
1705
+ cwd: wsRoot,
1653
1706
  skipTests: true
1654
1707
  });
1655
1708
 
@@ -1700,7 +1753,8 @@ app.post("/workspace/test-local/compile", async (req, res) => {
1700
1753
  * 🧠 UPDATED: Now uses AI Vibe Coding Engine for auto-healing
1701
1754
  */
1702
1755
  app.post("/workspace/test-local/start", async (req, res) => {
1703
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1756
+ const wsRoot = resolveWorkspaceRoot(req);
1757
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1704
1758
 
1705
1759
  const { port } = req.body || {};
1706
1760
 
@@ -1708,14 +1762,14 @@ app.post("/workspace/test-local/start", async (req, res) => {
1708
1762
  console.log(`\n🚀 [TEST-LOCAL] Starting server (SIMPLE MODE)...`);
1709
1763
  TEST_LOCAL_STATE.status = "starting";
1710
1764
 
1711
- const meta = await detectProject(WORKSPACE_ROOT);
1765
+ const meta = await detectProject(wsRoot);
1712
1766
 
1713
1767
  // Detect port if not provided
1714
1768
  let serverPort = port || 8080;
1715
1769
 
1716
1770
  // Para Java, encontrar o JAR e correr directamente
1717
1771
  if (meta.language === 'java') {
1718
- const targetDir = path.join(WORKSPACE_ROOT, 'target');
1772
+ const targetDir = path.join(wsRoot, 'target');
1719
1773
 
1720
1774
  if (!fs.existsSync(targetDir)) {
1721
1775
  throw new Error('target/ directory not found. Run mvn clean install first.');
@@ -1759,7 +1813,7 @@ app.post("/workspace/test-local/start", async (req, res) => {
1759
1813
  const startConfig = {
1760
1814
  command,
1761
1815
  args,
1762
- cwd: WORKSPACE_ROOT,
1816
+ cwd: wsRoot,
1763
1817
  port: serverPort,
1764
1818
  env: cleanEnv
1765
1819
  };
@@ -1802,7 +1856,7 @@ app.post("/workspace/test-local/start", async (req, res) => {
1802
1856
  await processManager.start('test-local', {
1803
1857
  command,
1804
1858
  args,
1805
- cwd: WORKSPACE_ROOT,
1859
+ cwd: wsRoot,
1806
1860
  port: serverPort,
1807
1861
  env: { ...process.env, PORT: String(serverPort) }
1808
1862
  });
@@ -1853,7 +1907,8 @@ app.post("/workspace/test-local/stop", async (req, res) => {
1853
1907
  * Returns discovered endpoints
1854
1908
  */
1855
1909
  app.get("/workspace/test-local/endpoints", async (req, res) => {
1856
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
1910
+ const wsRoot = resolveWorkspaceRoot(req);
1911
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
1857
1912
 
1858
1913
  try {
1859
1914
  console.log("📋 [TEST-LOCAL] Getting endpoints...");
@@ -1868,13 +1923,13 @@ app.get("/workspace/test-local/endpoints", async (req, res) => {
1868
1923
  }
1869
1924
 
1870
1925
  // Otherwise discover them
1871
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
1926
+ const scanner = new WorkspaceScanner(wsRoot);
1872
1927
  const structure = await scanner.scan();
1873
1928
 
1874
- const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
1929
+ const controllerAnalyzer = new ControllerAnalyzer(wsRoot);
1875
1930
  const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
1876
1931
 
1877
- const dtoAnalyzer = new DTOAnalyzer(WORKSPACE_ROOT);
1932
+ const dtoAnalyzer = new DTOAnalyzer(wsRoot);
1878
1933
  const payloadDocs = await dtoAnalyzer.generatePayloadDocs(structure.files, apiDocs.endpoints);
1879
1934
 
1880
1935
  TEST_LOCAL_STATE.endpoints = payloadDocs.endpoints;
@@ -2027,16 +2082,17 @@ app.get("/workspace/test-local/logs", (req, res) => {
2027
2082
 
2028
2083
 
2029
2084
  /** Discover controllers and endpoints */
2030
- app.get("/workspace/controllers", async (_req, res) => {
2031
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
2085
+ app.get("/workspace/controllers", async (req, res) => {
2086
+ const wsRoot = resolveWorkspaceRoot(req);
2087
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
2032
2088
 
2033
2089
  try {
2034
2090
  console.log("🔍 [CONTROLLERS] Discovering controllers...");
2035
2091
 
2036
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
2092
+ const scanner = new WorkspaceScanner(wsRoot);
2037
2093
  const structure = await scanner.scan();
2038
2094
 
2039
- const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
2095
+ const controllerAnalyzer = new ControllerAnalyzer(wsRoot);
2040
2096
  const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
2041
2097
 
2042
2098
  console.log(`✅ [CONTROLLERS] Found ${apiDocs.totalEndpoints} endpoints in ${apiDocs.totalControllers} controllers`);
@@ -2052,19 +2108,20 @@ app.get("/workspace/controllers", async (_req, res) => {
2052
2108
  });
2053
2109
 
2054
2110
  /** Analyze DTOs and payloads */
2055
- app.get("/workspace/dtos", async (_req, res) => {
2056
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
2111
+ app.get("/workspace/dtos", async (req, res) => {
2112
+ const wsRoot = resolveWorkspaceRoot(req);
2113
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
2057
2114
 
2058
2115
  try {
2059
2116
  console.log("📦 [DTOS] Analyzing DTOs...");
2060
2117
 
2061
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
2118
+ const scanner = new WorkspaceScanner(wsRoot);
2062
2119
  const structure = await scanner.scan();
2063
2120
 
2064
- const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
2121
+ const controllerAnalyzer = new ControllerAnalyzer(wsRoot);
2065
2122
  const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
2066
2123
 
2067
- const dtoAnalyzer = new DTOAnalyzer(WORKSPACE_ROOT);
2124
+ const dtoAnalyzer = new DTOAnalyzer(wsRoot);
2068
2125
  const payloadDocs = await dtoAnalyzer.generatePayloadDocs(structure.files, apiDocs.endpoints);
2069
2126
 
2070
2127
  console.log(`✅ [DTOS] Found ${payloadDocs.totalDtos} DTOs`);
@@ -2080,13 +2137,14 @@ app.get("/workspace/dtos", async (_req, res) => {
2080
2137
  });
2081
2138
 
2082
2139
  /** Get server configuration */
2083
- app.get("/workspace/config", async (_req, res) => {
2084
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
2140
+ app.get("/workspace/config", async (req, res) => {
2141
+ const wsRoot = resolveWorkspaceRoot(req);
2142
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
2085
2143
 
2086
2144
  try {
2087
2145
  console.log("⚙️ [CONFIG] Analyzing configuration...");
2088
2146
 
2089
- const configAnalyzer = new ConfigAnalyzer(WORKSPACE_ROOT);
2147
+ const configAnalyzer = new ConfigAnalyzer(wsRoot);
2090
2148
  const config = await configAnalyzer.analyze();
2091
2149
 
2092
2150
  console.log(`✅ [CONFIG] Server port: ${config.server.port}`);
@@ -2154,7 +2212,8 @@ function extractTargetFiles(diff) {
2154
2212
  * Applies patch with automatic backup and rollback on failure
2155
2213
  */
2156
2214
  app.post("/workspace/safe-patch", async (req, res) => {
2157
- if (!WORKSPACE_ROOT) {
2215
+ const wsRoot = resolveWorkspaceRoot(req);
2216
+ if (!wsRoot) {
2158
2217
  return res.status(400).json({
2159
2218
  error: "workspace not set",
2160
2219
  hint: "call POST /workspace/open first"
@@ -2186,7 +2245,7 @@ app.post("/workspace/safe-patch", async (req, res) => {
2186
2245
  const backupFiles = [];
2187
2246
 
2188
2247
  for (const relPath of targetFiles) {
2189
- const fullPath = path.join(WORKSPACE_ROOT, relPath);
2248
+ const fullPath = path.join(wsRoot, relPath);
2190
2249
  if (await exists(fullPath)) {
2191
2250
  const content = await readFile(fullPath, 'utf8');
2192
2251
  backupFiles.push({ path: relPath, content });
@@ -2225,7 +2284,7 @@ app.post("/workspace/safe-patch", async (req, res) => {
2225
2284
 
2226
2285
  // 4. Apply patch
2227
2286
  try {
2228
- const result = await applyUnifiedDiff(WORKSPACE_ROOT, diff);
2287
+ const result = await applyUnifiedDiff(wsRoot, diff);
2229
2288
  console.log(`✅ Patch applied successfully: ${result.target}`);
2230
2289
 
2231
2290
  res.json({
@@ -2240,7 +2299,7 @@ app.post("/workspace/safe-patch", async (req, res) => {
2240
2299
  console.error(`❌ Patch failed, rolling back: ${patchError.message}`);
2241
2300
 
2242
2301
  for (const file of backupFiles) {
2243
- const fullPath = path.join(WORKSPACE_ROOT, file.path);
2302
+ const fullPath = path.join(wsRoot, file.path);
2244
2303
  await writeFile(fullPath, file.content, 'utf8');
2245
2304
  }
2246
2305
 
@@ -2264,7 +2323,8 @@ app.post("/workspace/safe-patch", async (req, res) => {
2264
2323
  * Validates and simulates patch application WITHOUT modifying files
2265
2324
  */
2266
2325
  app.post("/workspace/patch/dry-run", async (req, res) => {
2267
- if (!WORKSPACE_ROOT) {
2326
+ const wsRoot = resolveWorkspaceRoot(req);
2327
+ if (!wsRoot) {
2268
2328
  return res.status(400).json({
2269
2329
  error: "workspace not set",
2270
2330
  hint: "call POST /workspace/open first"
@@ -2296,7 +2356,7 @@ app.post("/workspace/patch/dry-run", async (req, res) => {
2296
2356
  // 3. Check if files exist
2297
2357
  const fileChecks = await Promise.all(
2298
2358
  targetFiles.map(async (relPath) => {
2299
- const fullPath = path.join(WORKSPACE_ROOT, relPath);
2359
+ const fullPath = path.join(wsRoot, relPath);
2300
2360
  const fileExists = await exists(fullPath);
2301
2361
  let currentContent = null;
2302
2362
  let lineCount = 0;
@@ -2367,7 +2427,8 @@ app.post("/workspace/validate-diff", async (req, res) => {
2367
2427
  * Creates manual backup of specific files
2368
2428
  */
2369
2429
  app.post("/workspace/backup", async (req, res) => {
2370
- if (!WORKSPACE_ROOT) {
2430
+ const wsRoot = resolveWorkspaceRoot(req);
2431
+ if (!wsRoot) {
2371
2432
  return res.status(400).json({
2372
2433
  error: "workspace not set",
2373
2434
  hint: "call POST /workspace/open first"
@@ -2384,7 +2445,7 @@ app.post("/workspace/backup", async (req, res) => {
2384
2445
  const backupFiles = [];
2385
2446
 
2386
2447
  for (const relPath of files) {
2387
- const fullPath = path.join(WORKSPACE_ROOT, relPath);
2448
+ const fullPath = path.join(wsRoot, relPath);
2388
2449
  if (await exists(fullPath)) {
2389
2450
  const content = await readFile(fullPath, 'utf8');
2390
2451
  backupFiles.push({ path: relPath, content });
@@ -2424,7 +2485,8 @@ app.post("/workspace/backup", async (req, res) => {
2424
2485
  * Restores files from a backup
2425
2486
  */
2426
2487
  app.post("/workspace/rollback", async (req, res) => {
2427
- if (!WORKSPACE_ROOT) {
2488
+ const wsRoot = resolveWorkspaceRoot(req);
2489
+ if (!wsRoot) {
2428
2490
  return res.status(400).json({
2429
2491
  error: "workspace not set",
2430
2492
  hint: "call POST /workspace/open first"
@@ -2448,7 +2510,7 @@ app.post("/workspace/rollback", async (req, res) => {
2448
2510
  console.log(`♻️ Rolling back to backup: ${backupId}`);
2449
2511
 
2450
2512
  for (const file of backup.files) {
2451
- const fullPath = path.join(WORKSPACE_ROOT, file.path);
2513
+ const fullPath = path.join(wsRoot, file.path);
2452
2514
  await writeFile(fullPath, file.content, 'utf8');
2453
2515
  }
2454
2516
 
@@ -2470,7 +2532,7 @@ app.post("/workspace/rollback", async (req, res) => {
2470
2532
  * GET /workspace/backups
2471
2533
  * Lists all available backups
2472
2534
  */
2473
- app.get("/workspace/backups", (_req, res) => {
2535
+ app.get("/workspace/backups", (req, res) => {
2474
2536
  const backupList = Array.from(BACKUPS.entries()).map(([id, data]) => ({
2475
2537
  backupId: id,
2476
2538
  timestamp: data.timestamp,
@@ -2524,7 +2586,8 @@ app.delete("/workspace/backups/:backupId", (req, res) => {
2524
2586
  app.get("/workspace/diff/by-incident/:incidentId", async (req, res) => {
2525
2587
  const { incidentId } = req.params;
2526
2588
 
2527
- if (!WORKSPACE_ROOT) {
2589
+ const wsRoot = resolveWorkspaceRoot(req);
2590
+ if (!wsRoot) {
2528
2591
  return res.status(400).json({ error: "workspace not set" });
2529
2592
  }
2530
2593
 
@@ -2574,7 +2637,7 @@ app.get("/workspace/diff/by-incident/:incidentId", async (req, res) => {
2574
2637
  try {
2575
2638
  const diffs = [];
2576
2639
  for (const file of matchedBackup.files) {
2577
- const fullPath = path.join(WORKSPACE_ROOT, file.path);
2640
+ const fullPath = path.join(wsRoot, file.path);
2578
2641
  let currentContent = '';
2579
2642
  try {
2580
2643
  currentContent = await readFile(fullPath, 'utf8');
@@ -2635,7 +2698,8 @@ app.get("/workspace/diff/by-incident/:incidentId", async (req, res) => {
2635
2698
  app.get("/workspace/diff/:backupId", async (req, res) => {
2636
2699
  const { backupId } = req.params;
2637
2700
 
2638
- if (!WORKSPACE_ROOT) {
2701
+ const wsRoot = resolveWorkspaceRoot(req);
2702
+ if (!wsRoot) {
2639
2703
  return res.status(400).json({ error: "workspace not set" });
2640
2704
  }
2641
2705
 
@@ -2652,7 +2716,7 @@ app.get("/workspace/diff/:backupId", async (req, res) => {
2652
2716
  const diffs = [];
2653
2717
 
2654
2718
  for (const file of backup.files) {
2655
- const fullPath = path.join(WORKSPACE_ROOT, file.path);
2719
+ const fullPath = path.join(wsRoot, file.path);
2656
2720
  let currentContent = '';
2657
2721
 
2658
2722
  try {
@@ -2709,7 +2773,8 @@ app.get("/workspace/diff/:backupId", async (req, res) => {
2709
2773
  * - framework: spring-boot, express, flask, etc (optional)
2710
2774
  */
2711
2775
  app.get("/workspace/detect-port", async (req, res) => {
2712
- if (!WORKSPACE_ROOT) {
2776
+ const wsRoot = resolveWorkspaceRoot(req);
2777
+ if (!wsRoot) {
2713
2778
  return res.status(400).json({
2714
2779
  error: "workspace not set",
2715
2780
  hint: "call POST /workspace/open first"
@@ -2719,7 +2784,7 @@ app.get("/workspace/detect-port", async (req, res) => {
2719
2784
  const { servicePath = '', language, framework } = req.query;
2720
2785
 
2721
2786
  try {
2722
- const fullPath = path.join(WORKSPACE_ROOT, servicePath);
2787
+ const fullPath = path.join(wsRoot, servicePath);
2723
2788
  console.log(`🔍 Detecting port for service at: ${fullPath}`);
2724
2789
 
2725
2790
  let port = null;
@@ -2973,18 +3038,19 @@ async function detectDotNetPort(servicePath) {
2973
3038
 
2974
3039
  /** Prepare for testing: compile + discover endpoints */
2975
3040
  app.post("/workspace/test-local/prepare", async (req, res) => {
2976
- if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
3041
+ const wsRoot = resolveWorkspaceRoot(req);
3042
+ if (!wsRoot) return res.status(400).json({ error: "workspace not set" });
2977
3043
 
2978
3044
  try {
2979
3045
  console.log("🔧 [TEST-LOCAL] Preparing test environment...");
2980
3046
  TEST_LOCAL_STATE.status = "compiling";
2981
3047
 
2982
3048
  // Step 1: Compile
2983
- const meta = await detectProject(WORKSPACE_ROOT);
3049
+ const meta = await detectProject(wsRoot);
2984
3050
  const compileResult = await compileAndTest({
2985
3051
  language: meta.language,
2986
3052
  buildTool: meta.buildTool,
2987
- cwd: WORKSPACE_ROOT,
3053
+ cwd: wsRoot,
2988
3054
  skipTests: true
2989
3055
  });
2990
3056
 
@@ -3000,19 +3066,19 @@ app.post("/workspace/test-local/prepare", async (req, res) => {
3000
3066
  TEST_LOCAL_STATE.status = "compiled";
3001
3067
 
3002
3068
  // Step 2: Discover endpoints
3003
- const scanner = new WorkspaceScanner(WORKSPACE_ROOT);
3069
+ const scanner = new WorkspaceScanner(wsRoot);
3004
3070
  const structure = await scanner.scan();
3005
3071
 
3006
- const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
3072
+ const controllerAnalyzer = new ControllerAnalyzer(wsRoot);
3007
3073
  const apiDocs = await controllerAnalyzer.generateApiDocs(structure.files);
3008
3074
 
3009
- const dtoAnalyzer = new DTOAnalyzer(WORKSPACE_ROOT);
3075
+ const dtoAnalyzer = new DTOAnalyzer(wsRoot);
3010
3076
  const payloadDocs = await dtoAnalyzer.generatePayloadDocs(structure.files, apiDocs.endpoints);
3011
3077
 
3012
3078
  TEST_LOCAL_STATE.endpoints = payloadDocs.endpoints;
3013
3079
 
3014
3080
  // Step 3: Get config
3015
- const configAnalyzer = new ConfigAnalyzer(WORKSPACE_ROOT);
3081
+ const configAnalyzer = new ConfigAnalyzer(wsRoot);
3016
3082
  const config = await configAnalyzer.analyze();
3017
3083
  TEST_LOCAL_STATE.config = config;
3018
3084
 
@@ -3050,7 +3116,8 @@ app.post("/workspace/test-local/prepare", async (req, res) => {
3050
3116
  * SSE endpoint for real-time compilation logs
3051
3117
  */
3052
3118
  app.get("/workspace/test-local/compile/stream", async (req, res) => {
3053
- if (!WORKSPACE_ROOT) {
3119
+ const wsRoot = resolveWorkspaceRoot(req);
3120
+ if (!wsRoot) {
3054
3121
  res.status(400).json({ error: "workspace not set" });
3055
3122
  return;
3056
3123
  }
@@ -3064,7 +3131,7 @@ app.get("/workspace/test-local/compile/stream", async (req, res) => {
3064
3131
  res.flushHeaders();
3065
3132
 
3066
3133
  try {
3067
- const meta = await detectProject(WORKSPACE_ROOT);
3134
+ const meta = await detectProject(wsRoot);
3068
3135
 
3069
3136
  // Send initial event
3070
3137
  res.write(`event: log\n`);
@@ -3106,7 +3173,7 @@ app.get("/workspace/test-local/compile/stream", async (req, res) => {
3106
3173
 
3107
3174
  const startTime = Date.now();
3108
3175
  const proc = spawn(command, args, {
3109
- cwd: WORKSPACE_ROOT,
3176
+ cwd: wsRoot,
3110
3177
  shell: true,
3111
3178
  env: { ...process.env, MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.defaultLogLevel=info" }
3112
3179
  });
@@ -3578,14 +3645,15 @@ app.get("/system/info", (req, res) => {
3578
3645
  * Analisa controllers e retorna todos os endpoints da API
3579
3646
  */
3580
3647
  app.get("/workspace/api-docs", async (req, res) => {
3581
- if (!WORKSPACE_ROOT) {
3648
+ const wsRoot = resolveWorkspaceRoot(req);
3649
+ if (!wsRoot) {
3582
3650
  return res.status(400).json({
3583
3651
  error: "workspace not set",
3584
3652
  hint: "call POST /workspace/open first"
3585
3653
  });
3586
3654
  }
3587
3655
 
3588
- console.log(`📚 [API-DOCS] Analyzing controllers in ${WORKSPACE_ROOT}`);
3656
+ console.log(`📚 [API-DOCS] Analyzing controllers in ${wsRoot}`);
3589
3657
 
3590
3658
  try {
3591
3659
  // Primeiro, escanear arquivos do workspace
@@ -3602,7 +3670,7 @@ app.get("/workspace/api-docs", async (req, res) => {
3602
3670
 
3603
3671
  for (const entry of entries) {
3604
3672
  const fullPath = pathModule.join(dir, entry.name);
3605
- const relativePath = pathModule.relative(WORKSPACE_ROOT, fullPath);
3673
+ const relativePath = pathModule.relative(wsRoot, fullPath);
3606
3674
 
3607
3675
  // Skip common ignored directories
3608
3676
  if (entry.name === 'node_modules' || entry.name === 'target' ||
@@ -3622,12 +3690,12 @@ app.get("/workspace/api-docs", async (req, res) => {
3622
3690
  }
3623
3691
  }
3624
3692
 
3625
- await scanDir(WORKSPACE_ROOT);
3693
+ await scanDir(wsRoot);
3626
3694
 
3627
3695
  console.log(`📁 [API-DOCS] Found ${files.length} Java files`);
3628
3696
 
3629
3697
  // Agora analisar os controllers
3630
- const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
3698
+ const controllerAnalyzer = new ControllerAnalyzer(wsRoot);
3631
3699
  const apiDocs = await controllerAnalyzer.generateApiDocs(files);
3632
3700
 
3633
3701
  console.log(`✅ [API-DOCS] Found ${apiDocs.totalEndpoints} endpoints in ${apiDocs.totalControllers} controllers`);
@@ -3638,7 +3706,7 @@ app.get("/workspace/api-docs", async (req, res) => {
3638
3706
  controllers: apiDocs.controllers.map(c => c.className),
3639
3707
  totalEndpoints: apiDocs.totalEndpoints,
3640
3708
  totalControllers: apiDocs.totalControllers,
3641
- workspace: WORKSPACE_ROOT
3709
+ workspace: wsRoot
3642
3710
  });
3643
3711
 
3644
3712
  } catch (error) {
@@ -3660,7 +3728,8 @@ app.get("/workspace/api-docs", async (req, res) => {
3660
3728
  * Retorna endpoints de uma controller específica
3661
3729
  */
3662
3730
  app.get("/workspace/api-docs/:controller", async (req, res) => {
3663
- if (!WORKSPACE_ROOT) {
3731
+ const wsRoot = resolveWorkspaceRoot(req);
3732
+ if (!wsRoot) {
3664
3733
  return res.status(400).json({
3665
3734
  error: "workspace not set",
3666
3735
  hint: "call POST /workspace/open first"
@@ -3685,7 +3754,7 @@ app.get("/workspace/api-docs/:controller", async (req, res) => {
3685
3754
 
3686
3755
  for (const entry of entries) {
3687
3756
  const fullPath = pathModule.join(dir, entry.name);
3688
- const relativePath = pathModule.relative(WORKSPACE_ROOT, fullPath);
3757
+ const relativePath = pathModule.relative(wsRoot, fullPath);
3689
3758
 
3690
3759
  if (entry.name === 'node_modules' || entry.name === 'target' ||
3691
3760
  entry.name === 'build' || entry.name === '.git' ||
@@ -3704,10 +3773,10 @@ app.get("/workspace/api-docs/:controller", async (req, res) => {
3704
3773
  }
3705
3774
  }
3706
3775
 
3707
- await scanDir(WORKSPACE_ROOT);
3776
+ await scanDir(wsRoot);
3708
3777
 
3709
3778
  // Analisar os controllers
3710
- const controllerAnalyzer = new ControllerAnalyzer(WORKSPACE_ROOT);
3779
+ const controllerAnalyzer = new ControllerAnalyzer(wsRoot);
3711
3780
  const apiDocs = await controllerAnalyzer.generateApiDocs(files);
3712
3781
 
3713
3782
  const found = apiDocs.controllers?.find(c =>
@@ -3828,7 +3897,8 @@ app.get("/ai-engine/fixes", (req, res) => {
3828
3897
  * Recolhe ficheiros de configuração para análise AI
3829
3898
  */
3830
3899
  app.get("/workspace/smart-config", async (req, res) => {
3831
- if (!WORKSPACE_ROOT) {
3900
+ const wsRoot = resolveWorkspaceRoot(req);
3901
+ if (!wsRoot) {
3832
3902
  return res.status(400).json({ error: "workspace not set" });
3833
3903
  }
3834
3904
 
@@ -3851,10 +3921,10 @@ app.get("/workspace/smart-config", async (req, res) => {
3851
3921
  ];
3852
3922
 
3853
3923
  const collectedFiles = [];
3854
- const meta = await detectProject(WORKSPACE_ROOT);
3924
+ const meta = await detectProject(wsRoot);
3855
3925
 
3856
3926
  for (const pattern of configPatterns) {
3857
- const filePath = path.join(WORKSPACE_ROOT, pattern);
3927
+ const filePath = path.join(wsRoot, pattern);
3858
3928
  if (fs.existsSync(filePath)) {
3859
3929
  try {
3860
3930
  const content = fs.readFileSync(filePath, 'utf8');
@@ -3875,7 +3945,7 @@ app.get("/workspace/smart-config", async (req, res) => {
3875
3945
 
3876
3946
  // Detectar profiles disponíveis
3877
3947
  const availableProfiles = [];
3878
- const resourcesDir = path.join(WORKSPACE_ROOT, 'src/main/resources');
3948
+ const resourcesDir = path.join(wsRoot, 'src/main/resources');
3879
3949
  if (fs.existsSync(resourcesDir)) {
3880
3950
  const files = fs.readdirSync(resourcesDir);
3881
3951
  files.forEach(file => {
@@ -3888,7 +3958,7 @@ app.get("/workspace/smart-config", async (req, res) => {
3888
3958
 
3889
3959
  res.json({
3890
3960
  ok: true,
3891
- workspace: WORKSPACE_ROOT,
3961
+ workspace: wsRoot,
3892
3962
  language: meta.language,
3893
3963
  buildTool: meta.buildTool,
3894
3964
  framework: meta.framework || 'unknown',
@@ -3974,7 +4044,8 @@ app.post("/workspace/test-local/restart", async (req, res) => {
3974
4044
  * Response: { "ok": true, "results": "file:line: matching text\n...", "count": 15 }
3975
4045
  */
3976
4046
  app.post("/workspace/search", async (req, res) => {
3977
- if (!WORKSPACE_ROOT) {
4047
+ const wsRoot = resolveWorkspaceRoot(req);
4048
+ if (!wsRoot) {
3978
4049
  return res.status(400).json({
3979
4050
  ok: false,
3980
4051
  error: "workspace not set",
@@ -4009,7 +4080,7 @@ app.post("/workspace/search", async (req, res) => {
4009
4080
 
4010
4081
  // Escape pattern for shell safety and limit results
4011
4082
  const safePattern = pattern.replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
4012
- cmd += `"${safePattern}" "${WORKSPACE_ROOT}" | head -100`;
4083
+ cmd += `"${safePattern}" "${wsRoot}" | head -100`;
4013
4084
 
4014
4085
  let stdout = '';
4015
4086
  try {
@@ -4041,7 +4112,7 @@ app.post("/workspace/search", async (req, res) => {
4041
4112
  const results = stdout
4042
4113
  .split('\n')
4043
4114
  .filter(line => line.trim())
4044
- .map(line => line.replace(WORKSPACE_ROOT + '/', ''))
4115
+ .map(line => line.replace(wsRoot + '/', ''))
4045
4116
  .join('\n');
4046
4117
 
4047
4118
  const count = results.split('\n').filter(l => l.trim()).length;
@@ -4071,7 +4142,8 @@ app.post("/workspace/search", async (req, res) => {
4071
4142
  * Dangerous commands are blocked.
4072
4143
  */
4073
4144
  app.post("/workspace/exec", async (req, res) => {
4074
- if (!WORKSPACE_ROOT) {
4145
+ const wsRoot = resolveWorkspaceRoot(req);
4146
+ if (!wsRoot) {
4075
4147
  return res.status(400).json({
4076
4148
  ok: false,
4077
4149
  error: "workspace not set",
@@ -4107,7 +4179,7 @@ app.post("/workspace/exec", async (req, res) => {
4107
4179
  const execAsync = promisify(execCb);
4108
4180
 
4109
4181
  const result = await execAsync(command, {
4110
- cwd: WORKSPACE_ROOT,
4182
+ cwd: wsRoot,
4111
4183
  timeout: 60000,
4112
4184
  maxBuffer: 5 * 1024 * 1024,
4113
4185
  env: { ...process.env, FORCE_COLOR: '0' }
@@ -4238,7 +4310,8 @@ app.post("/workspace/test-endpoint", async (req, res) => {
4238
4310
  * Body: { branchName, commitMessage, files? }
4239
4311
  */
4240
4312
  app.post("/workspace/git/create-fix-branch", async (req, res) => {
4241
- if (!WORKSPACE_ROOT) {
4313
+ const wsRoot = resolveWorkspaceRoot(req);
4314
+ if (!wsRoot) {
4242
4315
  return res.status(400).json({ error: "workspace not set" });
4243
4316
  }
4244
4317
 
@@ -4250,7 +4323,7 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
4250
4323
  try {
4251
4324
  const { execSync } = await import('child_process');
4252
4325
  const opts = {
4253
- cwd: WORKSPACE_ROOT,
4326
+ cwd: wsRoot,
4254
4327
  encoding: 'utf8',
4255
4328
  timeout: 30000,
4256
4329
  env: {
@@ -4612,14 +4685,32 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
4612
4685
  .replace(/\.git$/, '');
4613
4686
  const [workspace, repoSlug] = repoPath.split('/');
4614
4687
 
4615
- // gitToken é "username:appPassword" — codificar para Basic Auth
4616
- const credentials = Buffer.from(gitToken).toString('base64');
4688
+ // gitToken may be:
4689
+ // - "x-token-auth:TOKEN" (Repository Access Token from Gateway)
4690
+ // - "username:appPassword" (App Password)
4691
+ // - plain TOKEN (Repository Access Token)
4692
+ let authHeader;
4693
+ if (gitToken.includes(':')) {
4694
+ // Contains colon — could be user:pass (App Password) or x-token-auth:token
4695
+ const [user, pass] = gitToken.split(':', 2);
4696
+ if (user === 'x-token-auth') {
4697
+ // Repository Access Token — use Bearer with the token part
4698
+ authHeader = `Bearer ${pass}`;
4699
+ } else {
4700
+ // App Password — use Basic auth
4701
+ const credentials = Buffer.from(gitToken).toString('base64');
4702
+ authHeader = `Basic ${credentials}`;
4703
+ }
4704
+ } else {
4705
+ // Plain token — use Bearer (Repository Access Token)
4706
+ authHeader = `Bearer ${gitToken}`;
4707
+ }
4617
4708
 
4618
4709
  const response = await fetch(
4619
4710
  `https://api.bitbucket.org/2.0/repositories/${workspace}/${repoSlug}/pullrequests`, {
4620
4711
  method: 'POST',
4621
4712
  headers: {
4622
- 'Authorization': `Basic ${credentials}`,
4713
+ 'Authorization': authHeader,
4623
4714
  'Content-Type': 'application/json'
4624
4715
  },
4625
4716
  body: JSON.stringify({
@@ -4674,13 +4765,14 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
4674
4765
  * Returns current git status (branch, changes, remote)
4675
4766
  */
4676
4767
  app.get("/workspace/git/status", async (req, res) => {
4677
- if (!WORKSPACE_ROOT) {
4768
+ const wsRoot = resolveWorkspaceRoot(req);
4769
+ if (!wsRoot) {
4678
4770
  return res.status(400).json({ error: "workspace not set" });
4679
4771
  }
4680
4772
 
4681
4773
  try {
4682
4774
  const { execSync } = await import('child_process');
4683
- const opts = { cwd: WORKSPACE_ROOT, encoding: 'utf8', timeout: 10000 };
4775
+ const opts = { cwd: wsRoot, encoding: 'utf8', timeout: 10000 };
4684
4776
 
4685
4777
  let branch = '', remoteUrl = '', status = '';
4686
4778
  let isGitRepo = false;
@@ -4723,7 +4815,8 @@ app.get("/workspace/git/status", async (req, res) => {
4723
4815
  * - filter: 'all' | 'unresolved' | 'review' (default: 'all')
4724
4816
  */
4725
4817
  app.get("/workspace/git/pr/comments", async (req, res) => {
4726
- if (!WORKSPACE_ROOT) {
4818
+ const wsRoot = resolveWorkspaceRoot(req);
4819
+ if (!wsRoot) {
4727
4820
  return res.status(400).json({ error: "workspace not set" });
4728
4821
  }
4729
4822
 
@@ -4788,7 +4881,8 @@ app.get("/workspace/git/pr/comments", async (req, res) => {
4788
4881
  * - prNumber (required): PR/MR number
4789
4882
  */
4790
4883
  app.get("/workspace/git/pr/comments/:commentId", async (req, res) => {
4791
- if (!WORKSPACE_ROOT) {
4884
+ const wsRoot = resolveWorkspaceRoot(req);
4885
+ if (!wsRoot) {
4792
4886
  return res.status(400).json({ error: "workspace not set" });
4793
4887
  }
4794
4888
 
@@ -4831,7 +4925,8 @@ app.get("/workspace/git/pr/comments/:commentId", async (req, res) => {
4831
4925
  * - inReplyToId: Parent comment ID (for thread replies)
4832
4926
  */
4833
4927
  app.post("/workspace/git/pr/comments", async (req, res) => {
4834
- if (!WORKSPACE_ROOT) {
4928
+ const wsRoot = resolveWorkspaceRoot(req);
4929
+ if (!wsRoot) {
4835
4930
  return res.status(400).json({ error: "workspace not set" });
4836
4931
  }
4837
4932
 
@@ -4888,7 +4983,8 @@ app.post("/workspace/git/pr/comments", async (req, res) => {
4888
4983
  * - prNumber (required): PR/MR number
4889
4984
  */
4890
4985
  app.post("/workspace/git/pr/comments/:commentId/resolve", async (req, res) => {
4891
- if (!WORKSPACE_ROOT) {
4986
+ const wsRoot = resolveWorkspaceRoot(req);
4987
+ if (!wsRoot) {
4892
4988
  return res.status(400).json({ error: "workspace not set" });
4893
4989
  }
4894
4990
 
@@ -4925,7 +5021,8 @@ app.post("/workspace/git/pr/comments/:commentId/resolve", async (req, res) => {
4925
5021
  * - prNumber (required): PR/MR number
4926
5022
  */
4927
5023
  app.post("/workspace/git/pr/comments/:commentId/unresolve", async (req, res) => {
4928
- if (!WORKSPACE_ROOT) {
5024
+ const wsRoot = resolveWorkspaceRoot(req);
5025
+ if (!wsRoot) {
4929
5026
  return res.status(400).json({ error: "workspace not set" });
4930
5027
  }
4931
5028
 
@@ -4966,7 +5063,8 @@ app.post("/workspace/git/pr/comments/:commentId/unresolve", async (req, res) =>
4966
5063
  * - branch: Branch to apply fix on (defaults to current branch)
4967
5064
  */
4968
5065
  app.post("/workspace/git/pr/comments/:commentId/fix-and-resolve", async (req, res) => {
4969
- if (!WORKSPACE_ROOT) {
5066
+ const wsRoot = resolveWorkspaceRoot(req);
5067
+ if (!wsRoot) {
4970
5068
  return res.status(400).json({ error: "workspace not set" });
4971
5069
  }
4972
5070
 
@@ -5101,7 +5199,7 @@ async function _getGitProvider() {
5101
5199
  // ============================================
5102
5200
 
5103
5201
  /** Lista todos os workspaces abertos */
5104
- app.get("/workspaces", (_req, res) => {
5202
+ app.get("/workspaces", (req, res) => {
5105
5203
  res.json({
5106
5204
  workspaces: wsManager.list(),
5107
5205
  total: wsManager.count,