@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/.env.example +10 -0
- package/README.md +166 -0
- package/cli.cjs +640 -0
- package/config.json +101 -0
- package/devs-loop.md +677 -0
- package/lib/api.cjs +87 -0
- package/lib/coach.cjs +222 -0
- package/lib/config.cjs +104 -0
- package/lib/learnings.cjs +215 -0
- package/lib/listResolver.cjs +370 -0
- package/lib/paths.cjs +37 -0
- package/lib/progress.cjs +157 -0
- package/lib/session.cjs +220 -0
- package/lib/task.cjs +365 -0
- package/mcp-server.cjs +464 -0
- package/package.json +44 -0
package/lib/session.cjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// DEVS-LOOP — Session Manager
|
|
3
|
+
// Estado persiste em .devs-loop/.session.json
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { ensureDir, getHomeConfigDir, getProjectConfigDir } = require("./paths.cjs");
|
|
9
|
+
|
|
10
|
+
const SESSION_PATHS = [
|
|
11
|
+
path.join(getProjectConfigDir(), ".session.json"),
|
|
12
|
+
path.join(getHomeConfigDir(), ".session.json"),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const LOG_PATHS = [
|
|
16
|
+
path.join(getProjectConfigDir(), ".session.log"),
|
|
17
|
+
path.join(getHomeConfigDir(), ".session.log"),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function formatMinutes(totalMinutes = 0) {
|
|
21
|
+
if (!totalMinutes || totalMinutes <= 0) return "0h 0min";
|
|
22
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
23
|
+
const minutes = totalMinutes % 60;
|
|
24
|
+
return `${hours}h ${minutes}min`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findTaskEntry(sessionData, taskId) {
|
|
28
|
+
return sessionData.taskIds.find((task) => task.id === taskId) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addTrackedMinutes(sessionData, taskId, minutes) {
|
|
32
|
+
if (!taskId || !minutes || minutes <= 0) return;
|
|
33
|
+
const task = findTaskEntry(sessionData, taskId);
|
|
34
|
+
if (!task) return;
|
|
35
|
+
task.minutes = (task.minutes || 0) + minutes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getSessionPath() {
|
|
39
|
+
const projectDir = path.dirname(SESSION_PATHS[0]);
|
|
40
|
+
try {
|
|
41
|
+
ensureDir(projectDir);
|
|
42
|
+
return SESSION_PATHS[0];
|
|
43
|
+
} catch {
|
|
44
|
+
ensureDir(path.dirname(SESSION_PATHS[1]));
|
|
45
|
+
return SESSION_PATHS[1];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getLogPath() {
|
|
50
|
+
const projectDir = path.dirname(LOG_PATHS[0]);
|
|
51
|
+
try {
|
|
52
|
+
ensureDir(projectDir);
|
|
53
|
+
return LOG_PATHS[0];
|
|
54
|
+
} catch {
|
|
55
|
+
ensureDir(path.dirname(LOG_PATHS[1]));
|
|
56
|
+
return LOG_PATHS[1];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadSession() {
|
|
61
|
+
const p = getSessionPath();
|
|
62
|
+
if (fs.existsSync(p)) {
|
|
63
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function saveSession(data) {
|
|
69
|
+
fs.writeFileSync(getSessionPath(), JSON.stringify(data, null, 2), "utf8");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearSession() {
|
|
73
|
+
const p = getSessionPath();
|
|
74
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
75
|
+
|
|
76
|
+
const l = getLogPath();
|
|
77
|
+
if (fs.existsSync(l)) fs.unlinkSync(l);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function log(message) {
|
|
81
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
82
|
+
fs.appendFileSync(getLogPath(), `[${timestamp}] ${message}\n`, "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function init({ project, projectId, initiative, listId }) {
|
|
86
|
+
const session = {
|
|
87
|
+
project,
|
|
88
|
+
projectId,
|
|
89
|
+
initiative: initiative || "Sessão geral",
|
|
90
|
+
listId,
|
|
91
|
+
startedAt: new Date().toISOString(),
|
|
92
|
+
startTimestamp: Date.now(),
|
|
93
|
+
tasksCreated: 0,
|
|
94
|
+
tasksCompleted: 0,
|
|
95
|
+
taskIds: [],
|
|
96
|
+
activeTask: null,
|
|
97
|
+
timerStart: null,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
saveSession(session);
|
|
101
|
+
log(`SESSION_INIT project=${project} initiative='${session.initiative}'`);
|
|
102
|
+
return session;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addTask(taskId, taskName, meta = {}) {
|
|
106
|
+
const s = loadSession();
|
|
107
|
+
if (!s) return;
|
|
108
|
+
|
|
109
|
+
s.tasksCreated++;
|
|
110
|
+
s.taskIds.push({
|
|
111
|
+
id: taskId,
|
|
112
|
+
name: taskName,
|
|
113
|
+
type: meta.type || null,
|
|
114
|
+
size: meta.size || null,
|
|
115
|
+
parent: meta.parent || null,
|
|
116
|
+
listId: meta.listId || s.listId || null,
|
|
117
|
+
createdAt: new Date().toISOString(),
|
|
118
|
+
completed: false,
|
|
119
|
+
minutes: 0,
|
|
120
|
+
});
|
|
121
|
+
saveSession(s);
|
|
122
|
+
log(`TASK_CREATED id=${taskId} name='${taskName}'`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function completeTask(taskId) {
|
|
126
|
+
const s = loadSession();
|
|
127
|
+
if (!s) return;
|
|
128
|
+
|
|
129
|
+
const task = s.taskIds.find((t) => t.id === taskId);
|
|
130
|
+
if (task && !task.completed) {
|
|
131
|
+
s.tasksCompleted++;
|
|
132
|
+
task.completed = true;
|
|
133
|
+
task.completedAt = new Date().toISOString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Parar timer se ativo nessa task
|
|
137
|
+
if (s.activeTask === taskId) {
|
|
138
|
+
const elapsed = s.timerStart ? Math.floor((Date.now() - s.timerStart) / 60000) : 0;
|
|
139
|
+
log(`TIMER_STOP task_id=${taskId} minutes=${elapsed}`);
|
|
140
|
+
addTrackedMinutes(s, taskId, elapsed);
|
|
141
|
+
s.activeTask = null;
|
|
142
|
+
s.timerStart = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveSession(s);
|
|
146
|
+
log(`TASK_COMPLETED id=${taskId}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function startTimer(taskId) {
|
|
150
|
+
const s = loadSession();
|
|
151
|
+
if (!s) return;
|
|
152
|
+
|
|
153
|
+
s.activeTask = taskId;
|
|
154
|
+
s.timerStart = Date.now();
|
|
155
|
+
saveSession(s);
|
|
156
|
+
log(`TIMER_START task_id=${taskId}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function stopTimer() {
|
|
160
|
+
const s = loadSession();
|
|
161
|
+
if (!s || !s.timerStart) return { minutes: 0, taskId: null };
|
|
162
|
+
|
|
163
|
+
const elapsed = Math.floor((Date.now() - s.timerStart) / 60000);
|
|
164
|
+
const taskId = s.activeTask;
|
|
165
|
+
|
|
166
|
+
log(`TIMER_STOP task_id=${taskId} minutes=${elapsed}`);
|
|
167
|
+
|
|
168
|
+
addTrackedMinutes(s, taskId, elapsed);
|
|
169
|
+
s.activeTask = null;
|
|
170
|
+
s.timerStart = null;
|
|
171
|
+
saveSession(s);
|
|
172
|
+
|
|
173
|
+
return { minutes: elapsed, taskId };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function timerStatus() {
|
|
177
|
+
const s = loadSession();
|
|
178
|
+
if (!s || !s.timerStart) return null;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
taskId: s.activeTask,
|
|
182
|
+
minutes: Math.floor((Date.now() - s.timerStart) / 60000),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function summary() {
|
|
187
|
+
const s = loadSession();
|
|
188
|
+
if (!s) return null;
|
|
189
|
+
|
|
190
|
+
const elapsed = Math.floor((Date.now() - s.startTimestamp) / 60000);
|
|
191
|
+
const trackedMinutes = (s.taskIds || []).reduce((sum, task) => sum + (task.minutes || 0), 0);
|
|
192
|
+
const totalMinutes = trackedMinutes > 0 ? trackedMinutes : elapsed;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
project: s.project,
|
|
196
|
+
initiative: s.initiative,
|
|
197
|
+
listId: s.listId,
|
|
198
|
+
tasksCreated: s.tasksCreated,
|
|
199
|
+
tasksCompleted: s.tasksCompleted,
|
|
200
|
+
tasksPending: s.tasksCreated - s.tasksCompleted,
|
|
201
|
+
totalMinutes,
|
|
202
|
+
totalTime: formatMinutes(totalMinutes),
|
|
203
|
+
tasks: s.taskIds || [],
|
|
204
|
+
date: new Date().toLocaleDateString("pt-BR"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
loadSession,
|
|
210
|
+
saveSession,
|
|
211
|
+
clearSession,
|
|
212
|
+
log,
|
|
213
|
+
init,
|
|
214
|
+
addTask,
|
|
215
|
+
completeTask,
|
|
216
|
+
startTimer,
|
|
217
|
+
stopTimer,
|
|
218
|
+
timerStatus,
|
|
219
|
+
summary,
|
|
220
|
+
};
|
package/lib/task.cjs
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// DEVS-LOOP - Task Operations
|
|
3
|
+
// Create, complete and update ClickUp tasks
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
const { api } = require("./api.cjs");
|
|
7
|
+
const config = require("./config.cjs");
|
|
8
|
+
const session = require("./session.cjs");
|
|
9
|
+
const listResolver = require("./listResolver.cjs");
|
|
10
|
+
|
|
11
|
+
let customItemCache = null;
|
|
12
|
+
|
|
13
|
+
function normalize(value = "") {
|
|
14
|
+
return String(value)
|
|
15
|
+
.normalize("NFD")
|
|
16
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getTask(taskId) {
|
|
22
|
+
const res = await api.get(`/task/${taskId}`);
|
|
23
|
+
return res.data || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getList(listId) {
|
|
27
|
+
const res = await api.get(`/list/${listId}`);
|
|
28
|
+
return res.data || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function loadCustomItems() {
|
|
32
|
+
if (customItemCache) return customItemCache;
|
|
33
|
+
|
|
34
|
+
const cfg = config.load();
|
|
35
|
+
const res = await api.get(`/team/${cfg.workspace_id}/custom_item`);
|
|
36
|
+
customItemCache = res.data?.custom_items || [];
|
|
37
|
+
return customItemCache;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function resolveCustomItemId(taskType) {
|
|
41
|
+
const normalizedTaskType = normalize(taskType);
|
|
42
|
+
const items = await loadCustomItems();
|
|
43
|
+
const match = items.find((item) => normalize(item.name) === normalizedTaskType);
|
|
44
|
+
return match?.id || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function ensureCustomFieldValue(taskId, fieldId, value) {
|
|
48
|
+
const res = await api.post(`/task/${taskId}/field/${fieldId}`, { value });
|
|
49
|
+
return res.status >= 200 && res.status < 300;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ensureCustomFields(taskId, customFields) {
|
|
53
|
+
for (const field of customFields) {
|
|
54
|
+
try {
|
|
55
|
+
await ensureCustomFieldValue(taskId, field.id, field.value);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.log(`Warning: could not guarantee custom field ${field.id} on task ${taskId}.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function flattenChecklistItems(items = []) {
|
|
63
|
+
const flat = [];
|
|
64
|
+
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
if (!item) continue;
|
|
67
|
+
flat.push(item);
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
70
|
+
const nestedObjects = item.children.filter((child) => child && typeof child === "object");
|
|
71
|
+
flat.push(...flattenChecklistItems(nestedObjects));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return flat;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolveNativeChecklists(taskId) {
|
|
79
|
+
const task = await getTask(taskId);
|
|
80
|
+
const checklists = task?.checklists || [];
|
|
81
|
+
let resolvedCount = 0;
|
|
82
|
+
|
|
83
|
+
for (const checklist of checklists) {
|
|
84
|
+
const items = flattenChecklistItems(checklist.items || []);
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
if (item.resolved) continue;
|
|
87
|
+
|
|
88
|
+
await api.put(`/checklist/${checklist.id}/checklist_item/${item.id}`, {
|
|
89
|
+
resolved: true,
|
|
90
|
+
});
|
|
91
|
+
resolvedCount += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return resolvedCount;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findMatchingStatus(listData, desiredStatusName) {
|
|
99
|
+
const desired = normalize(desiredStatusName);
|
|
100
|
+
return (listData?.statuses || []).find((status) => normalize(status.status) === desired) || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findClosedStatus(listData) {
|
|
104
|
+
return (listData?.statuses || []).find((status) => status.type === "closed") || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function resolveStartStatus(taskId, fallbackStatusName) {
|
|
108
|
+
const task = await getTask(taskId);
|
|
109
|
+
const listData = await getList(task.list.id);
|
|
110
|
+
const exact = findMatchingStatus(listData, fallbackStatusName);
|
|
111
|
+
if (exact) return exact.status;
|
|
112
|
+
|
|
113
|
+
const inProgress = (listData?.statuses || []).find((status) => normalize(status.status).includes("andamento"));
|
|
114
|
+
return inProgress?.status || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function resolveDoneStatus(taskId, fallbackStatusName) {
|
|
118
|
+
const task = await getTask(taskId);
|
|
119
|
+
const listData = await getList(task.list.id);
|
|
120
|
+
const exact = findMatchingStatus(listData, fallbackStatusName);
|
|
121
|
+
if (exact) return exact.status;
|
|
122
|
+
|
|
123
|
+
const closed = findClosedStatus(listData);
|
|
124
|
+
return closed?.status || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildMarkdownDescription(description, checklist = []) {
|
|
128
|
+
let md = `**O que deve ser feito?**\n${description || "..."}\n\n`;
|
|
129
|
+
|
|
130
|
+
// If we have native checklist support, avoid duplicating criteria in markdown.
|
|
131
|
+
if (checklist.length === 0) {
|
|
132
|
+
md += "**Critérios de Conclusão**\n";
|
|
133
|
+
md += "- [ ] Definir critérios nesta descrição quando não houver checklist nativa disponível.\n\n";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
md += "**Observações**\n...";
|
|
137
|
+
return md;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function createTask({
|
|
141
|
+
name,
|
|
142
|
+
type = "Feature",
|
|
143
|
+
size = "P",
|
|
144
|
+
project,
|
|
145
|
+
description,
|
|
146
|
+
checklist = [],
|
|
147
|
+
listId,
|
|
148
|
+
parent,
|
|
149
|
+
assignee,
|
|
150
|
+
}) {
|
|
151
|
+
if (!project) {
|
|
152
|
+
const s = session.loadSession();
|
|
153
|
+
project = s?.project;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!project) {
|
|
157
|
+
console.error("Project not defined. Use --project or start a session.");
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const projectId = config.resolveProject(project);
|
|
162
|
+
if (!projectId) {
|
|
163
|
+
console.error(`Project '${project}' not found. Use: devs-loop projects`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sizeId = config.resolveSize(size);
|
|
168
|
+
const typeLabelId = config.resolveTypeLabel(type);
|
|
169
|
+
const structureId = config.resolveStructure(type);
|
|
170
|
+
const cfg = config.load();
|
|
171
|
+
|
|
172
|
+
const customFields = [];
|
|
173
|
+
|
|
174
|
+
if (typeLabelId) {
|
|
175
|
+
customFields.push({
|
|
176
|
+
id: cfg.custom_fields.tipos_tarefas,
|
|
177
|
+
value: [typeLabelId],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
customFields.push({
|
|
182
|
+
id: cfg.custom_fields.projeto_produto,
|
|
183
|
+
value: projectId,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (sizeId) {
|
|
187
|
+
customFields.push({
|
|
188
|
+
id: cfg.custom_fields.tamanho_task,
|
|
189
|
+
value: [sizeId],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (structureId) {
|
|
194
|
+
customFields.push({
|
|
195
|
+
id: cfg.custom_fields.estrutura_projeto,
|
|
196
|
+
value: structureId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (parent) {
|
|
201
|
+
customFields.push({
|
|
202
|
+
id: cfg.custom_fields.ok,
|
|
203
|
+
value: cfg.labels.ok.SUBTAREFAS,
|
|
204
|
+
});
|
|
205
|
+
} else if (size === "G") {
|
|
206
|
+
customFields.push({
|
|
207
|
+
id: cfg.custom_fields.ok,
|
|
208
|
+
value: cfg.labels.ok.TAREFA_PAI,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let targetList = listId || session.loadSession()?.listId;
|
|
213
|
+
|
|
214
|
+
if (!targetList) {
|
|
215
|
+
try {
|
|
216
|
+
const resolvedList = await listResolver.resolveList({
|
|
217
|
+
project,
|
|
218
|
+
allowPrompt: true,
|
|
219
|
+
});
|
|
220
|
+
targetList = resolvedList.listId;
|
|
221
|
+
|
|
222
|
+
if (resolvedList.list) {
|
|
223
|
+
console.log(`Selected list: ${listResolver.formatListPath(resolvedList.list)} (${resolvedList.list.id})`);
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(`Error: ${error.message}`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const payload = {
|
|
232
|
+
name,
|
|
233
|
+
markdown_description: buildMarkdownDescription(description, checklist),
|
|
234
|
+
priority: cfg.default_priority,
|
|
235
|
+
custom_fields: customFields,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (parent) payload.parent = parent;
|
|
239
|
+
if (assignee) payload.assignees = [assignee];
|
|
240
|
+
|
|
241
|
+
const res = await api.post(`/list/${targetList}/task`, payload);
|
|
242
|
+
|
|
243
|
+
if (!res.data?.id) {
|
|
244
|
+
console.error("Failed to create task:", res.data?.err || res.data?.message || "unknown error");
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const taskId = res.data.id;
|
|
249
|
+
const taskUrl = res.data.url;
|
|
250
|
+
|
|
251
|
+
const customItemId = await resolveCustomItemId(type);
|
|
252
|
+
if (customItemId) {
|
|
253
|
+
await api.put(`/task/${taskId}`, { custom_item_id: customItemId }).catch(() => {});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await ensureCustomFields(taskId, customFields);
|
|
257
|
+
|
|
258
|
+
console.log(`Task created: ${taskUrl}`);
|
|
259
|
+
session.addTask(taskId, name, {
|
|
260
|
+
type,
|
|
261
|
+
size,
|
|
262
|
+
parent: parent || null,
|
|
263
|
+
listId: targetList,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (checklist.length > 0) {
|
|
267
|
+
try {
|
|
268
|
+
const clRes = await api.post(`/task/${taskId}/checklist`, {
|
|
269
|
+
name: "Critérios de Conclusão",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const checklistId = clRes.data?.checklist?.id;
|
|
273
|
+
if (checklistId) {
|
|
274
|
+
for (const item of checklist) {
|
|
275
|
+
await api.post(`/checklist/${checklistId}/checklist_item`, {
|
|
276
|
+
name: item,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
console.log(`Native checklist created with ${checklist.length} items`);
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.log("Warning: native checklist was not created.");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { id: taskId, url: taskUrl };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function completeTask(taskId, comment) {
|
|
290
|
+
const cfg = config.load();
|
|
291
|
+
|
|
292
|
+
const timer = session.stopTimer();
|
|
293
|
+
if (timer.minutes > 0) {
|
|
294
|
+
console.log(`Timer stopped: ${timer.minutes}min`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const resolvedCount = await resolveNativeChecklists(taskId);
|
|
299
|
+
if (resolvedCount > 0) {
|
|
300
|
+
console.log(`Native checklist resolved: ${resolvedCount} item(s)`);
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.log(`Warning: could not resolve native checklist items on task ${taskId}.`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const doneStatus = await resolveDoneStatus(taskId, cfg.default_status_done);
|
|
307
|
+
if (doneStatus) {
|
|
308
|
+
await api.put(`/task/${taskId}`, {
|
|
309
|
+
status: doneStatus,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (comment) {
|
|
314
|
+
await api.post(`/task/${taskId}/comment`, {
|
|
315
|
+
comment_text: `✅ ${comment}`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
session.completeTask(taskId);
|
|
320
|
+
console.log(`Task ${taskId} completed`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function startTimer(taskId) {
|
|
324
|
+
const cfg = config.load();
|
|
325
|
+
const currentTimer = session.timerStatus();
|
|
326
|
+
|
|
327
|
+
if (currentTimer?.taskId === taskId) {
|
|
328
|
+
console.log(`Timer already active for task ${taskId}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (currentTimer?.taskId && currentTimer.taskId !== taskId) {
|
|
333
|
+
await stopTimer();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await api.post(`/team/${cfg.workspace_id}/time_entries/start`, {
|
|
337
|
+
tid: taskId,
|
|
338
|
+
}).catch(() => {});
|
|
339
|
+
|
|
340
|
+
const startStatus = await resolveStartStatus(taskId, cfg.default_status_start);
|
|
341
|
+
if (startStatus) {
|
|
342
|
+
await api.put(`/task/${taskId}`, {
|
|
343
|
+
status: startStatus,
|
|
344
|
+
}).catch(() => {});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
session.startTimer(taskId);
|
|
348
|
+
console.log(`Timer started for task ${taskId}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function stopTimer() {
|
|
352
|
+
const cfg = config.load();
|
|
353
|
+
|
|
354
|
+
await api.post(`/team/${cfg.workspace_id}/time_entries/stop`, {}).catch(() => {});
|
|
355
|
+
|
|
356
|
+
const result = session.stopTimer();
|
|
357
|
+
if (result.taskId) {
|
|
358
|
+
console.log(`Timer stopped: ${result.minutes}min (task: ${result.taskId})`);
|
|
359
|
+
} else {
|
|
360
|
+
console.log("No active timer");
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = { createTask, completeTask, startTimer, stopTimer };
|