@dalmasonto/taskflow-mcp 1.0.0 → 1.0.2
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/dist/tools/agent.js +26 -46
- package/dist/tools/tasks.js +49 -2
- package/dist/tools/timer.js +18 -6
- package/package.json +1 -1
package/dist/tools/agent.js
CHANGED
|
@@ -10,59 +10,39 @@ export async function getAgentInstructions() {
|
|
|
10
10
|
const blockedCount = db.prepare("SELECT COUNT(*) AS c FROM tasks WHERE status = 'blocked'").get().c;
|
|
11
11
|
const unreadNotifs = db.prepare('SELECT COUNT(*) AS c FROM notifications WHERE read = 0').get().c;
|
|
12
12
|
const instructions = {
|
|
13
|
-
role: '
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'Call list_tasks with status "blocked" to see what is stuck and might need your help.',
|
|
19
|
-
'Call list_notifications with unread_only=true to check for pending notifications.',
|
|
13
|
+
role: 'TaskFlow — local-first task & time tracker with MCP integration.',
|
|
14
|
+
startup: [
|
|
15
|
+
'search_projects to find the current project (try name variants; confirm with user if ambiguous)',
|
|
16
|
+
'list_tasks status="in_progress" and status="blocked"',
|
|
17
|
+
'list_notifications unread_only=true',
|
|
20
18
|
],
|
|
21
|
-
|
|
19
|
+
state: {
|
|
22
20
|
projects: projectCount,
|
|
23
|
-
|
|
21
|
+
tasks: taskCount,
|
|
24
22
|
in_progress: inProgressCount,
|
|
25
23
|
blocked: blockedCount,
|
|
26
|
-
|
|
24
|
+
unread: unreadNotifs,
|
|
27
25
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
'
|
|
40
|
-
|
|
41
|
-
'
|
|
42
|
-
'Use Markdown in description fields — headings, bullet lists, code blocks, bold/italic. Task and project descriptions render Markdown in the UI, so well-formatted descriptions are more readable for the user.',
|
|
43
|
-
'When debugging, use the log_debug tool to record your process — what you are investigating, what you tried, error messages, hypotheses, and findings. Link it to the relevant task with task_id. This creates a visible breadcrumb trail in the Activity Pulse that helps the user understand your reasoning.',
|
|
26
|
+
rules: [
|
|
27
|
+
// Task tracking
|
|
28
|
+
'Proactively create tasks for ALL substantial work (features, bugs, refactors). search_tasks first to avoid duplicates. Link tasks to the confirmed project.',
|
|
29
|
+
'Timer lifecycle: start_timer → work → stop_timer(final_status). Use "done"/"partial_done"/"blocked". pause_timer when waiting for input.',
|
|
30
|
+
'MUST stop_timer with "done" when work is complete. Never leave finished tasks in "in_progress" or "paused".',
|
|
31
|
+
// Dependencies
|
|
32
|
+
'Check task dependencies before starting. If any dep is incomplete, set task to "blocked".',
|
|
33
|
+
'After completing a task, check if blocked tasks depending on it can be unblocked.',
|
|
34
|
+
// Prioritization
|
|
35
|
+
'When user is unsure what to work on: list_tasks priority="critical"/"high" status="not_started".',
|
|
36
|
+
// Logging
|
|
37
|
+
'Use log_debug with task_id to record debugging steps, hypotheses, findings, decisions. Use Markdown formatting.',
|
|
38
|
+
// Formatting
|
|
39
|
+
'Use Markdown in descriptions — headings, bullets, code blocks, bold. The UI renders it.',
|
|
44
40
|
],
|
|
45
|
-
|
|
46
|
-
description: 'The standard lifecycle of a task through the system',
|
|
47
|
-
flow: 'not_started → in_progress (start_timer) → paused (pause_timer) → in_progress (start_timer) → done/partial_done/blocked (stop_timer)',
|
|
48
|
-
valid_transitions: {
|
|
49
|
-
not_started: ['in_progress', 'blocked'],
|
|
50
|
-
in_progress: ['paused', 'blocked', 'partial_done', 'done'],
|
|
51
|
-
paused: ['in_progress', 'blocked', 'partial_done', 'done'],
|
|
52
|
-
blocked: ['not_started', 'in_progress'],
|
|
53
|
-
partial_done: ['in_progress', 'done'],
|
|
54
|
-
done: ['in_progress'],
|
|
55
|
-
},
|
|
56
|
-
},
|
|
41
|
+
workflow: 'not_started → in_progress (start_timer) → paused (pause_timer) → done/partial_done/blocked (stop_timer)',
|
|
57
42
|
tips: [
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'Use search_tasks to find tasks by keyword when you are not sure of the exact task ID.',
|
|
62
|
-
'Use search_projects to find a project by name. Try the repo name, directory name, or common abbreviations.',
|
|
63
|
-
'Read task descriptions carefully — they often contain implementation details, acceptance criteria, or context that will help you do better work.',
|
|
64
|
-
'Write task descriptions in Markdown: use ## headings for sections, - bullet lists for steps, ```code blocks``` for snippets, and **bold** for emphasis. The UI renders Markdown natively.',
|
|
65
|
-
'When creating tasks that depend on others, set dependencies and use the "blocked" status to indicate the blocking relationship. This shows up in the dependency graph.',
|
|
43
|
+
'Filter by tags (list_tasks tag="bug"), search by keyword (search_tasks), get full detail (get_task id).',
|
|
44
|
+
'get_analytics for time spent & completion rates. Dependencies show in the dependency graph.',
|
|
45
|
+
'list_tasks/search_tasks return compact summaries. Use get_task(id) to read full descriptions.',
|
|
66
46
|
],
|
|
67
47
|
};
|
|
68
48
|
return successResponse(instructions);
|
package/dist/tools/tasks.js
CHANGED
|
@@ -10,6 +10,28 @@ function parseTask(row) {
|
|
|
10
10
|
tags: JSON.parse(row.tags),
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
+
/** Compact task for list/search responses — omits description, null fields, and empty arrays */
|
|
14
|
+
function parseTaskCompact(row) {
|
|
15
|
+
const tags = JSON.parse(row.tags);
|
|
16
|
+
const deps = JSON.parse(row.dependencies);
|
|
17
|
+
const result = {
|
|
18
|
+
id: row.id,
|
|
19
|
+
title: row.title,
|
|
20
|
+
status: row.status,
|
|
21
|
+
priority: row.priority,
|
|
22
|
+
};
|
|
23
|
+
if (row.project_id != null)
|
|
24
|
+
result.project_id = row.project_id;
|
|
25
|
+
if (tags.length > 0)
|
|
26
|
+
result.tags = tags;
|
|
27
|
+
if (deps.length > 0)
|
|
28
|
+
result.dependencies = deps;
|
|
29
|
+
if (row.due_date != null)
|
|
30
|
+
result.due_date = row.due_date;
|
|
31
|
+
if (row.estimated_time != null)
|
|
32
|
+
result.estimated_time = row.estimated_time;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
13
35
|
function detectCycle(taskId, proposedDeps) {
|
|
14
36
|
const db = getDb();
|
|
15
37
|
const allTasks = db.prepare('SELECT id, dependencies FROM tasks').all();
|
|
@@ -116,7 +138,7 @@ export async function listTasks(params) {
|
|
|
116
138
|
return tags.includes(params.tag);
|
|
117
139
|
});
|
|
118
140
|
}
|
|
119
|
-
return successResponse(rows.map(
|
|
141
|
+
return successResponse(rows.map(parseTaskCompact));
|
|
120
142
|
}
|
|
121
143
|
export async function getTask(params) {
|
|
122
144
|
const db = getDb();
|
|
@@ -236,6 +258,31 @@ export async function updateTaskStatus(params) {
|
|
|
236
258
|
return errorResponse(`Cannot transition from "${currentStatus}" to "${params.status}"`, 'INVALID_TRANSITION');
|
|
237
259
|
}
|
|
238
260
|
const ts = now();
|
|
261
|
+
// Auto-start a timer session when entering in_progress
|
|
262
|
+
if (params.status === 'in_progress') {
|
|
263
|
+
const existingOpen = db.prepare('SELECT id FROM sessions WHERE task_id = ? AND end IS NULL').get(params.id);
|
|
264
|
+
if (!existingOpen) {
|
|
265
|
+
const result = db.prepare('INSERT INTO sessions (task_id, start, end) VALUES (?, ?, ?)').run(params.id, ts, null);
|
|
266
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(result.lastInsertRowid);
|
|
267
|
+
logActivity('timer_started', row.title, { entityType: 'task', entityId: params.id });
|
|
268
|
+
broadcastChange('timer', 'timer_started', { task_id: params.id, session, task_status: 'in_progress' });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Auto-close any active timer session when leaving an active state
|
|
272
|
+
const terminalStatuses = ['done', 'partial_done', 'blocked', 'paused'];
|
|
273
|
+
if (terminalStatuses.includes(params.status)) {
|
|
274
|
+
const openSession = db.prepare('SELECT * FROM sessions WHERE task_id = ? AND end IS NULL').get(params.id);
|
|
275
|
+
if (openSession) {
|
|
276
|
+
db.prepare('UPDATE sessions SET end = ? WHERE id = ?').run(ts, openSession.id);
|
|
277
|
+
const duration = new Date(ts).getTime() - new Date(openSession.start).getTime();
|
|
278
|
+
logActivity('timer_stopped', row.title, {
|
|
279
|
+
detail: `Auto-stopped: duration ${duration}ms, status → ${params.status}`,
|
|
280
|
+
entityType: 'task',
|
|
281
|
+
entityId: params.id,
|
|
282
|
+
});
|
|
283
|
+
broadcastChange('timer', 'timer_stopped', { task_id: params.id, session: { ...openSession, end: ts, duration }, task_status: params.status });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
239
286
|
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?').run(params.status, ts, params.id);
|
|
240
287
|
// Log appropriate action
|
|
241
288
|
let statusAction;
|
|
@@ -295,7 +342,7 @@ export async function searchTasks(params) {
|
|
|
295
342
|
const db = getDb();
|
|
296
343
|
const like = `%${params.query}%`;
|
|
297
344
|
const rows = db.prepare(`SELECT * FROM tasks WHERE title LIKE ? COLLATE NOCASE OR description LIKE ? COLLATE NOCASE`).all(like, like);
|
|
298
|
-
return successResponse(rows.map(
|
|
345
|
+
return successResponse(rows.map(parseTaskCompact));
|
|
299
346
|
}
|
|
300
347
|
// ─── MCP registration ─────────────────────────────────────────────────
|
|
301
348
|
export function registerTaskTools(server) {
|
package/dist/tools/timer.js
CHANGED
|
@@ -59,13 +59,25 @@ export async function stopTimer(params) {
|
|
|
59
59
|
return errorResponse('Task not found', 'NOT_FOUND');
|
|
60
60
|
// Find open session
|
|
61
61
|
const session = db.prepare('SELECT * FROM sessions WHERE task_id = ? AND end IS NULL').get(params.task_id);
|
|
62
|
-
if (!session)
|
|
63
|
-
return errorResponse('No active timer session for this task', 'NO_ACTIVE_SESSION');
|
|
64
62
|
const finalStatus = params.final_status ?? 'done';
|
|
65
63
|
const endTime = now();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
let duration;
|
|
65
|
+
let closedSession;
|
|
66
|
+
if (session) {
|
|
67
|
+
// Normal path — close the active session
|
|
68
|
+
duration = new Date(endTime).getTime() - new Date(session.start).getTime();
|
|
69
|
+
db.prepare('UPDATE sessions SET end = ? WHERE id = ?').run(endTime, session.id);
|
|
70
|
+
closedSession = { ...session, end: endTime };
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Fallback — session was already closed (e.g. by orphan cleanup or update_task_status)
|
|
74
|
+
// Still update the task status so the caller's intent is honored
|
|
75
|
+
const lastSession = db.prepare('SELECT * FROM sessions WHERE task_id = ? ORDER BY start DESC LIMIT 1').get(params.task_id);
|
|
76
|
+
if (!lastSession)
|
|
77
|
+
return errorResponse('No timer sessions found for this task', 'NO_ACTIVE_SESSION');
|
|
78
|
+
duration = new Date(lastSession.end).getTime() - new Date(lastSession.start).getTime();
|
|
79
|
+
closedSession = lastSession;
|
|
80
|
+
}
|
|
69
81
|
// Update task to final_status
|
|
70
82
|
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?').run(finalStatus, endTime, params.task_id);
|
|
71
83
|
logActivity('timer_stopped', task.title, {
|
|
@@ -80,7 +92,7 @@ export async function stopTimer(params) {
|
|
|
80
92
|
else if (finalStatus === 'partial_done') {
|
|
81
93
|
logActivity('task_partial_done', task.title, { entityType: 'task', entityId: params.task_id });
|
|
82
94
|
}
|
|
83
|
-
const stoppedSession = { ...
|
|
95
|
+
const stoppedSession = { ...closedSession, duration };
|
|
84
96
|
broadcastChange('timer', 'timer_stopped', { task_id: params.task_id, session: stoppedSession, task_status: finalStatus });
|
|
85
97
|
return successResponse(stoppedSession);
|
|
86
98
|
}
|