@dalmasonto/taskflow-mcp 1.0.33 → 2.0.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.
Files changed (99) hide show
  1. package/README.md +138 -193
  2. package/dist/attachment-download.d.ts +74 -0
  3. package/dist/attachment-download.js +193 -0
  4. package/dist/attachment-download.js.map +1 -0
  5. package/dist/attachments.d.ts +23 -0
  6. package/dist/attachments.js +66 -0
  7. package/dist/attachments.js.map +1 -0
  8. package/dist/client.d.ts +206 -0
  9. package/dist/client.js +279 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/config.d.ts +137 -18
  12. package/dist/config.js +187 -106
  13. package/dist/config.js.map +1 -0
  14. package/dist/connect.d.ts +89 -0
  15. package/dist/connect.js +269 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/doctor.d.ts +24 -0
  18. package/dist/doctor.js +120 -0
  19. package/dist/doctor.js.map +1 -0
  20. package/dist/events.d.ts +186 -0
  21. package/dist/events.js +407 -0
  22. package/dist/events.js.map +1 -0
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.js +124 -184
  25. package/dist/index.js.map +1 -0
  26. package/dist/instructions.d.ts +12 -0
  27. package/dist/instructions.js +114 -0
  28. package/dist/instructions.js.map +1 -0
  29. package/dist/mint.d.ts +62 -0
  30. package/dist/mint.js +135 -0
  31. package/dist/mint.js.map +1 -0
  32. package/dist/mirror.d.ts +68 -0
  33. package/dist/mirror.js +103 -0
  34. package/dist/mirror.js.map +1 -0
  35. package/dist/pane-queue.d.ts +29 -0
  36. package/dist/pane-queue.js +35 -0
  37. package/dist/pane-queue.js.map +1 -0
  38. package/dist/prompts.d.ts +79 -0
  39. package/dist/prompts.js +211 -0
  40. package/dist/prompts.js.map +1 -0
  41. package/dist/resolve.d.ts +72 -0
  42. package/dist/resolve.js +89 -0
  43. package/dist/resolve.js.map +1 -0
  44. package/dist/runtime.d.ts +54 -0
  45. package/dist/runtime.js +321 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/server.d.ts +56 -0
  48. package/dist/server.js +793 -0
  49. package/dist/server.js.map +1 -0
  50. package/dist/session-identifier.d.ts +48 -0
  51. package/dist/session-identifier.js +44 -0
  52. package/dist/session-identifier.js.map +1 -0
  53. package/dist/sessions-store.d.ts +38 -0
  54. package/dist/sessions-store.js +88 -0
  55. package/dist/sessions-store.js.map +1 -0
  56. package/dist/tmux.d.ts +200 -0
  57. package/dist/tmux.js +580 -0
  58. package/dist/tmux.js.map +1 -0
  59. package/hooks/metadata.mjs +99 -0
  60. package/hooks/permission-prompt.mjs +100 -0
  61. package/hooks/taskflow-hook.mjs +499 -0
  62. package/hooks/tool-logging.mjs +63 -0
  63. package/package.json +38 -29
  64. package/dist/agent-registry.d.ts +0 -27
  65. package/dist/agent-registry.js +0 -153
  66. package/dist/db.d.ts +0 -5
  67. package/dist/db.js +0 -220
  68. package/dist/helpers.d.ts +0 -21
  69. package/dist/helpers.js +0 -27
  70. package/dist/resources.d.ts +0 -2
  71. package/dist/resources.js +0 -89
  72. package/dist/sse.d.ts +0 -10
  73. package/dist/sse.js +0 -766
  74. package/dist/tmux-bridge.d.ts +0 -12
  75. package/dist/tmux-bridge.js +0 -157
  76. package/dist/tools/activity.d.ts +0 -39
  77. package/dist/tools/activity.js +0 -152
  78. package/dist/tools/agent-inbox.d.ts +0 -12
  79. package/dist/tools/agent-inbox.js +0 -257
  80. package/dist/tools/agent.d.ts +0 -14
  81. package/dist/tools/agent.js +0 -168
  82. package/dist/tools/analytics.d.ts +0 -21
  83. package/dist/tools/analytics.js +0 -191
  84. package/dist/tools/checkpoint.d.ts +0 -27
  85. package/dist/tools/checkpoint.js +0 -105
  86. package/dist/tools/notifications.d.ts +0 -31
  87. package/dist/tools/notifications.js +0 -59
  88. package/dist/tools/projects.d.ts +0 -55
  89. package/dist/tools/projects.js +0 -112
  90. package/dist/tools/settings.d.ts +0 -19
  91. package/dist/tools/settings.js +0 -73
  92. package/dist/tools/tasks.d.ts +0 -105
  93. package/dist/tools/tasks.js +0 -403
  94. package/dist/tools/terminal.d.ts +0 -4
  95. package/dist/tools/terminal.js +0 -98
  96. package/dist/tools/timer.d.ts +0 -37
  97. package/dist/tools/timer.js +0 -154
  98. package/dist/types.d.ts +0 -82
  99. package/dist/types.js +0 -30
