mcp-lab-agent 2.1.4 → 2.1.10
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/README.md +194 -224
- package/dist/index.js +1391 -310
- package/dist/index.js.map +1 -1
- package/learning-hub/README.md +66 -0
- package/learning-hub/package.json +17 -0
- package/learning-hub/src/dashboard.html +73 -0
- package/learning-hub/src/server.js +129 -0
- package/learning-hub/src/store.js +114 -0
- package/package.json +8 -5
- package/slack-bot/.env.example +17 -2
- package/slack-bot/CREDENTIALS.md +23 -0
- package/slack-bot/README.md +74 -16
- package/slack-bot/TROUBLESHOOTING.md +109 -0
- package/slack-bot/check-config.js +80 -37
- package/slack-bot/setup.js +14 -8
- package/slack-bot/src/config.js +18 -8
- package/slack-bot/src/index.js +46 -12
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# QA Lab Learning Hub
|
|
2
|
+
|
|
3
|
+
API centralizada para aprendizados do **mcp-lab-agent**. Permite acumular memória entre projetos e ver métricas em um dashboard.
|
|
4
|
+
|
|
5
|
+
## Rodar
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Via npx (sem clonar)
|
|
9
|
+
npx mcp-lab-agent learning-hub
|
|
10
|
+
|
|
11
|
+
# Ou local
|
|
12
|
+
npm run learning-hub
|
|
13
|
+
# ou: node learning-hub/src/server.js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Porta padrão: **3847**. Acesse http://localhost:3847 para o dashboard.
|
|
17
|
+
|
|
18
|
+
## Endpoints
|
|
19
|
+
|
|
20
|
+
| Método | Rota | Descrição |
|
|
21
|
+
|--------|------|-----------|
|
|
22
|
+
| GET | `/` | Dashboard web (taxa de sucesso, padrões, recomendações) |
|
|
23
|
+
| POST | `/learning` | Recebe learnings do agente |
|
|
24
|
+
| GET | `/patterns` | Padrões agregados (`?framework=&projectId=&limit=`) |
|
|
25
|
+
| GET | `/health` | Health check |
|
|
26
|
+
|
|
27
|
+
## Configurar o agente para enviar ao Hub
|
|
28
|
+
|
|
29
|
+
No `.env` do seu projeto:
|
|
30
|
+
|
|
31
|
+
```env
|
|
32
|
+
LEARNING_HUB_URL=http://localhost:3847
|
|
33
|
+
LEARNING_HUB_PROJECT_ID=meu-projeto
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
O agente enviará automaticamente cada aprendizado para o Hub (assíncrono, não bloqueia).
|
|
37
|
+
|
|
38
|
+
## Formato POST /learning
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"learnings": [
|
|
43
|
+
{
|
|
44
|
+
"type": "element_not_rendered",
|
|
45
|
+
"framework": "playwright",
|
|
46
|
+
"request": "login flow",
|
|
47
|
+
"fix": "await element.waitFor({ state: 'attached' })",
|
|
48
|
+
"success": false,
|
|
49
|
+
"timestamp": "2025-03-18T..."
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Armazenamento
|
|
56
|
+
|
|
57
|
+
Por padrão, dados em `./data/learnings.json`. Configure:
|
|
58
|
+
|
|
59
|
+
```env
|
|
60
|
+
LEARNING_HUB_DATA=/caminho/para/data
|
|
61
|
+
LEARNING_HUB_PORT=3847
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Escalar
|
|
65
|
+
|
|
66
|
+
Para produção: troque `store.js` por SQLite ou Postgres. A API permanece a mesma.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qa-lab-learning-hub",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Learning Hub - API centralizada para aprendizados do mcp-lab-agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/server.js",
|
|
9
|
+
"dev": "node --watch src/server.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["qa", "mcp-lab-agent", "learning", "dashboard"],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>QA Lab - Learning Hub Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 1.5rem; background: #0f172a; color: #e2e8f0; }
|
|
10
|
+
h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
|
|
11
|
+
.sub { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1.5rem; }
|
|
12
|
+
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
13
|
+
.card { background: #1e293b; border-radius: 8px; padding: 1rem; border: 1px solid #334155; }
|
|
14
|
+
.card h3 { margin: 0 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: #94a3b8; }
|
|
15
|
+
.card .val { font-size: 1.75rem; font-weight: 700; }
|
|
16
|
+
.card.green .val { color: #22c55e; }
|
|
17
|
+
.card.yellow .val { color: #eab308; }
|
|
18
|
+
.card.red .val { color: #ef4444; }
|
|
19
|
+
.section { margin-top: 1.5rem; }
|
|
20
|
+
.section h2 { font-size: 1rem; margin: 0 0 0.75rem; }
|
|
21
|
+
ul { list-style: none; padding: 0; margin: 0; }
|
|
22
|
+
li { padding: 0.35rem 0; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; }
|
|
23
|
+
li:last-child { border-bottom: none; }
|
|
24
|
+
.rec { background: #1e293b; border-radius: 8px; padding: 1rem; margin-top: 0.5rem; border-left: 4px solid #3b82f6; }
|
|
25
|
+
.error { color: #ef4444; }
|
|
26
|
+
.refresh { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.875rem; }
|
|
27
|
+
.refresh:hover { background: #2563eb; }
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<h1>QA Lab Learning Hub</h1>
|
|
32
|
+
<p class="sub">Dashboard centralizado de aprendizados</p>
|
|
33
|
+
<button class="refresh" onclick="load()">Atualizar</button>
|
|
34
|
+
|
|
35
|
+
<div class="grid" id="metrics"></div>
|
|
36
|
+
<div class="section">
|
|
37
|
+
<h2>Padrões por tipo</h2>
|
|
38
|
+
<ul id="byType"></ul>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="section">
|
|
41
|
+
<h2>Recomendações</h2>
|
|
42
|
+
<div id="recs"></div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
const base = window.location.origin;
|
|
47
|
+
async function load() {
|
|
48
|
+
try {
|
|
49
|
+
const r = await fetch(base + '/patterns?limit=500');
|
|
50
|
+
const d = await r.json();
|
|
51
|
+
|
|
52
|
+
document.getElementById('metrics').innerHTML = [
|
|
53
|
+
{ k: 'Taxa de sucesso (1ª tent.)', v: d.firstAttemptSuccessRate + '%', c: d.firstAttemptSuccessRate >= 70 ? 'green' : d.firstAttemptSuccessRate >= 50 ? 'yellow' : 'red' },
|
|
54
|
+
{ k: 'Total de aprendizados', v: d.total, c: '' },
|
|
55
|
+
{ k: 'Testes gerados', v: d.testGenerated, c: '' },
|
|
56
|
+
].map(x => `<div class="card ${x.c}"><h3>${x.k}</h3><div class="val">${x.v}</div></div>`).join('');
|
|
57
|
+
|
|
58
|
+
const byType = Object.entries(d.byType || {}).filter(([,n]) => n > 0).sort((a,b) => b[1]-a[1]);
|
|
59
|
+
document.getElementById('byType').innerHTML = byType.length
|
|
60
|
+
? byType.map(([t,n]) => `<li><span>${t}</span><strong>${n}</strong></li>`).join('')
|
|
61
|
+
: '<li class="error">Nenhum aprendizado ainda</li>';
|
|
62
|
+
|
|
63
|
+
document.getElementById('recs').innerHTML = (d.recommendations || [])
|
|
64
|
+
.map(r => `<div class="rec">${r}</div>`).join('') || '<div class="rec">Continue gerando testes.</div>';
|
|
65
|
+
} catch (e) {
|
|
66
|
+
document.getElementById('metrics').innerHTML = `<div class="card error"><h3>Erro</h3><div class="val">${e.message}</div></div>`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
load();
|
|
70
|
+
setInterval(load, 30000);
|
|
71
|
+
</script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Learning Hub - API centralizada para aprendizados do mcp-lab-agent
|
|
4
|
+
*
|
|
5
|
+
* Endpoints:
|
|
6
|
+
* POST /learning — recebe learnings (body: { learnings: [...] } ou learnings: {...})
|
|
7
|
+
* GET /patterns — retorna padrões agregados (?framework=playwright&projectId=xyz&limit=100)
|
|
8
|
+
* GET /health — health check
|
|
9
|
+
*
|
|
10
|
+
* Uso:
|
|
11
|
+
* LEARNING_HUB_DATA=./data node src/server.js
|
|
12
|
+
* Porta padrão: 3847 (QA)
|
|
13
|
+
*/
|
|
14
|
+
import http from "node:http";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { addLearnings, getAggregatedPatterns } from "./store.js";
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PORT = parseInt(process.env.LEARNING_HUB_PORT || "3847", 10);
|
|
22
|
+
|
|
23
|
+
function parseBody(req) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let body = "";
|
|
26
|
+
req.on("data", (c) => (body += c));
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
try {
|
|
29
|
+
resolve(body ? JSON.parse(body) : {});
|
|
30
|
+
} catch {
|
|
31
|
+
reject(new Error("Invalid JSON"));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
req.on("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function send(res, status, data) {
|
|
39
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify(data));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handler(req, res) {
|
|
44
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
45
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
46
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
47
|
+
|
|
48
|
+
if (req.method === "OPTIONS") {
|
|
49
|
+
res.writeHead(204);
|
|
50
|
+
res.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
55
|
+
const path = url.pathname;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (path === "/health") {
|
|
59
|
+
send(res, 200, { ok: true, service: "learning-hub", version: "1.0.0" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (path === "/learning" && req.method === "POST") {
|
|
64
|
+
const body = await parseBody(req);
|
|
65
|
+
const learnings = body.learnings ?? (body.type ? body : null);
|
|
66
|
+
if (!learnings) {
|
|
67
|
+
send(res, 400, { error: "Missing learnings. Send { learnings: [...] } or a single learning object." });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const entries = Array.isArray(learnings) ? learnings : [learnings];
|
|
71
|
+
const added = addLearnings(entries);
|
|
72
|
+
send(res, 201, { ok: true, added });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (path === "/patterns" && req.method === "GET") {
|
|
77
|
+
const framework = url.searchParams.get("framework");
|
|
78
|
+
const projectId = url.searchParams.get("projectId");
|
|
79
|
+
const limit = parseInt(url.searchParams.get("limit") || "500", 10);
|
|
80
|
+
const data = getAggregatedPatterns({ framework, projectId, limit });
|
|
81
|
+
send(res, 200, data);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (path === "/" || path === "") {
|
|
86
|
+
const dashboardPath = path.join(__dirname, "dashboard.html");
|
|
87
|
+
if (fs.existsSync(dashboardPath)) {
|
|
88
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
89
|
+
res.end(fs.readFileSync(dashboardPath, "utf8"));
|
|
90
|
+
} else {
|
|
91
|
+
send(res, 200, {
|
|
92
|
+
name: "qa-lab-learning-hub",
|
|
93
|
+
endpoints: {
|
|
94
|
+
"GET /": "Dashboard",
|
|
95
|
+
"POST /learning": "Recebe learnings do agente",
|
|
96
|
+
"GET /patterns": "Padrões agregados (?framework=&projectId=&limit=)",
|
|
97
|
+
"GET /health": "Health check",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (path === "/api") {
|
|
105
|
+
send(res, 200, {
|
|
106
|
+
name: "qa-lab-learning-hub",
|
|
107
|
+
endpoints: {
|
|
108
|
+
"POST /learning": "Recebe learnings do agente",
|
|
109
|
+
"GET /patterns": "Padrões agregados (?framework=&projectId=&limit=)",
|
|
110
|
+
"GET /health": "Health check",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
send(res, 404, { error: "Not found" });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
send(res, 500, { error: err.message || "Internal error" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const server = http.createServer(handler);
|
|
123
|
+
server.listen(PORT, () => {
|
|
124
|
+
console.log(`Learning Hub rodando em http://localhost:${PORT}`);
|
|
125
|
+
console.log(" GET / — Dashboard");
|
|
126
|
+
console.log(" POST /learning — envia learnings");
|
|
127
|
+
console.log(" GET /patterns — padrões agregados");
|
|
128
|
+
console.log(" GET /health — health check");
|
|
129
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store para Learning Hub - persistência em JSON (como memory.js do agente).
|
|
3
|
+
* Escalável: troque por SQLite/Postgres se precisar.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const DATA_DIR = process.env.LEARNING_HUB_DATA || path.join(process.cwd(), "data");
|
|
9
|
+
const LEARNINGS_FILE = path.join(DATA_DIR, "learnings.json");
|
|
10
|
+
const MAX_LEARNINGS = 5000;
|
|
11
|
+
|
|
12
|
+
function ensureDir() {
|
|
13
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
14
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadLearnings() {
|
|
19
|
+
ensureDir();
|
|
20
|
+
if (!fs.existsSync(LEARNINGS_FILE)) return [];
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(LEARNINGS_FILE, "utf8");
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveLearnings(learnings) {
|
|
30
|
+
ensureDir();
|
|
31
|
+
const trimmed = learnings.length > MAX_LEARNINGS ? learnings.slice(-MAX_LEARNINGS) : learnings;
|
|
32
|
+
fs.writeFileSync(LEARNINGS_FILE, JSON.stringify(trimmed, null, 2), "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function addLearnings(entries) {
|
|
36
|
+
const learnings = loadLearnings();
|
|
37
|
+
const normalized = Array.isArray(entries) ? entries : [entries];
|
|
38
|
+
const withMeta = normalized.map((e) => ({
|
|
39
|
+
...e,
|
|
40
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
41
|
+
receivedAt: new Date().toISOString(),
|
|
42
|
+
}));
|
|
43
|
+
learnings.push(...withMeta);
|
|
44
|
+
saveLearnings(learnings);
|
|
45
|
+
return withMeta.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAggregatedPatterns(filters = {}) {
|
|
49
|
+
const learnings = loadLearnings();
|
|
50
|
+
const { framework, projectId, limit = 100 } = filters;
|
|
51
|
+
|
|
52
|
+
let filtered = learnings;
|
|
53
|
+
if (framework) {
|
|
54
|
+
filtered = filtered.filter((l) => (l.framework || "").toLowerCase().includes(framework.toLowerCase()));
|
|
55
|
+
}
|
|
56
|
+
if (projectId) {
|
|
57
|
+
filtered = filtered.filter((l) => l.projectId === projectId);
|
|
58
|
+
}
|
|
59
|
+
filtered = filtered.slice(-(limit || 500));
|
|
60
|
+
|
|
61
|
+
const byType = {};
|
|
62
|
+
const byFramework = {};
|
|
63
|
+
const byFrameworkAndType = {};
|
|
64
|
+
let testGenerated = 0;
|
|
65
|
+
let firstAttemptSuccess = 0;
|
|
66
|
+
|
|
67
|
+
filtered.forEach((l) => {
|
|
68
|
+
const t = l.type || "unknown";
|
|
69
|
+
byType[t] = (byType[t] || 0) + 1;
|
|
70
|
+
|
|
71
|
+
const f = l.framework || "unknown";
|
|
72
|
+
byFramework[f] = (byFramework[f] || 0) + 1;
|
|
73
|
+
|
|
74
|
+
const key = `${f}::${t}`;
|
|
75
|
+
byFrameworkAndType[key] = (byFrameworkAndType[key] || 0) + 1;
|
|
76
|
+
|
|
77
|
+
if (t === "test_generated") {
|
|
78
|
+
testGenerated++;
|
|
79
|
+
if (l.passedFirstTime) firstAttemptSuccess++;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const successRate = testGenerated > 0 ? Math.round((firstAttemptSuccess / testGenerated) * 100) : 0;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
total: filtered.length,
|
|
87
|
+
byType,
|
|
88
|
+
byFramework,
|
|
89
|
+
byFrameworkAndType,
|
|
90
|
+
testGenerated,
|
|
91
|
+
firstAttemptSuccessRate: successRate,
|
|
92
|
+
recommendations: buildRecommendations(byType, successRate),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildRecommendations(byType, successRate) {
|
|
97
|
+
const recs = [];
|
|
98
|
+
if (byType.element_not_rendered > 0 || byType.element_not_visible > 0) {
|
|
99
|
+
recs.push("Use waits explícitos (waitForSelector, waitForDisplayed) antes de interagir com elementos.");
|
|
100
|
+
}
|
|
101
|
+
if (byType.timing_fix > 0 || byType.element_stale > 0) {
|
|
102
|
+
recs.push("Aumente timeouts e use re-localização de elementos em listas dinâmicas.");
|
|
103
|
+
}
|
|
104
|
+
if (byType.selector_fix > 0 || byType.mobile_mapping_invisible > 0) {
|
|
105
|
+
recs.push("Priorize data-testid, role e seletores estáveis; em mobile, use mapeamento visível.");
|
|
106
|
+
}
|
|
107
|
+
if (successRate < 70) {
|
|
108
|
+
recs.push("Taxa de sucesso baixa: aplique waits inteligentes + assert final em cada teste.");
|
|
109
|
+
}
|
|
110
|
+
if (recs.length === 0) {
|
|
111
|
+
recs.push("Continue gerando testes. O agente está evoluindo bem.");
|
|
112
|
+
}
|
|
113
|
+
return recs;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-lab-agent",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.10",
|
|
4
|
+
"description": "Sistema de Inteligência em Qualidade de Software: executa, analisa, prevê, recomenda e aprende. Memória local + Learning Hub.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"learning-hub",
|
|
12
13
|
"slack-bot",
|
|
13
14
|
"qa-lab-agent.config.json",
|
|
14
15
|
"qa-lab-flows.json.example"
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
"build": "tsup",
|
|
18
19
|
"dev": "tsup --watch",
|
|
19
20
|
"slack-bot": "node slack-bot/src/index.js",
|
|
21
|
+
"slack-bot:check": "node slack-bot/check-config.js",
|
|
22
|
+
"learning-hub": "node learning-hub/src/server.js",
|
|
20
23
|
"test": "vitest run",
|
|
21
24
|
"test:watch": "vitest",
|
|
22
25
|
"test:coverage": "vitest run --coverage",
|
|
@@ -42,7 +45,7 @@
|
|
|
42
45
|
],
|
|
43
46
|
"repository": {
|
|
44
47
|
"type": "git",
|
|
45
|
-
"url": "https://github.com/Wesley-Gomes93/mcp-lab-agent"
|
|
48
|
+
"url": "git+https://github.com/Wesley-Gomes93/mcp-lab-agent.git"
|
|
46
49
|
},
|
|
47
50
|
"author": "Wesley Gomes",
|
|
48
51
|
"license": "MIT",
|
|
@@ -51,12 +54,12 @@
|
|
|
51
54
|
},
|
|
52
55
|
"dependencies": {
|
|
53
56
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
57
|
+
"@slack/bolt": "^3.21.0",
|
|
54
58
|
"dotenv": "^16.4.5",
|
|
55
59
|
"zod": "^3.25.0"
|
|
56
60
|
},
|
|
57
61
|
"optionalDependencies": {
|
|
58
|
-
"playwright": "^1.49.0"
|
|
59
|
-
"@slack/bolt": "^3.21.0"
|
|
62
|
+
"playwright": "^1.49.0"
|
|
60
63
|
},
|
|
61
64
|
"devDependencies": {
|
|
62
65
|
"@vitest/coverage-v8": "^2.1.0",
|
package/slack-bot/.env.example
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
# ===
|
|
1
|
+
# === CREDENCIAIS (api.slack.com/apps → sua app) ===
|
|
2
|
+
# Doc: https://docs.slack.dev/app-management/quickstart-app-settings
|
|
3
|
+
|
|
4
|
+
# OBRIGATÓRIO: Bot User OAuth Token
|
|
5
|
+
# Onde: OAuth & Permissions → OAuth Tokens → Bot User OAuth Token
|
|
2
6
|
SLACK_BOT_TOKEN=xoxb-...
|
|
3
|
-
|
|
7
|
+
|
|
8
|
+
# Escolha UM modo:
|
|
9
|
+
# A) Socket Mode — recomendado (PC corporativo, sem URL pública)
|
|
10
|
+
# Onde: Basic Information → App-Level Tokens → Generate (scope: connections:write)
|
|
11
|
+
SLACK_APP_TOKEN=xapp-...
|
|
12
|
+
|
|
13
|
+
# B) HTTP — precisa de ngrok
|
|
14
|
+
# Onde: Basic Information → App Credentials → Signing Secret (Show)
|
|
15
|
+
# SLACK_SIGNING_SECRET=...
|
|
4
16
|
|
|
5
17
|
# === REPO (se não configurou no qa-lab-agent.config.json) ===
|
|
6
18
|
# REPO_URL=https://github.com/sua-empresa/projeto.git
|
|
@@ -8,3 +20,6 @@ SLACK_SIGNING_SECRET=...
|
|
|
8
20
|
|
|
9
21
|
# === LLM (opcional - só para "crie testes") ===
|
|
10
22
|
# GROQ_API_KEY=...
|
|
23
|
+
|
|
24
|
+
# === AVANÇADO: caminho customizado do .env ===
|
|
25
|
+
# QA_LAB_ENV=/caminho/para/.env
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Credenciais — QA Lab Slack Bot
|
|
2
|
+
|
|
3
|
+
Referência rápida de onde obter cada credencial em [api.slack.com/apps](https://api.slack.com/apps).
|
|
4
|
+
|
|
5
|
+
Baseado em [Creating an app from app settings](https://docs.slack.dev/app-management/quickstart-app-settings).
|
|
6
|
+
|
|
7
|
+
## Tabela de credenciais
|
|
8
|
+
|
|
9
|
+
| Credencial | Chave no mcp.json | Chave no .env | Onde obter em api.slack.com |
|
|
10
|
+
|-----------------|-------------------|---------------------------|-----------------------------|
|
|
11
|
+
| Bot User OAuth | `botToken` | `SLACK_BOT_TOKEN` | OAuth & Permissions → OAuth Tokens → Bot User OAuth Token |
|
|
12
|
+
| App-Level Token | `appToken` | `SLACK_APP_TOKEN` | Basic Information → App-Level Tokens → Generate (scope: `connections:write`) |
|
|
13
|
+
| Signing Secret | `signingSecret` | `SLACK_SIGNING_SECRET` | Basic Information → App Credentials → Signing Secret (Show) |
|
|
14
|
+
|
|
15
|
+
## Formato dos tokens
|
|
16
|
+
|
|
17
|
+
- **Bot Token:** começa com `xoxb-` (não use Client Secret nem User Token)
|
|
18
|
+
- **App Token:** começa com `xapp-` (só para Socket Mode)
|
|
19
|
+
- **Signing Secret:** string longa (HTTP mode)
|
|
20
|
+
|
|
21
|
+
## Segurança
|
|
22
|
+
|
|
23
|
+
Mantenha as credenciais em segredo. Use variáveis de ambiente ou `~/.cursor/mcp.json` fora do versionamento.
|
package/slack-bot/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Bot Slack que analisa projetos e gera testes via **mcp-lab-agent**.
|
|
4
4
|
|
|
5
|
+
**Funciona em qualquer ambiente** — pessoal, corporativo, com ou sem firewall. Configure uma vez e rode em qualquer máquina.
|
|
6
|
+
|
|
7
|
+
| Ambiente | Modo recomendado | URL pública? |
|
|
8
|
+
|-----------------|------------------|------------------|
|
|
9
|
+
| PC corporativo | Socket Mode | Não precisa |
|
|
10
|
+
| PC pessoal | Socket ou HTTP | Só se usar HTTP |
|
|
11
|
+
| CI / servidor | Socket ou HTTP | Conforme rede |
|
|
12
|
+
|
|
5
13
|
## Rodar sem clonar o projeto
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -10,39 +18,87 @@ npx mcp-lab-agent slack-bot
|
|
|
10
18
|
|
|
11
19
|
Não precisa baixar o repo — o comando instala e executa o bot. Configure `~/.cursor/mcp.json` (ver seção abaixo).
|
|
12
20
|
|
|
13
|
-
## Configuração
|
|
21
|
+
## Configuração
|
|
22
|
+
|
|
23
|
+
Seguindo [Creating an app from app settings](https://docs.slack.dev/app-management/quickstart-app-settings):
|
|
14
24
|
|
|
15
|
-
### 1.
|
|
25
|
+
### 1. Criar o app no Slack
|
|
16
26
|
|
|
17
27
|
1. [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch**
|
|
18
|
-
2. **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
2. **App Name** e **Workspace** → **Create App**
|
|
29
|
+
|
|
30
|
+
### 2. Solicitar scopes (OAuth & Permissions)
|
|
31
|
+
|
|
32
|
+
1. **OAuth & Permissions** → **Scopes** → **Bot Token Scopes** → **Add an OAuth Scope**
|
|
33
|
+
2. Adicione:
|
|
34
|
+
- `app_mentions:read` — necessário para o evento `app_mention`
|
|
35
|
+
- `chat:write` — enviar mensagens
|
|
36
|
+
- `channels:read` — listar canais
|
|
37
|
+
- `channels:history` — ler histórico (para análise)
|
|
38
|
+
|
|
39
|
+
### 3. Instalar e autorizar o app
|
|
40
|
+
|
|
41
|
+
1. **OAuth & Permissions** → **Install to Workspace** → **Allow**
|
|
42
|
+
2. Copie o **Bot User OAuth Token** (`xoxb-...`) em **OAuth Tokens**
|
|
43
|
+
3. No canal desejado, digite: `/invite @NomeDoBot` — o bot precisa estar no canal para receber menções
|
|
44
|
+
|
|
45
|
+
### 4. Configurar eventos (Event Subscriptions)
|
|
46
|
+
|
|
47
|
+
1. **Event Subscriptions** → **Enable Events** → ON
|
|
48
|
+
2. **Subscribe to bot events** → **Add Bot User Event** → `app_mention`
|
|
49
|
+
3. Se alterou scopes/eventos: **Install App** → **Reinstall to Workspace**
|
|
50
|
+
|
|
51
|
+
### 5. Escolher modo: Socket ou HTTP
|
|
52
|
+
|
|
53
|
+
**A) Socket Mode** (recomendado — PC corporativo, sem URL pública):
|
|
22
54
|
|
|
23
|
-
|
|
55
|
+
1. **Socket Mode** → **Enable Socket Mode** → ON
|
|
56
|
+
2. **Basic Information** → **App-Level Tokens** → **Generate** → scope: `connections:write`
|
|
57
|
+
3. Copie o token (`xapp-...`)
|
|
24
58
|
|
|
25
|
-
**
|
|
59
|
+
**B) HTTP** (ngrok):
|
|
60
|
+
|
|
61
|
+
1. **Event Subscriptions** → **Request URL**: `https://SEU_DOMINIO/slack/events`
|
|
62
|
+
2. **Basic Information** → **App Credentials** → **Signing Secret** (Show)
|
|
63
|
+
|
|
64
|
+
### 6. Configurar credenciais localmente
|
|
65
|
+
|
|
66
|
+
**Socket Mode** — em `~/.cursor/mcp.json` ou `.env`:
|
|
26
67
|
|
|
27
68
|
```json
|
|
28
69
|
{
|
|
29
70
|
"qa-lab-agent": {
|
|
30
71
|
"slack": {
|
|
31
|
-
"botToken": "xoxb
|
|
32
|
-
"
|
|
72
|
+
"botToken": "xoxb-...",
|
|
73
|
+
"appToken": "xapp-...",
|
|
74
|
+
"useLocal": true,
|
|
75
|
+
"workDir": "/caminho/completo/para/projeto-com-testes"
|
|
33
76
|
}
|
|
34
77
|
}
|
|
35
78
|
}
|
|
36
79
|
```
|
|
37
80
|
|
|
38
|
-
|
|
81
|
+
> **Importante:** Com `useLocal: true`, defina `workDir` com o caminho absoluto do projeto. Caso contrário, o bot usa a pasta de onde foi iniciado e pode não encontrar os testes. Veja [TROUBLESHOOTING.md](./TROUBLESHOOTING.md#6-bot-identifica-o-projeto-mas-não-vê-os-testes).
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
| Credencial | Onde obter em api.slack.com |
|
|
84
|
+
|-------------|-----------------------------|
|
|
85
|
+
| `botToken` | OAuth & Permissions → OAuth Tokens → Bot User OAuth Token |
|
|
86
|
+
| `appToken` | Basic Information → App-Level Tokens (scope: connections:write) |
|
|
87
|
+
|
|
88
|
+
> Detalhes: [CREDENTIALS.md](./CREDENTIALS.md)
|
|
44
89
|
|
|
45
|
-
|
|
90
|
+
**HTTP** — para ngrok:
|
|
91
|
+
|
|
92
|
+
| Credencial | Onde obter em api.slack.com |
|
|
93
|
+
|-----------------|-----------------------------|
|
|
94
|
+
| `botToken` | OAuth & Permissions → OAuth Tokens → Bot User OAuth Token |
|
|
95
|
+
| `signingSecret` | Basic Information → App Credentials → Signing Secret |
|
|
96
|
+
|
|
97
|
+
**Via `.env`:** `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` (Socket) ou `SLACK_SIGNING_SECRET` (HTTP). O `.env` pode ficar em `slack-bot/` ou na pasta atual.
|
|
98
|
+
|
|
99
|
+
> **Importante:** Mantenha as credenciais em segredo. Use variáveis de ambiente ou `mcp.json` (fora do versionamento).
|
|
100
|
+
|
|
101
|
+
### 7. Configure o repositório
|
|
46
102
|
|
|
47
103
|
**Opção A** — No `qa-lab-agent.config.json` (raiz do projeto):
|
|
48
104
|
|
|
@@ -80,3 +136,5 @@ Local com ngrok: `ngrok http 3000` e use a URL em Event Subscriptions.
|
|
|
80
136
|
@QA Lab Bot analise o projeto
|
|
81
137
|
@QA Lab Bot crie testes para login
|
|
82
138
|
```
|
|
139
|
+
|
|
140
|
+
**Bot não responde?** Veja [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) — em geral: `/invite @NomeDoBot` no canal e conferir **Subscribe to bot events** → `app_mention`.
|