@renatocostaguedesdemorais/devs-loop-mcp 0.1.0

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/lib/api.cjs ADDED
@@ -0,0 +1,87 @@
1
+ // ============================================================
2
+ // DEVS-LOOP — ClickUp API Client
3
+ // Zero dependências externas — usa apenas Node.js nativo
4
+ // ============================================================
5
+
6
+ const https = require("https");
7
+ const path = require("path");
8
+ const fs = require("fs");
9
+ const { getHomeConfigDir, getPackageRoot, getProjectConfigDir } = require("./paths.cjs");
10
+
11
+ // Carregar .env manualmente (sem dotenv)
12
+ function loadEnv() {
13
+ const locations = [
14
+ path.join(getProjectConfigDir(), ".env"),
15
+ path.join(getHomeConfigDir(), ".env"),
16
+ path.join(process.cwd(), ".env"),
17
+ path.join(getPackageRoot(), ".env"),
18
+ ];
19
+
20
+ for (const loc of locations) {
21
+ if (fs.existsSync(loc)) {
22
+ const content = fs.readFileSync(loc, "utf8");
23
+ for (const line of content.split("\n")) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed || trimmed.startsWith("#")) continue;
26
+ const eqIndex = trimmed.indexOf("=");
27
+ if (eqIndex === -1) continue;
28
+ const key = trimmed.slice(0, eqIndex).trim();
29
+ const val = trimmed.slice(eqIndex + 1).trim();
30
+ if (!process.env[key]) process.env[key] = val;
31
+ }
32
+ break;
33
+ }
34
+ }
35
+ }
36
+
37
+ loadEnv();
38
+
39
+ const API_TOKEN = process.env.CLICKUP_API_TOKEN;
40
+
41
+ if (!API_TOKEN) {
42
+ console.error("❌ CLICKUP_API_TOKEN não definido.");
43
+ console.error(" Crie .devs-loop/.env com: CLICKUP_API_TOKEN=pk_...");
44
+ process.exit(1);
45
+ }
46
+
47
+ // Request genérico para a API do ClickUp
48
+ function request(method, endpoint, body = null) {
49
+ return new Promise((resolve, reject) => {
50
+ const options = {
51
+ hostname: "api.clickup.com",
52
+ path: `/api/v2${endpoint}`,
53
+ method,
54
+ headers: {
55
+ Authorization: API_TOKEN,
56
+ "Content-Type": "application/json",
57
+ },
58
+ };
59
+
60
+ const req = https.request(options, (res) => {
61
+ let data = "";
62
+ res.on("data", (chunk) => (data += chunk));
63
+ res.on("end", () => {
64
+ try {
65
+ resolve({ status: res.statusCode, data: JSON.parse(data) });
66
+ } catch {
67
+ resolve({ status: res.statusCode, data: data });
68
+ }
69
+ });
70
+ });
71
+
72
+ req.on("error", reject);
73
+
74
+ if (body) req.write(JSON.stringify(body));
75
+ req.end();
76
+ });
77
+ }
78
+
79
+ // Helpers
80
+ const api = {
81
+ get: (endpoint) => request("GET", endpoint),
82
+ post: (endpoint, body) => request("POST", endpoint, body),
83
+ put: (endpoint, body) => request("PUT", endpoint, body),
84
+ delete: (endpoint) => request("DELETE", endpoint),
85
+ };
86
+
87
+ module.exports = { api, loadEnv };
package/lib/coach.cjs ADDED
@@ -0,0 +1,222 @@
1
+ // ============================================================
2
+ // DEVS-LOOP — Coach (Guia Proativo)
3
+ // Observa, questiona e guia o dev — mas NUNCA bloqueia
4
+ // A palavra final é SEMPRE do dev
5
+ // ============================================================
6
+
7
+ const session = require("./session.cjs");
8
+ const learnings = require("./learnings.cjs");
9
+
10
+ // ─── Analisar e gerar nudges/alertas ───
11
+ function analyze(context = {}) {
12
+ const nudges = [];
13
+ const s = session.loadSession();
14
+ const l = learnings.load();
15
+
16
+ if (!s) return nudges;
17
+
18
+ // ─── 1. ESCOPO ───
19
+ if (context.currentFile && s.initiative) {
20
+ nudges.push(...checkScope(context, s));
21
+ }
22
+
23
+ // ─── 2. TEMPO ───
24
+ nudges.push(...checkTime(s, l));
25
+
26
+ // ─── 3. PADRÕES ───
27
+ nudges.push(...checkPatterns(context, s, l));
28
+
29
+ // ─── 4. QUALIDADE ───
30
+ nudges.push(...checkQuality(context, s));
31
+
32
+ // ─── 5. REGRAS CUSTOMIZADAS ───
33
+ nudges.push(...checkCustomRules(context, l));
34
+
35
+ return nudges;
36
+ }
37
+
38
+ // ─── Verificar escopo ───
39
+ function checkScope(context, s) {
40
+ const nudges = [];
41
+ const { currentFile, currentAction, filesChanged = [] } = context;
42
+
43
+ // Se está mexendo em muitos arquivos fora do padrão da iniciativa
44
+ if (filesChanged.length > 10) {
45
+ nudges.push({
46
+ type: "scope",
47
+ severity: "warning",
48
+ message: `⚠️ Você já mexeu em ${filesChanged.length} arquivos nesta sessão. A iniciativa era "${s.initiative}". Está tudo dentro do escopo ou surgiu algo novo?`,
49
+ suggestion: "Se surgiu algo fora do escopo, considere criar uma task separada para não misturar.",
50
+ });
51
+ }
52
+
53
+ return nudges;
54
+ }
55
+
56
+ // ─── Verificar tempo ───
57
+ function checkTime(s, l) {
58
+ const nudges = [];
59
+ const timer = session.timerStatus();
60
+
61
+ if (timer) {
62
+ // Verificar se está demorando mais que a média para o tipo
63
+ const avgTime = l.avgTimeByType;
64
+ const activeTask = s.taskIds?.find((t) => t.id === timer.taskId);
65
+
66
+ if (activeTask?.type && avgTime[activeTask.type]) {
67
+ const avg = avgTime[activeTask.type].avg;
68
+ if (timer.minutes > avg * 2 && timer.minutes > 30) {
69
+ nudges.push({
70
+ type: "time",
71
+ severity: "info",
72
+ message: `⏱️ Você está há ${timer.minutes}min nesta task. A média para ${activeTask.type} é ${avg}min. Está travado em algo?`,
73
+ suggestion: "Se estiver travado, considere: (1) criar um Spike separado, (2) pedir ajuda, ou (3) simplificar o escopo.",
74
+ });
75
+ }
76
+ }
77
+
78
+ // Alerta genérico se >60min sem concluir
79
+ if (timer.minutes > 60) {
80
+ nudges.push({
81
+ type: "time",
82
+ severity: "warning",
83
+ message: `⏱️ Já são ${timer.minutes}min na task atual. Talvez seja hora de quebrar em subtarefas menores?`,
84
+ suggestion: "Tasks grandes tendem a ser mal estimadas. Considere concluir o que já funciona e criar uma nova task para o restante.",
85
+ });
86
+ }
87
+ }
88
+
89
+ // Sessão muito longa sem pausa
90
+ if (s.startTimestamp) {
91
+ const sessionMinutes = Math.floor((Date.now() - s.startTimestamp) / 60000);
92
+ if (sessionMinutes > 180) {
93
+ nudges.push({
94
+ type: "wellbeing",
95
+ severity: "info",
96
+ message: `☕ Sessão ativa há ${Math.floor(sessionMinutes / 60)}h${sessionMinutes % 60}min. Já fez uma pausa?`,
97
+ suggestion: "Pausas regulares melhoram a qualidade do código. Considere um break de 10min.",
98
+ });
99
+ }
100
+ }
101
+
102
+ return nudges;
103
+ }
104
+
105
+ // ─── Verificar padrões ───
106
+ function checkPatterns(context, s, l) {
107
+ const nudges = [];
108
+
109
+ // Se o dev está criando muitas tasks Bug, algo pode estar errado
110
+ const currentBugs = (s.taskIds || []).filter((t) => t.type === "Bug").length;
111
+ if (currentBugs >= 3) {
112
+ nudges.push({
113
+ type: "pattern",
114
+ severity: "info",
115
+ message: `🐛 Já são ${currentBugs} bugs nesta sessão. Pode ser um sinal de que algo estrutural precisa de atenção.`,
116
+ suggestion: "Considere criar uma task de Refactor para resolver a causa raiz ao invés de corrigir sintomas.",
117
+ });
118
+ }
119
+
120
+ // Se o dev está investigando muito sem implementar
121
+ const spikes = (s.taskIds || []).filter((t) => t.type === "Spike").length;
122
+ const features = (s.taskIds || []).filter((t) => t.type === "Feature").length;
123
+ if (spikes >= 3 && features === 0) {
124
+ nudges.push({
125
+ type: "pattern",
126
+ severity: "info",
127
+ message: `🔍 Já foram ${spikes} investigações sem nenhuma Feature implementada. Já tem informação suficiente para começar?`,
128
+ suggestion: "Às vezes é melhor começar com um MVP simples do que investigar todas as possibilidades.",
129
+ });
130
+ }
131
+
132
+ // Se tem tasks criadas mas nenhuma concluída
133
+ if (s.tasksCreated >= 5 && s.tasksCompleted === 0) {
134
+ nudges.push({
135
+ type: "pattern",
136
+ severity: "warning",
137
+ message: `📋 ${s.tasksCreated} tasks criadas, nenhuma concluída. Está pulando entre tarefas?`,
138
+ suggestion: "Foco em concluir uma task antes de começar outra. Context switching reduz produtividade.",
139
+ });
140
+ }
141
+
142
+ return nudges;
143
+ }
144
+
145
+ // ─── Verificar qualidade ───
146
+ function checkQuality(context, s) {
147
+ const nudges = [];
148
+
149
+ // Se está criando Feature sem testes
150
+ if (context.hasNewFeature && !context.hasTests) {
151
+ nudges.push({
152
+ type: "quality",
153
+ severity: "suggestion",
154
+ message: "🧪 Feature nova sem testes detectados. Quer que eu crie uma task de QA para validação?",
155
+ suggestion: "Cada Feature deveria ter pelo menos uma task de QA correspondente.",
156
+ });
157
+ }
158
+
159
+ // Se não tem Spike mas é algo novo
160
+ if (context.isNewDomain && !context.hasSpikeInSession) {
161
+ nudges.push({
162
+ type: "quality",
163
+ severity: "suggestion",
164
+ message: "🔍 Parece que é a primeira vez trabalhando com isso. Faz sentido criar um Spike primeiro para investigar?",
165
+ suggestion: "Um Spike de 20-30min pode evitar horas de retrabalho.",
166
+ });
167
+ }
168
+
169
+ return nudges;
170
+ }
171
+
172
+ // ─── Verificar regras customizadas ───
173
+ function checkCustomRules(context, l) {
174
+ const nudges = [];
175
+ const activeRules = l.customRules?.filter((r) => r.active) || [];
176
+
177
+ for (const rule of activeRules) {
178
+ nudges.push({
179
+ type: "custom_rule",
180
+ severity: "reminder",
181
+ message: `📌 Lembrete: ${rule.rule}`,
182
+ });
183
+ }
184
+
185
+ return nudges;
186
+ }
187
+
188
+ // ─── Formatar nudges para exibição ───
189
+ function formatNudges(nudges) {
190
+ if (nudges.length === 0) return "";
191
+
192
+ const lines = ["\n┌─ 🧭 DEVS-LOOP Coach ─────────────────────"];
193
+
194
+ for (const n of nudges) {
195
+ lines.push(`│ ${n.message}`);
196
+ if (n.suggestion) {
197
+ lines.push(`│ 💡 ${n.suggestion}`);
198
+ }
199
+ lines.push("│");
200
+ }
201
+
202
+ lines.push("│ → Essas são sugestões. A decisão é sua.");
203
+ lines.push("└───────────────────────────────────────────\n");
204
+
205
+ return lines.join("\n");
206
+ }
207
+
208
+ // ─── Mensagem proativa baseada em contexto ───
209
+ function getProactiveMessage(context = {}) {
210
+ const nudges = analyze(context);
211
+ return nudges.length > 0 ? formatNudges(nudges) : "";
212
+ }
213
+
214
+ module.exports = {
215
+ analyze,
216
+ formatNudges,
217
+ getProactiveMessage,
218
+ checkScope,
219
+ checkTime,
220
+ checkPatterns,
221
+ checkQuality,
222
+ };
package/lib/config.cjs ADDED
@@ -0,0 +1,104 @@
1
+ // ============================================================
2
+ // DEVS-LOOP — Config Loader
3
+ // Carrega config.json e resolve IDs por nome
4
+ // ============================================================
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const { getHomeConfigDir, getPackageRoot, getProjectConfigDir } = require("./paths.cjs");
9
+
10
+ const CONFIG_PATHS = [
11
+ path.join(getProjectConfigDir(), "config.json"),
12
+ path.join(getHomeConfigDir(), "config.json"),
13
+ path.join(getPackageRoot(), "config.json"),
14
+ ];
15
+
16
+ let _config = null;
17
+
18
+ function load() {
19
+ if (_config) return _config;
20
+
21
+ for (const p of CONFIG_PATHS) {
22
+ if (fs.existsSync(p)) {
23
+ _config = JSON.parse(fs.readFileSync(p, "utf8"));
24
+ return _config;
25
+ }
26
+ }
27
+
28
+ console.error("❌ config.json não encontrado");
29
+ process.exit(1);
30
+ }
31
+
32
+ function get(key) {
33
+ const cfg = load();
34
+ return key.split(".").reduce((obj, k) => obj?.[k], cfg);
35
+ }
36
+
37
+ // Resolvedores de ID por nome legível
38
+ function resolveProject(name) {
39
+ const cfg = load();
40
+ const upper = name.toUpperCase();
41
+
42
+ // Busca exata
43
+ if (cfg.projetos[name]) return cfg.projetos[name];
44
+
45
+ // Busca case-insensitive
46
+ for (const [key, val] of Object.entries(cfg.projetos)) {
47
+ if (key.toUpperCase() === upper) return val;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ function resolveProjectList(name) {
54
+ const cfg = load();
55
+ const mapping = cfg.project_lists || {};
56
+ const upper = name.toUpperCase();
57
+
58
+ if (mapping[name]) {
59
+ return typeof mapping[name] === "string" ? mapping[name] : mapping[name]?.id || null;
60
+ }
61
+
62
+ for (const [key, val] of Object.entries(mapping)) {
63
+ if (key.toUpperCase() === upper) {
64
+ return typeof val === "string" ? val : val?.id || null;
65
+ }
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ function resolveSize(size) {
72
+ const cfg = load();
73
+ return cfg.labels?.tamanho?.[size] || null;
74
+ }
75
+
76
+ function resolveTypeLabel(taskType) {
77
+ const cfg = load();
78
+ const mapping = cfg.task_type_mapping?.[taskType];
79
+ if (!mapping) return null;
80
+ return cfg.labels?.tipos_tarefas?.[mapping.label] || null;
81
+ }
82
+
83
+ function resolveStructure(taskType) {
84
+ const cfg = load();
85
+ const mapping = cfg.task_type_mapping?.[taskType];
86
+ if (!mapping?.estrutura) return null;
87
+ return cfg.labels?.estrutura?.[mapping.estrutura] || null;
88
+ }
89
+
90
+ function listProjects() {
91
+ const cfg = load();
92
+ return Object.keys(cfg.projetos).sort();
93
+ }
94
+
95
+ module.exports = {
96
+ load,
97
+ get,
98
+ resolveProject,
99
+ resolveProjectList,
100
+ resolveSize,
101
+ resolveTypeLabel,
102
+ resolveStructure,
103
+ listProjects,
104
+ };
@@ -0,0 +1,215 @@
1
+ // ============================================================
2
+ // DEVS-LOOP — Learnings (Auto-aprendizado)
3
+ // Acumula conhecimento a cada sessão e se atualiza sozinho
4
+ // ============================================================
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const { ensureDir, getHomeConfigDir, getPackageRoot } = require("./paths.cjs");
9
+
10
+ const LEARNINGS_FILE = path.join(getHomeConfigDir(), "learnings.json");
11
+ const CONFIG_FILE = path.join(getHomeConfigDir(), "config.json");
12
+ const DEFAULT_CONFIG_FILE = path.join(getPackageRoot(), "config.json");
13
+
14
+ function ensureConfigSeeded() {
15
+ ensureDir(getHomeConfigDir());
16
+
17
+ if (!fs.existsSync(CONFIG_FILE) && fs.existsSync(DEFAULT_CONFIG_FILE)) {
18
+ fs.copyFileSync(DEFAULT_CONFIG_FILE, CONFIG_FILE);
19
+ }
20
+ }
21
+
22
+ function load() {
23
+ ensureDir(getHomeConfigDir());
24
+ if (fs.existsSync(LEARNINGS_FILE)) {
25
+ return JSON.parse(fs.readFileSync(LEARNINGS_FILE, "utf8"));
26
+ }
27
+ return {
28
+ version: 1,
29
+ lastUpdated: null,
30
+ sessions: { total: 0, history: [] },
31
+ patterns: {},
32
+ projectStats: {},
33
+ taskTypeStats: {},
34
+ avgTimeByType: {},
35
+ customRules: [],
36
+ knownIssues: [],
37
+ devPreferences: {},
38
+ };
39
+ }
40
+
41
+ function save(data) {
42
+ ensureDir(getHomeConfigDir());
43
+ data.lastUpdated = new Date().toISOString();
44
+ fs.writeFileSync(LEARNINGS_FILE, JSON.stringify(data, null, 2), "utf8");
45
+ }
46
+
47
+ // ─── Registrar fim de sessão ───
48
+ function recordSession(sessionData) {
49
+ const l = load();
50
+
51
+ l.sessions.total++;
52
+ l.sessions.history.push({
53
+ date: new Date().toISOString(),
54
+ project: sessionData.project,
55
+ initiative: sessionData.initiative,
56
+ tasksCreated: sessionData.tasksCreated,
57
+ tasksCompleted: sessionData.tasksCompleted,
58
+ totalMinutes: sessionData.totalMinutes || 0,
59
+ tasks: (sessionData.tasks || []).map((t) => ({
60
+ name: t.name,
61
+ type: t.type,
62
+ size: t.size,
63
+ completed: t.completed,
64
+ })),
65
+ });
66
+
67
+ // Manter apenas últimas 50 sessões
68
+ if (l.sessions.history.length > 50) {
69
+ l.sessions.history = l.sessions.history.slice(-50);
70
+ }
71
+
72
+ // Stats por projeto
73
+ const proj = sessionData.project;
74
+ if (!l.projectStats[proj]) {
75
+ l.projectStats[proj] = { sessions: 0, tasks: 0, totalMinutes: 0 };
76
+ }
77
+ l.projectStats[proj].sessions++;
78
+ l.projectStats[proj].tasks += sessionData.tasksCreated;
79
+ l.projectStats[proj].totalMinutes += sessionData.totalMinutes || 0;
80
+
81
+ save(l);
82
+ }
83
+
84
+ // ─── Registrar padrão observado ───
85
+ function recordPattern(patternKey, data) {
86
+ const l = load();
87
+
88
+ if (!l.patterns[patternKey]) {
89
+ l.patterns[patternKey] = { count: 0, firstSeen: new Date().toISOString(), data: [] };
90
+ }
91
+
92
+ l.patterns[patternKey].count++;
93
+ l.patterns[patternKey].lastSeen = new Date().toISOString();
94
+ l.patterns[patternKey].data.push(data);
95
+
96
+ // Manter últimos 20 por padrão
97
+ if (l.patterns[patternKey].data.length > 20) {
98
+ l.patterns[patternKey].data = l.patterns[patternKey].data.slice(-20);
99
+ }
100
+
101
+ save(l);
102
+ }
103
+
104
+ // ─── Registrar tempo médio por tipo de task ───
105
+ function recordTaskTime(taskType, minutes) {
106
+ const l = load();
107
+
108
+ if (!l.avgTimeByType[taskType]) {
109
+ l.avgTimeByType[taskType] = { total: 0, count: 0, avg: 0 };
110
+ }
111
+
112
+ l.avgTimeByType[taskType].total += minutes;
113
+ l.avgTimeByType[taskType].count++;
114
+ l.avgTimeByType[taskType].avg = Math.round(
115
+ l.avgTimeByType[taskType].total / l.avgTimeByType[taskType].count
116
+ );
117
+
118
+ save(l);
119
+ }
120
+
121
+ // ─── Auto-detectar novo projeto ───
122
+ function detectNewProject(projectName) {
123
+ ensureConfigSeeded();
124
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
125
+
126
+ // Verificar se já existe (case-insensitive)
127
+ const upper = projectName.toUpperCase();
128
+ for (const key of Object.keys(config.projetos)) {
129
+ if (key.toUpperCase() === upper) return false;
130
+ }
131
+
132
+ return true; // É novo
133
+ }
134
+
135
+ // ─── Adicionar projeto ao config ───
136
+ function addProject(projectName, projectId) {
137
+ ensureConfigSeeded();
138
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
139
+ config.projetos[projectName] = projectId;
140
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
141
+
142
+ recordPattern("new_project_added", { name: projectName, id: projectId });
143
+ console.log(`📦 Projeto '${projectName}' adicionado ao config.json`);
144
+ }
145
+
146
+ // ─── Registrar preferência do dev ───
147
+ function setPreference(key, value) {
148
+ const l = load();
149
+ l.devPreferences[key] = { value, updatedAt: new Date().toISOString() };
150
+ save(l);
151
+ }
152
+
153
+ function getPreference(key) {
154
+ const l = load();
155
+ return l.devPreferences[key]?.value || null;
156
+ }
157
+
158
+ // ─── Registrar regra customizada ───
159
+ function addCustomRule(rule) {
160
+ const l = load();
161
+ l.customRules.push({
162
+ rule,
163
+ addedAt: new Date().toISOString(),
164
+ active: true,
165
+ });
166
+ save(l);
167
+ console.log(`📝 Regra adicionada: "${rule}"`);
168
+ }
169
+
170
+ // ─── Registrar issue conhecida ───
171
+ function addKnownIssue(issue, resolution) {
172
+ const l = load();
173
+ l.knownIssues.push({
174
+ issue,
175
+ resolution,
176
+ addedAt: new Date().toISOString(),
177
+ });
178
+ save(l);
179
+ }
180
+
181
+ // ─── Gerar contexto para o agente ───
182
+ function getContextForAgent(project) {
183
+ const l = load();
184
+ const context = {
185
+ totalSessions: l.sessions.total,
186
+ avgTimeByType: l.avgTimeByType,
187
+ customRules: l.customRules.filter((r) => r.active).map((r) => r.rule),
188
+ devPreferences: l.devPreferences,
189
+ knownIssues: l.knownIssues.slice(-10),
190
+ };
191
+
192
+ if (project && l.projectStats[project]) {
193
+ context.projectHistory = l.projectStats[project];
194
+ }
195
+
196
+ // Últimas 5 sessões
197
+ context.recentSessions = l.sessions.history.slice(-5);
198
+
199
+ return context;
200
+ }
201
+
202
+ module.exports = {
203
+ load,
204
+ save,
205
+ recordSession,
206
+ recordPattern,
207
+ recordTaskTime,
208
+ detectNewProject,
209
+ addProject,
210
+ setPreference,
211
+ getPreference,
212
+ addCustomRule,
213
+ addKnownIssue,
214
+ getContextForAgent,
215
+ };