@@ -1,168 +0,0 @@
1
- import { z } from 'zod';
2
- import { getDb } from '../db.js';
3
- import { logActivity, successResponse, broadcastChange } from '../helpers.js';
4
- // ─── exported handler functions ───────────────────────────────────────
5
- export async function getAgentInstructions() {
6
- const db = getDb();
7
- // Gather live context to include in the instructions
8
- const projectCount = db.prepare('SELECT COUNT(*) AS c FROM projects').get().c;
9
- const taskCount = db.prepare('SELECT COUNT(*) AS c FROM tasks').get().c;
10
- const inProgressCount = db.prepare("SELECT COUNT(*) AS c FROM tasks WHERE status = 'in_progress'").get().c;
11
- const blockedCount = db.prepare("SELECT COUNT(*) AS c FROM tasks WHERE status = 'blocked'").get().c;
12
- const unreadNotifs = db.prepare('SELECT COUNT(*) AS c FROM notifications WHERE read = 0').get().c;
13
- const instructions = {
14
- role: 'TaskFlow — local-first task & time tracker with MCP integration. Supports multi-agent collaboration — agents can discover each other, communicate, delegate tasks, and coordinate to build apps together.',
15
- startup: [
16
- 'Derive the project name from the **folder name** of the current working directory (e.g. `/home/user/projects/my-app` → search for "my-app"). Use search_projects with that name. If 2–3 results match, **ask the user** which project to use — never guess.',
17
- 'list_tasks status="in_progress" and status="blocked" for the confirmed project.',
18
- 'list_notifications unread_only=true',
19
- 'register_agent with a descriptive name for your role (e.g. "backend", "frontend", "lead"). Then list_agents to see who else is online. If 2+ disconnected agents exist for the same project path, ask the user which name to register as — the name preserves message history from previous sessions. If other agents are active on the same project, check_messages for any pending messages addressed to you.',
20
- ],
21
- state: {
22
- projects: projectCount,
23
- tasks: taskCount,
24
- in_progress: inProgressCount,
25
- blocked: blockedCount,
26
- unread: unreadNotifs,
27
- },
28
- rules: [
29
- // Project discovery
30
- 'The project name should be derived from the working directory folder name. Always search_projects first. If no match, create the project using create_project with the folder name. If multiple matches, present them to the user and ask which one to use.',
31
- // Task tracking
32
- 'Proactively create tasks for ALL substantial work — features, bugs, refactors, AND debugging/investigation. Debugging is real work: create a task for it (e.g. "Debug: SSE connection dropping"), start a timer, and track it the same way you would a feature. search_tasks first to avoid duplicates. Link tasks to the confirmed project.',
33
- 'Timer lifecycle: start_timer → work → stop_timer(final_status). Use "done"/"partial_done"/"blocked". pause_timer when waiting for input. This applies equally to debugging tasks — start a timer before investigating, stop it when resolved or blocked.',
34
- 'MUST stop_timer with "done" when work is complete. Never leave finished tasks in "in_progress" or "paused". This includes debugging tasks — when the bug is fixed or the investigation concludes, stop the timer.',
35
- // Bugs & post-build debugging
36
- 'When a user reports a bug or you discover one while testing/building: FIRST search_tasks for an existing related task. If found (even if "done"), move it back to in_progress (update_task_status) — this reopens the task and auto-starts a timer. Then log_debug on that task to document the new bug. If no related task exists, create a new one (e.g. "Bug: sidebar not rendering in light mode") with tag "bug", start the timer, and begin debugging.',
37
- 'This also applies to post-build issues: after running a build/test and seeing failures, do NOT silently fix them. Open or reopen a task first, log what failed, then fix. The task becomes the paper trail — the user and future agents can see what broke, what was tried, and how it was resolved.',
38
- // Dependencies
39
- 'Check task dependencies before starting. If any dep is incomplete, set task to "blocked".',
40
- 'After completing a task, check if blocked tasks depending on it can be unblocked.',
41
- // Prioritization
42
- 'When user is unsure what to work on: list_tasks priority="critical"/"high" status="not_started".',
43
- // Transparency — CRITICAL
44
- 'ALWAYS tell the user what commands you are running and why. Before executing a shell command, state: "Running: `<command>`". After significant actions (file edits, installs, config changes), summarize what changed. The user must be able to reconstruct what happened from your messages alone — they should never wonder "what did the agent do?".',
45
- 'When starting work, briefly state the approach: what you plan to do, which files you expect to touch, and what commands you will run. This gives the user a chance to course-correct before you act.',
46
- // Debug logging — comprehensive
47
- 'ALWAYS log your work using log_debug. Log with a task_id when working on a specific task, or with project_id for project-level observations. Use log_debug as a running journal — it shows up in the Activity Pulse and on the project page so the user (and future agents) can follow your reasoning.',
48
- 'What to log: **every stage** of your process. When you start investigating, when you form a hypothesis, when you read/edit a file, the exact commands you run (with output snippets), errors encountered, fixes attempted, and the resolution. Log the **path you took**, not just the destination.',
49
- 'Log early and often — not just at the end. A debug log entry per significant step (e.g. "Read `sse.ts:57` — found the healthz route exists but is unreachable because..."). Use Markdown: `## headings` for stages, `` `code blocks` `` for errors/paths/commands, **bold** for key findings.',
50
- 'For project-level notes (architecture decisions, setup gotchas, "how to run the app"), use log_debug with project_id instead of task_id. These appear on the project page and serve as living documentation.',
51
- // Formatting
52
- 'Use Markdown in descriptions — headings, bullets, code blocks, bold. The UI renders it.',
53
- // Agent Inbox — remote communication
54
- 'When you need user input (choices, confirmations, clarifications), ALWAYS do BOTH: (1) ask the question normally in the terminal conversation, AND (2) call ask_user to post it to the Agent Inbox. This lets the user respond from either the terminal or the TaskFlow UI remotely. If the user explicitly tells you not to use the inbox (e.g. "don\'t post to inbox"), skip the ask_user call.',
55
- 'After calling ask_user, continue your work if possible. The user may respond from the UI, and the response will be delivered to your terminal automatically (if running in tmux). You can also call check_response to poll for the answer.',
56
- // Agent Inbox — formatting
57
- 'When calling ask_user, write **proper titles** for the question field — clear, concise, well-formed sentences (not random fragments or debug-style text). The question is the headline the user sees first.',
58
- 'The `context` field in ask_user is rendered as Markdown in the UI. Format it well: use `## headings` to organize sections, `**bold**` for key terms, bullet lists for options/trade-offs, and `\\`code\\`` for file paths or commands. Use real newlines (not literal \\\\n). The context should read like a well-written message, not raw debug output.',
59
- // ── Multi-Agent Collaboration ──────────────────────────────────────
60
- // Identity & discovery
61
- 'On startup, call register_agent with a descriptive name that reflects your role (e.g. "backend", "frontend", "designer", "qa"). If the user assigns you a role, use that. This name is how other agents address you and how your message history is preserved across sessions.',
62
- 'Call list_agents to discover who else is online. If you see 2+ disconnected agents on the same project path, ask the user which name to register as — registering with an existing name resumes that agent\'s full message history. Before starting work, check if another agent is already working on the same project — coordinate instead of duplicating effort.',
63
- // Communication
64
- 'Use send_to_agent for fire-and-forget updates — status changes, "I finished task X", "file Y is ready for you". Use ask_agent when you need a response before proceeding — "Should I use REST or GraphQL for this endpoint?", "Is the auth middleware ready?".',
65
- 'When you receive a message from another agent (via check_messages), respond promptly using respond_to_message. Treat agent messages with the same priority as user messages.',
66
- 'Check for incoming messages (check_messages) periodically — at minimum: (1) when you finish a task, (2) before starting a new task, and (3) when you\'ve been working for a while without checking. Other agents may be blocked waiting for your response.',
67
- // Collaborative app building — the coordination pattern
68
- 'When multiple agents collaborate on building an app, follow this coordination pattern:\n' +
69
- ' 1. **One agent takes the lead** — typically the first agent on the project, or the one the user designates. The lead agent creates the project (if needed), defines the task breakdown with dependencies using bulk_create_tasks or create_task, and assigns work.\n' +
70
- ' 2. **Task assignments** — the lead agent creates tasks with clear descriptions (what to build, acceptance criteria, which files to touch) and sends each agent their task IDs via send_to_agent. Use task dependencies to enforce build order (e.g. "API endpoints" blocks "Frontend integration").\n' +
71
- ' 3. **Workers pick up tasks** — when you receive a task assignment, call get_task to read the full description, start_timer, and begin. Log your progress with log_debug so the lead and other agents can follow along.\n' +
72
- ' 4. **Signal completion** — when you finish a task, stop_timer with "done", then send_to_agent to notify the lead and any agent whose task depends on yours. Include a summary of what you built and any decisions you made.\n' +
73
- ' 5. **Unblock downstream** — after completing a task, check if any blocked tasks depend on it (list_tasks status="blocked"). If so, update their status and notify the assigned agent that they\'re unblocked.',
74
- // Guiding another agent
75
- 'When guiding another agent through building something, be explicit in your task descriptions. Include:\n' +
76
- ' - **What to build** — feature name, user-facing behavior, expected output\n' +
77
- ' - **Technical approach** — which libraries/patterns to use, which files to create or modify\n' +
78
- ' - **Interfaces & contracts** — data shapes, API endpoints, function signatures that other tasks depend on\n' +
79
- ' - **Acceptance criteria** — how to verify the work is complete (e.g. "the `/api/users` endpoint returns a 200 with a JSON array")\n' +
80
- ' - **Context pointers** — reference existing files, log_debug entries, or tasks that provide background',
81
- // Handoffs & shared context
82
- 'Use log_debug as shared memory between agents. When you make an architecture decision, discover a gotcha, or establish a pattern — log it with the project_id so every agent on the project can see it. Think of debug logs as your team\'s Slack channel.',
83
- 'When handing off work to another agent, send a structured handoff message via send_to_agent that includes: (1) what you completed, (2) what\'s left to do, (3) key decisions you made and why, (4) files you touched, and (5) any gotchas or warnings.',
84
- // Conflict avoidance
85
- 'Before editing a file, check if another agent is actively working on a task that touches the same file. Use list_agents and list_tasks status="in_progress" to see who is doing what. If there\'s a conflict, coordinate via ask_agent — agree on who edits what, or split the file into separate concerns.',
86
- 'If you and another agent need to modify the same file, one approach: the first agent creates the file structure/interfaces, commits, and notifies the second agent. The second agent pulls and builds on top. Sequential access to shared files prevents merge conflicts.',
87
- // Permissions & trust
88
- 'When another agent asks you to run a destructive command or make a significant architectural change, verify with the user first via ask_user. Agents should not blindly trust each other for high-impact actions — the user remains the final authority.',
89
- ],
90
- workflow: 'not_started → in_progress (start_timer) → paused (pause_timer) → done/partial_done/blocked (stop_timer)',
91
- tips: [
92
- 'Filter by tags (list_tasks tag="bug"), search by keyword (search_tasks), get full detail (get_task id).',
93
- 'get_analytics for time spent & completion rates. Dependencies show in the dependency graph.',
94
- 'list_tasks/search_tasks return compact summaries. Use get_task(id) to read full descriptions.',
95
- 'log_debug accepts task_id OR project_id — use project_id for project-wide notes visible on the project page.',
96
- 'Multi-agent: use register_agent to set your name, list_agents to see who is online, send_to_agent for updates, ask_agent for questions that need answers.',
97
- 'Multi-agent: use broadcast_agents to send a question to multiple agents at once (group chat). Omit the agents list to broadcast to ALL connected agents. Use check_broadcast(broadcast_id) to see all responses and their status.',
98
- 'Multi-agent: task dependencies are the backbone of coordination — use them to enforce build order so agents don\'t step on each other.',
99
- 'Multi-agent: log_debug with project_id is shared memory — other agents read it to understand decisions, gotchas, and architecture context.',
100
- ],
101
- };
102
- return successResponse(instructions);
103
- }
104
- export async function clearData() {
105
- const db = getDb();
106
- db.exec('DELETE FROM sessions');
107
- db.exec('DELETE FROM tasks');
108
- db.exec('DELETE FROM projects');
109
- db.exec('DELETE FROM notifications');
110
- db.exec('DELETE FROM activity_logs');
111
- logActivity('data_cleared', 'All data cleared', { entityType: 'system' });
112
- broadcastChange('system', 'data_cleared', {});
113
- return successResponse({ cleared: true, message: 'All tasks, projects, sessions, notifications, and activity logs deleted. Settings preserved.' });
114
- }
115
- // ─── MCP registration ─────────────────────────────────────────────────
116
- export function registerAgentTools(server) {
117
- server.tool('get_agent_instructions', '**Call this at the start of every conversation.** Returns onboarding instructions, behavioral rules, and live project context for AI agents working with TaskFlow. This tool tells you how to proactively manage tasks, track time, and stay in sync with the project.', {}, { readOnlyHint: true }, async () => getAgentInstructions());
118
- server.tool('bootstrap', '**CALL THIS FIRST.** One-shot startup: returns your agent instructions, active project (auto-detected from folder name), open/blocked tasks, unread notifications, and registered agents — all in one call. Replaces the need to call get_agent_instructions + search_projects + list_tasks + list_notifications + list_agents separately.', {
119
- project_name: z.string().optional().describe('Override project name (default: auto-detected from working directory folder name)'),
120
- }, { readOnlyHint: true }, async (params) => {
121
- const db = getDb();
122
- const { listAgents } = await import('../agent-registry.js');
123
- // Get instructions
124
- const instructionsResult = await getAgentInstructions();
125
- const instructions = instructionsResult.content?.[0]?.text
126
- ? JSON.parse(instructionsResult.content[0].text)
127
- : null;
128
- // Auto-detect project from folder name or use override
129
- const { myAgentName } = await import('./agent-inbox.js');
130
- const agentEntry = myAgentName
131
- ? db.prepare('SELECT project_path FROM agent_registry WHERE name = ?').get(myAgentName)
132
- : null;
133
- const folderName = params.project_name
134
- ?? (agentEntry?.project_path ? agentEntry.project_path.split('/').pop() : null);
135
- let project = null;
136
- let tasks = [];
137
- if (folderName) {
138
- const projects = db.prepare('SELECT * FROM projects WHERE LOWER(name) LIKE ?').all(`%${folderName.toLowerCase()}%`);
139
- if (projects.length === 1) {
140
- project = projects[0];
141
- tasks = db.prepare("SELECT id, title, status, priority, tags, estimated_time FROM tasks WHERE project_id = ? AND status IN ('not_started', 'in_progress', 'paused', 'blocked') ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END").all(project.id);
142
- }
143
- else if (projects.length > 1) {
144
- project = { _ambiguous: true, matches: projects.map(p => ({ id: p.id, name: p.name })), message: 'Multiple projects match — ask the user which one to use.' };
145
- }
146
- }
147
- // Unread notifications
148
- const notifications = db.prepare("SELECT * FROM notifications WHERE read = 0 ORDER BY created_at DESC LIMIT 10").all();
149
- // Active agents
150
- const agents = listAgents('connected');
151
- // Pending inbox messages for this agent
152
- const agentName = myAgentName ?? 'unknown';
153
- const pendingMessages = db.prepare("SELECT id, sender_name, question, created_at FROM agent_messages WHERE recipient_name = ? AND status = 'pending' ORDER BY created_at ASC").all(agentName);
154
- return successResponse({
155
- instructions,
156
- project,
157
- tasks,
158
- taskCount: tasks.length,
159
- notifications,
160
- unreadCount: notifications.length,
161
- agents,
162
- pendingMessages,
163
- pendingMessageCount: pendingMessages.length,
164
- agentName,
165
- });
166
- });
167
- server.tool('clear_data', 'Delete ALL tasks, projects, sessions, notifications, and activity logs. Settings are preserved. Use with extreme caution — this is irreversible.', {}, { destructiveHint: true }, async () => clearData());
168
- }
@@ -1,21 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function getAnalytics(params: {
3
- start_date?: string;
4
- end_date?: string;
5
- }): Promise<{
6
- content: {
7
- type: "text";
8
- text: string;
9
- }[];
10
- }>;
11
- export declare function getTimeline(params: {
12
- start_date?: string;
13
- end_date?: string;
14
- group_by?: 'day' | 'week';
15
- }): Promise<{
16
- content: {
17
- type: "text";
18
- text: string;
19
- }[];
20
- }>;
21
- export declare function registerAnalyticsTools(server: McpServer): void;
@@ -1,191 +0,0 @@
1
- import { z } from 'zod';
2
- import { getDb } from '../db.js';
3
- import { successResponse, errorResponse } from '../helpers.js';
4
- // ─── exported handler functions ───────────────────────────────────────
5
- export async function getAnalytics(params) {
6
- const db = getDb();
7
- const { start_date, end_date } = params;
8
- // Build date filter for sessions
9
- const sessionConditions = [];
10
- const sessionValues = [];
11
- if (start_date) {
12
- sessionConditions.push('start >= ?');
13
- sessionValues.push(start_date);
14
- }
15
- if (end_date) {
16
- sessionConditions.push('start <= ?');
17
- sessionValues.push(end_date);
18
- }
19
- const sessionWhere = sessionConditions.length > 0
20
- ? `WHERE ${sessionConditions.join(' AND ')}`
21
- : '';
22
- // Total focused time (sum of all session durations in ms)
23
- const durationFormula = `(julianday(COALESCE(end, datetime('now'))) - julianday(start)) * 86400000`;
24
- const timeRow = db.prepare(`SELECT COALESCE(SUM(${durationFormula}), 0) AS total_focused_time FROM sessions ${sessionWhere}`).get(...sessionValues);
25
- const total_focused_time = Math.round(timeRow.total_focused_time);
26
- // Task counts by status
27
- const taskRows = db.prepare('SELECT status, COUNT(*) AS count FROM tasks GROUP BY status').all();
28
- const status_distribution = {};
29
- let tasks_completed = 0;
30
- let tasks_in_progress = 0;
31
- let total_tasks = 0;
32
- for (const row of taskRows) {
33
- status_distribution[row.status] = row.count;
34
- total_tasks += row.count;
35
- if (row.status === 'done')
36
- tasks_completed = row.count;
37
- if (row.status === 'in_progress')
38
- tasks_in_progress = row.count;
39
- }
40
- // Time per project (only tasks that belong to a project)
41
- const projectTimeRows = db.prepare(`SELECT
42
- t.project_id,
43
- p.name AS project_name,
44
- COALESCE(SUM(${durationFormula.replace(/start/g, 's.start').replace(/end/g, 's.end')}), 0) AS total_time
45
- FROM sessions s
46
- JOIN tasks t ON s.task_id = t.id
47
- JOIN projects p ON t.project_id = p.id
48
- ${sessionWhere.replace(/start/g, 's.start')}
49
- GROUP BY t.project_id, p.name`).all(...sessionValues);
50
- const time_per_project = projectTimeRows.map(row => ({
51
- project_id: row.project_id,
52
- project_name: row.project_name,
53
- total_time: Math.round(row.total_time),
54
- }));
55
- return successResponse({
56
- total_focused_time,
57
- tasks_completed,
58
- tasks_in_progress,
59
- total_tasks,
60
- status_distribution,
61
- time_per_project,
62
- });
63
- }
64
- export async function getTimeline(params) {
65
- const db = getDb();
66
- const { start_date, end_date, group_by = 'day' } = params;
67
- const periodExpr = group_by === 'week'
68
- ? `strftime('%Y-W%W', start)`
69
- : `strftime('%Y-%m-%d', start)`;
70
- const durationFormula = `(julianday(COALESCE(end, datetime('now'))) - julianday(start)) * 86400000`;
71
- const conditions = [];
72
- const values = [];
73
- if (start_date) {
74
- conditions.push('start >= ?');
75
- values.push(start_date);
76
- }
77
- if (end_date) {
78
- conditions.push('start <= ?');
79
- values.push(end_date);
80
- }
81
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
82
- const rows = db.prepare(`SELECT
83
- ${periodExpr} AS period,
84
- COALESCE(SUM(${durationFormula}), 0) AS total_time,
85
- COUNT(*) AS session_count
86
- FROM sessions
87
- ${where}
88
- GROUP BY period
89
- ORDER BY period ASC`).all(...values);
90
- const timeline = rows.map(row => ({
91
- period: row.period,
92
- total_time: Math.round(row.total_time),
93
- session_count: row.session_count,
94
- }));
95
- return successResponse(timeline);
96
- }
97
- // ─── MCP registration ─────────────────────────────────────────────────
98
- export function registerAnalyticsTools(server) {
99
- server.tool('get_analytics', 'Get a high-level analytics summary: total focused time, task completion rates, status distribution, and time per project. Useful for standup reports or understanding workload.', {
100
- start_date: z.string().optional(),
101
- end_date: z.string().optional(),
102
- }, { readOnlyHint: true }, async (params) => getAnalytics(params));
103
- server.tool('get_timeline', 'Get focused time grouped by day or week. Use for visualizing work patterns over time.', {
104
- start_date: z.string().optional(),
105
- end_date: z.string().optional(),
106
- group_by: z.enum(['day', 'week']).optional(),
107
- }, { readOnlyHint: true }, async (params) => getTimeline(params));
108
- server.tool('get_tool_stats', 'Get tool execution statistics: call count, success rate, average duration per tool. Shows which tools are used most and which are slow or failing.', {
109
- since: z.string().optional().describe('ISO date to filter from (e.g. "2026-04-01"). Defaults to all time.'),
110
- tool_name: z.string().optional().describe('Filter to a specific tool name'),
111
- }, { readOnlyHint: true }, async (params) => {
112
- const db = getDb();
113
- const conditions = [];
114
- const values = [];
115
- if (params.since) {
116
- conditions.push('created_at >= ?');
117
- values.push(params.since);
118
- }
119
- if (params.tool_name) {
120
- conditions.push('tool_name = ?');
121
- values.push(params.tool_name);
122
- }
123
- const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
124
- const stats = db.prepare(`
125
- SELECT
126
- tool_name,
127
- COUNT(*) as call_count,
128
- SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
129
- SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failure_count,
130
- ROUND(AVG(duration_ms)) as avg_duration_ms,
131
- MAX(duration_ms) as max_duration_ms,
132
- MIN(created_at) as first_call,
133
- MAX(created_at) as last_call
134
- FROM tool_executions${where}
135
- GROUP BY tool_name
136
- ORDER BY call_count DESC
137
- `).all(...values);
138
- const total = db.prepare(`SELECT COUNT(*) as count FROM tool_executions${where}`).get(...values);
139
- return successResponse({ total_executions: total.count, tools: stats });
140
- });
141
- server.tool('get_task_cost', 'Get per-task cost metrics: tool calls made during active timer sessions, total execution time, and tool breakdown. Useful for understanding which tasks consume the most resources.', {
142
- task_id: z.number().optional().describe('Get cost for a specific task. Omit for all tasks.'),
143
- project_id: z.number().optional().describe('Get cost for all tasks in a project'),
144
- }, { readOnlyHint: true }, async (params) => {
145
- const db = getDb();
146
- if (params.task_id) {
147
- // Cost for a single task — use subqueries to avoid cross-join inflation
148
- const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(params.task_id);
149
- if (!task)
150
- return errorResponse(`Task ${params.task_id} not found`, 'NOT_FOUND');
151
- const sessionTime = db.prepare('SELECT COALESCE(SUM(CASE WHEN end IS NOT NULL THEN (julianday(end) - julianday(start)) * 86400000 ELSE 0 END), 0) as total FROM sessions WHERE task_id = ?').get(params.task_id);
152
- // Tool calls that occurred during any session for this task
153
- const toolCalls = db.prepare(`
154
- SELECT COUNT(*) as total, COALESCE(SUM(te.duration_ms), 0) as duration_ms,
155
- SUM(CASE WHEN te.success = 0 THEN 1 ELSE 0 END) as failed
156
- FROM tool_executions te
157
- WHERE EXISTS (SELECT 1 FROM sessions s WHERE s.task_id = ? AND te.created_at >= s.start AND (s.end IS NULL OR te.created_at <= s.end))
158
- `).get(params.task_id);
159
- // Tool breakdown for this task
160
- const tools = db.prepare(`
161
- SELECT te.tool_name, COUNT(*) as call_count, ROUND(AVG(te.duration_ms)) as avg_ms
162
- FROM tool_executions te
163
- WHERE EXISTS (SELECT 1 FROM sessions s WHERE s.task_id = ? AND te.created_at >= s.start AND (s.end IS NULL OR te.created_at <= s.end))
164
- GROUP BY te.tool_name
165
- ORDER BY call_count DESC
166
- `).all(params.task_id);
167
- return successResponse({
168
- task: { task_id: task.id, title: task.title, status: task.status, tool_calls: toolCalls.total, total_tool_duration_ms: toolCalls.duration_ms, failed_calls: toolCalls.failed, total_session_time_ms: sessionTime.total },
169
- tool_breakdown: tools,
170
- });
171
- }
172
- // All tasks (optionally filtered by project) — use subqueries to avoid cross-join
173
- const projectFilter = params.project_id ? 'WHERE t.project_id = ?' : '';
174
- const filterValues = params.project_id ? [params.project_id] : [];
175
- const tasks = db.prepare(`
176
- SELECT
177
- t.id as task_id,
178
- t.title,
179
- t.status,
180
- (SELECT COUNT(*) FROM tool_executions te WHERE EXISTS (SELECT 1 FROM sessions s WHERE s.task_id = t.id AND te.created_at >= s.start AND (s.end IS NULL OR te.created_at <= s.end))) as tool_calls,
181
- (SELECT COALESCE(SUM(te.duration_ms), 0) FROM tool_executions te WHERE EXISTS (SELECT 1 FROM sessions s WHERE s.task_id = t.id AND te.created_at >= s.start AND (s.end IS NULL OR te.created_at <= s.end))) as total_tool_duration_ms,
182
- (SELECT COALESCE(SUM(CASE WHEN end IS NOT NULL THEN (julianday(end) - julianday(start)) * 86400000 ELSE 0 END), 0) FROM sessions WHERE task_id = t.id) as total_session_time_ms
183
- FROM tasks t
184
- ${projectFilter}
185
- HAVING tool_calls > 0
186
- ORDER BY tool_calls DESC
187
- LIMIT 30
188
- `).all(...filterValues);
189
- return successResponse({ tasks });
190
- });
191
- }
@@ -1,27 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function createCheckpoint(params: {
3
- task_id: number;
4
- }): Promise<{
5
- content: {
6
- type: "text";
7
- text: string;
8
- }[];
9
- }>;
10
- export declare function getCheckpoint(params: {
11
- task_id: number;
12
- latest?: boolean;
13
- }): Promise<{
14
- content: {
15
- type: "text";
16
- text: string;
17
- }[];
18
- }>;
19
- export declare function listCheckpoints(params: {
20
- task_id: number;
21
- }): Promise<{
22
- content: {
23
- type: "text";
24
- text: string;
25
- }[];
26
- }>;
27
- export declare function registerCheckpointTools(server: McpServer): void;
@@ -1,105 +0,0 @@
1
- import { z } from 'zod';
2
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
3
- import { resolve } from 'path';
4
- import { homedir } from 'os';
5
- import { getDb } from '../db.js';
6
- import { successResponse, errorResponse } from '../helpers.js';
7
- const CHECKPOINTS_DIR = resolve(homedir(), '.taskflow/checkpoints');
8
- const MAX_CHECKPOINTS_PER_TASK = 10;
9
- function getTaskDir(taskId) {
10
- const dir = resolve(CHECKPOINTS_DIR, String(taskId));
11
- mkdirSync(dir, { recursive: true });
12
- return dir;
13
- }
14
- export async function createCheckpoint(params) {
15
- const db = getDb();
16
- const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.task_id);
17
- if (!task)
18
- return errorResponse(`Task ${params.task_id} not found`, 'NOT_FOUND');
19
- // Gather task state
20
- const deps = db.prepare('SELECT dependency_id FROM task_dependencies WHERE task_id = ?').all(params.task_id);
21
- const recentActivity = db.prepare('SELECT * FROM activity_logs WHERE entity_id = ? AND entity_type = ? ORDER BY created_at DESC LIMIT 20').all(params.task_id, 'task');
22
- const activeSession = db.prepare('SELECT * FROM sessions WHERE task_id = ? AND end IS NULL').get(params.task_id);
23
- const totalTime = db.prepare('SELECT SUM(CASE WHEN end IS NOT NULL THEN (julianday(end) - julianday(start)) * 86400000 ELSE 0 END) as total FROM sessions WHERE task_id = ?').get(params.task_id);
24
- // Tool stats during this task's sessions
25
- const toolStats = db.prepare(`
26
- SELECT te.tool_name, COUNT(*) as calls, ROUND(AVG(te.duration_ms)) as avg_ms
27
- FROM tool_executions te
28
- JOIN sessions s ON s.task_id = ? AND te.created_at >= s.start AND (s.end IS NULL OR te.created_at <= s.end)
29
- GROUP BY te.tool_name ORDER BY calls DESC LIMIT 10
30
- `).all(params.task_id);
31
- const checkpoint = {
32
- task_id: params.task_id,
33
- timestamp: new Date().toISOString(),
34
- task,
35
- dependencies: deps.map(d => d.dependency_id),
36
- recent_activity: recentActivity,
37
- active_session: activeSession,
38
- total_time_ms: totalTime.total ?? 0,
39
- tool_stats: toolStats,
40
- };
41
- // Write checkpoint file
42
- const taskDir = getTaskDir(params.task_id);
43
- const filename = `${checkpoint.timestamp.replace(/[:.]/g, '-')}.json`;
44
- const filepath = resolve(taskDir, filename);
45
- writeFileSync(filepath, JSON.stringify(checkpoint, null, 2), 'utf-8');
46
- // Prune old checkpoints
47
- const files = readdirSync(taskDir).filter(f => f.endsWith('.json')).sort();
48
- while (files.length > MAX_CHECKPOINTS_PER_TASK) {
49
- const oldest = files.shift();
50
- try {
51
- unlinkSync(resolve(taskDir, oldest));
52
- }
53
- catch { /* ignore */ }
54
- }
55
- return successResponse({
56
- task_id: params.task_id,
57
- checkpoint: filename,
58
- path: filepath,
59
- total_checkpoints: Math.min(files.length, MAX_CHECKPOINTS_PER_TASK),
60
- });
61
- }
62
- export async function getCheckpoint(params) {
63
- const taskDir = resolve(CHECKPOINTS_DIR, String(params.task_id));
64
- let files;
65
- try {
66
- files = readdirSync(taskDir).filter(f => f.endsWith('.json')).sort();
67
- }
68
- catch {
69
- return errorResponse(`No checkpoints found for task ${params.task_id}`, 'NOT_FOUND');
70
- }
71
- if (files.length === 0) {
72
- return errorResponse(`No checkpoints found for task ${params.task_id}`, 'NOT_FOUND');
73
- }
74
- // Return latest by default
75
- const file = files[files.length - 1];
76
- const data = JSON.parse(readFileSync(resolve(taskDir, file), 'utf-8'));
77
- return successResponse({
78
- checkpoint: file,
79
- data,
80
- available_checkpoints: files.length,
81
- });
82
- }
83
- export async function listCheckpoints(params) {
84
- const taskDir = resolve(CHECKPOINTS_DIR, String(params.task_id));
85
- let files;
86
- try {
87
- files = readdirSync(taskDir).filter(f => f.endsWith('.json')).sort();
88
- }
89
- catch {
90
- return successResponse({ task_id: params.task_id, checkpoints: [] });
91
- }
92
- return successResponse({
93
- task_id: params.task_id,
94
- checkpoints: files.map(f => ({
95
- filename: f,
96
- timestamp: f.replace('.json', '').replace(/-/g, (m, offset) => offset <= 9 ? '-' : offset <= 15 ? ':' : '.'),
97
- })),
98
- });
99
- }
100
- // ─── MCP registration ─────────────────────────────────────────────────
101
- export function registerCheckpointTools(server) {
102
- server.tool('create_checkpoint', 'Create a checkpoint snapshot of a task\'s current state. Captures: task data, dependencies, recent activity, active timer, tool stats. Useful before context switches or at key milestones.', { task_id: z.number().describe('The task ID to checkpoint') }, { readOnlyHint: false }, async (params) => createCheckpoint(params));
103
- server.tool('get_checkpoint', 'Get the latest checkpoint for a task. Returns the full task snapshot — useful for resuming work after an interruption.', { task_id: z.number().describe('The task ID to get checkpoint for') }, { readOnlyHint: true }, async (params) => getCheckpoint(params));
104
- server.tool('list_checkpoints', 'List all available checkpoints for a task.', { task_id: z.number().describe('The task ID to list checkpoints for') }, { readOnlyHint: true }, async (params) => listCheckpoints(params));
105
- }
@@ -1,31 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function listNotifications(params: {
3
- limit?: number;
4
- unread_only?: boolean;
5
- }): Promise<{
6
- content: {
7
- type: "text";
8
- text: string;
9
- }[];
10
- }>;
11
- export declare function markNotificationRead(params: {
12
- id: number;
13
- }): Promise<{
14
- content: {
15
- type: "text";
16
- text: string;
17
- }[];
18
- }>;
19
- export declare function markAllNotificationsRead(): Promise<{
20
- content: {
21
- type: "text";
22
- text: string;
23
- }[];
24
- }>;
25
- export declare function clearNotifications(): Promise<{
26
- content: {
27
- type: "text";
28
- text: string;
29
- }[];
30
- }>;
31
- export declare function registerNotificationTools(server: McpServer): void;
@@ -1,59 +0,0 @@
1
- import { z } from 'zod';
2
- import { getDb } from '../db.js';
3
- import { errorResponse, successResponse, broadcastChange } from '../helpers.js';
4
- // ─── exported handler functions ───────────────────────────────────────
5
- export async function listNotifications(params) {
6
- const db = getDb();
7
- const limit = params.limit ?? 50;
8
- let sql = 'SELECT * FROM notifications';
9
- const values = [];
10
- if (params.unread_only) {
11
- sql += ' WHERE read = 0';
12
- }
13
- sql += ' ORDER BY created_at DESC LIMIT ?';
14
- values.push(limit);
15
- const rows = db.prepare(sql).all(...values);
16
- return successResponse(rows);
17
- }
18
- export async function markNotificationRead(params) {
19
- const db = getDb();
20
- const result = db
21
- .prepare('UPDATE notifications SET read = 1 WHERE id = ?')
22
- .run(params.id);
23
- if (result.changes === 0) {
24
- return errorResponse('Notification not found', 'NOT_FOUND');
25
- }
26
- const row = db
27
- .prepare('SELECT * FROM notifications WHERE id = ?')
28
- .get(params.id);
29
- const notification = row;
30
- broadcastChange('notification', 'notification_updated', notification);
31
- return successResponse(notification);
32
- }
33
- export async function markAllNotificationsRead() {
34
- const db = getDb();
35
- const result = db
36
- .prepare('UPDATE notifications SET read = 1 WHERE read = 0')
37
- .run();
38
- broadcastChange('notification', 'notifications_all_read', {});
39
- return successResponse({ updated: result.changes });
40
- }
41
- export async function clearNotifications() {
42
- const db = getDb();
43
- const countRow = db
44
- .prepare('SELECT COUNT(*) AS count FROM notifications')
45
- .get();
46
- db.prepare('DELETE FROM notifications').run();
47
- broadcastChange('notification', 'notifications_cleared', {});
48
- return successResponse({ deleted: countRow.count, message: 'Notifications cleared' });
49
- }
50
- // ─── MCP registration ─────────────────────────────────────────────────
51
- export function registerNotificationTools(server) {
52
- server.tool('list_notifications', 'List notifications. Check with unread_only=true at conversation start to surface important updates for the user.', {
53
- limit: z.number().optional(),
54
- unread_only: z.boolean().optional(),
55
- }, { readOnlyHint: true }, async (params) => listNotifications(params));
56
- server.tool('mark_notification_read', 'Mark a notification as read after surfacing it to the user.', { id: z.number() }, { readOnlyHint: false }, async (params) => markNotificationRead(params));
57
- server.tool('mark_all_notifications_read', 'Mark all unread notifications as read. Call after the user has been briefed on pending notifications.', {}, { readOnlyHint: false }, async () => markAllNotificationsRead());
58
- server.tool('clear_notifications', 'Delete all notifications. Use with caution — this is irreversible.', {}, { destructiveHint: true }, async () => clearNotifications());
59
- }