@dalmasonto/taskflow-mcp 1.0.35 → 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 (101) 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 +121 -218
  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 -28
  65. package/dist/agent-registry.js +0 -158
  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/retry.d.ts +0 -34
  73. package/dist/retry.js +0 -94
  74. package/dist/sse.d.ts +0 -10
  75. package/dist/sse.js +0 -824
  76. package/dist/tmux-bridge.d.ts +0 -13
  77. package/dist/tmux-bridge.js +0 -217
  78. package/dist/tools/activity.d.ts +0 -39
  79. package/dist/tools/activity.js +0 -152
  80. package/dist/tools/agent-inbox.d.ts +0 -12
  81. package/dist/tools/agent-inbox.js +0 -272
  82. package/dist/tools/agent.d.ts +0 -14
  83. package/dist/tools/agent.js +0 -168
  84. package/dist/tools/analytics.d.ts +0 -21
  85. package/dist/tools/analytics.js +0 -191
  86. package/dist/tools/checkpoint.d.ts +0 -27
  87. package/dist/tools/checkpoint.js +0 -105
  88. package/dist/tools/notifications.d.ts +0 -31
  89. package/dist/tools/notifications.js +0 -59
  90. package/dist/tools/projects.d.ts +0 -55
  91. package/dist/tools/projects.js +0 -112
  92. package/dist/tools/settings.d.ts +0 -19
  93. package/dist/tools/settings.js +0 -73
  94. package/dist/tools/tasks.d.ts +0 -105
  95. package/dist/tools/tasks.js +0 -403
  96. package/dist/tools/terminal.d.ts +0 -4
  97. package/dist/tools/terminal.js +0 -98
  98. package/dist/tools/timer.d.ts +0 -37
  99. package/dist/tools/timer.js +0 -154
  100. package/dist/types.d.ts +0 -83
  101. package/dist/types.js +0 -30
@@ -1,73 +0,0 @@
1
- import { z } from 'zod';
2
- import { getDb } from '../db.js';
3
- import { logActivity, successResponse, broadcastChange } from '../helpers.js';
4
- import { getConfig, isServerConfigKey, writeConfigKey } from '../config.js';
5
- // ─── defaults ─────────────────────────────────────────────────────────
6
- const DEFAULT_SETTINGS = {
7
- timerBarDisplayMode: 'carousel',
8
- notificationInterval: 30,
9
- browserNotificationsEnabled: true,
10
- statusColors: {
11
- not_started: '#484847',
12
- in_progress: '#00fbfb',
13
- paused: '#de8eff',
14
- blocked: '#ff6e84',
15
- partial_done: '#ffeb3b',
16
- done: '#69fd5d',
17
- },
18
- glowIntensity: 50,
19
- backdropBlur: 8,
20
- shadowSpread: 4,
21
- operatorName: 'operator',
22
- systemName: 'SYSTEM',
23
- terminalHistory: [],
24
- };
25
- // ─── exported handler functions ───────────────────────────────────────
26
- export async function getSetting(params) {
27
- // Server-level settings come from the config file
28
- if (isServerConfigKey(params.key)) {
29
- const config = getConfig();
30
- const value = config[params.key] ?? null;
31
- return successResponse({ key: params.key, value, source: 'config_file' });
32
- }
33
- // UI settings come from SQLite
34
- const db = getDb();
35
- const row = db
36
- .prepare('SELECT value FROM settings WHERE key = ?')
37
- .get(params.key);
38
- if (row) {
39
- return successResponse({ key: params.key, value: JSON.parse(row.value) });
40
- }
41
- const defaultValue = Object.prototype.hasOwnProperty.call(DEFAULT_SETTINGS, params.key)
42
- ? DEFAULT_SETTINGS[params.key]
43
- : null;
44
- return successResponse({ key: params.key, value: defaultValue });
45
- }
46
- export async function updateSetting(params) {
47
- // Server-level settings write back to ~/.taskflow_config.json
48
- if (isServerConfigKey(params.key)) {
49
- writeConfigKey(params.key, params.value);
50
- logActivity('settings_saved', params.key, { detail: `Server setting "${params.key}" updated in config file` });
51
- broadcastChange('setting', 'settings_saved', { key: params.key, value: params.value });
52
- return successResponse({
53
- key: params.key,
54
- value: params.value,
55
- source: 'config_file',
56
- note: 'Server setting saved to ~/.taskflow_config.json. Restart the MCP server for the change to take full effect.',
57
- });
58
- }
59
- // UI settings go to SQLite
60
- const db = getDb();
61
- db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(params.key, JSON.stringify(params.value));
62
- logActivity('settings_saved', params.key, { detail: `Setting "${params.key}" updated` });
63
- broadcastChange('setting', 'settings_saved', { key: params.key, value: params.value });
64
- return successResponse({ key: params.key, value: params.value });
65
- }
66
- // ─── MCP registration ─────────────────────────────────────────────────
67
- export function registerSettingsTools(server) {
68
- server.tool('get_setting', 'Get a setting value by key. Server settings (port, host, databasePath, logLevel, agentLivenessInterval, maxPortAttempts) come from ~/.taskflow_config.json. UI settings (timerBarDisplayMode, notificationInterval, statusColors, operatorName, systemName, etc.) come from SQLite.', { key: z.string() }, { readOnlyHint: true }, async (params) => getSetting(params));
69
- server.tool('update_setting', 'Update a setting value. Server settings (port, host, databasePath, logLevel, agentLivenessInterval, maxPortAttempts) are written to ~/.taskflow_config.json and require a restart. UI settings are saved to the local SQLite database.', {
70
- key: z.string(),
71
- value: z.unknown(),
72
- }, { readOnlyHint: false }, async (params) => updateSetting(params));
73
- }
@@ -1,105 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function createTask(params: {
3
- title: string;
4
- description?: string;
5
- status?: string;
6
- priority?: string;
7
- project_id?: number;
8
- dependencies?: number[];
9
- tags?: string[];
10
- links?: Array<{
11
- label: string;
12
- url: string;
13
- }>;
14
- due_date?: string;
15
- estimated_time?: number;
16
- }): Promise<{
17
- content: {
18
- type: "text";
19
- text: string;
20
- }[];
21
- }>;
22
- export declare function listTasks(params: {
23
- status?: string;
24
- project_id?: number;
25
- priority?: string;
26
- tag?: string;
27
- }): Promise<{
28
- content: {
29
- type: "text";
30
- text: string;
31
- }[];
32
- }>;
33
- export declare function getTask(params: {
34
- id: number;
35
- }): Promise<{
36
- content: {
37
- type: "text";
38
- text: string;
39
- }[];
40
- }>;
41
- export declare function updateTask(params: {
42
- id: number;
43
- title?: string;
44
- description?: string;
45
- status?: string;
46
- priority?: string;
47
- project_id?: number;
48
- dependencies?: number[];
49
- tags?: string[];
50
- links?: Array<{
51
- label: string;
52
- url: string;
53
- }>;
54
- due_date?: string;
55
- estimated_time?: number;
56
- allowed_tools?: string[];
57
- denied_tools?: string[];
58
- }): Promise<{
59
- content: {
60
- type: "text";
61
- text: string;
62
- }[];
63
- }>;
64
- export declare function updateTaskStatus(params: {
65
- id: number;
66
- status: string;
67
- }): Promise<{
68
- content: {
69
- type: "text";
70
- text: string;
71
- }[];
72
- }>;
73
- export declare function deleteTask(params: {
74
- id: number;
75
- }): Promise<{
76
- content: {
77
- type: "text";
78
- text: string;
79
- }[];
80
- }>;
81
- export declare function bulkCreateTasks(params: {
82
- tasks: Array<{
83
- title: string;
84
- description?: string;
85
- priority?: string;
86
- project_id?: number;
87
- status?: string;
88
- dependencies?: number[];
89
- tags?: string[];
90
- }>;
91
- }): Promise<{
92
- content: {
93
- type: "text";
94
- text: string;
95
- }[];
96
- }>;
97
- export declare function searchTasks(params: {
98
- query: string;
99
- }): Promise<{
100
- content: {
101
- type: "text";
102
- text: string;
103
- }[];
104
- }>;
105
- export declare function registerTaskTools(server: McpServer): void;
@@ -1,403 +0,0 @@
1
- import { z } from 'zod';
2
- import { getDb } from '../db.js';
3
- import { logActivity, errorResponse, successResponse, now, broadcastChange } from '../helpers.js';
4
- import { TaskStatus, TaskPriority, LinkSchema, VALID_TRANSITIONS } from '../types.js';
5
- function parseTask(row) {
6
- return {
7
- ...row,
8
- dependencies: JSON.parse(row.dependencies),
9
- links: JSON.parse(row.links),
10
- tags: JSON.parse(row.tags),
11
- };
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
- }
35
- function detectCycle(taskId, proposedDeps) {
36
- const db = getDb();
37
- const allTasks = db.prepare('SELECT id, dependencies FROM tasks').all();
38
- // Build adjacency list: task -> its dependencies (task depends on dep means edge task->dep)
39
- // A cycle means: taskId depends on X which depends on Y ... which depends on taskId
40
- // We need to check: from any of proposedDeps, can we reach taskId by following dependencies?
41
- // Actually, the direction matters. If task A depends on B, it means B must be done before A.
42
- // A cycle: A depends on B, B depends on A.
43
- // Adjacency: A -> [B], B -> [A]. Starting from A's deps [B], follow B's deps [A], found A = cycle.
44
- const adj = new Map();
45
- for (const t of allTasks) {
46
- const deps = JSON.parse(t.dependencies);
47
- if (t.id === taskId) {
48
- // Use proposed deps instead of current
49
- adj.set(t.id, proposedDeps);
50
- }
51
- else {
52
- adj.set(t.id, deps);
53
- }
54
- }
55
- // If task doesn't exist yet (create), add it
56
- if (!adj.has(taskId)) {
57
- adj.set(taskId, proposedDeps);
58
- }
59
- // DFS from taskId following dependency edges to see if we return to taskId
60
- const visited = new Set();
61
- const inStack = new Set();
62
- function dfs(node) {
63
- if (inStack.has(node))
64
- return true; // back-edge = cycle
65
- if (visited.has(node))
66
- return false;
67
- visited.add(node);
68
- inStack.add(node);
69
- const deps = adj.get(node) || [];
70
- for (const dep of deps) {
71
- if (dfs(dep))
72
- return true;
73
- }
74
- inStack.delete(node);
75
- return false;
76
- }
77
- return dfs(taskId);
78
- }
79
- // ─── exported handler functions ───────────────────────────────────────
80
- export async function createTask(params) {
81
- const db = getDb();
82
- const { title, description, status = 'not_started', priority = 'medium', project_id, dependencies = [], tags = [], links = [], due_date, estimated_time, } = params;
83
- // Validate project_id
84
- if (project_id != null) {
85
- const proj = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id);
86
- if (!proj)
87
- return errorResponse('Project not found', 'NOT_FOUND');
88
- }
89
- // Validate dependencies exist
90
- if (dependencies.length > 0) {
91
- const placeholders = dependencies.map(() => '?').join(',');
92
- const existing = db.prepare(`SELECT id FROM tasks WHERE id IN (${placeholders})`).all(...dependencies);
93
- if (existing.length !== dependencies.length) {
94
- return errorResponse('One or more dependency task IDs do not exist', 'VALIDATION_ERROR');
95
- }
96
- }
97
- // Check for cycles — we need a temporary ID; use max(id)+1 as placeholder
98
- if (dependencies.length > 0) {
99
- const maxRow = db.prepare('SELECT COALESCE(MAX(id), 0) + 1 AS next_id FROM tasks').get();
100
- if (detectCycle(maxRow.next_id, dependencies)) {
101
- return errorResponse('Adding these dependencies would create a cycle', 'CYCLE_DETECTED');
102
- }
103
- }
104
- const ts = now();
105
- const result = db.prepare(`INSERT INTO tasks (title, description, status, priority, project_id, dependencies, links, tags, due_date, estimated_time, created_at, updated_at)
106
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(title, description ?? null, status, priority, project_id ?? null, JSON.stringify(dependencies), JSON.stringify(links), JSON.stringify(tags), due_date ?? null, estimated_time ?? null, ts, ts);
107
- const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid);
108
- logActivity('task_created', title, { entityType: 'task', entityId: task.id });
109
- const createdTask = parseTask(task);
110
- broadcastChange('task', 'task_created', createdTask);
111
- return successResponse(createdTask);
112
- }
113
- export async function listTasks(params) {
114
- const db = getDb();
115
- const conditions = [];
116
- const values = [];
117
- if (params.status) {
118
- conditions.push('status = ?');
119
- values.push(params.status);
120
- }
121
- if (params.project_id != null) {
122
- conditions.push('project_id = ?');
123
- values.push(params.project_id);
124
- }
125
- if (params.priority) {
126
- conditions.push('priority = ?');
127
- values.push(params.priority);
128
- }
129
- let sql = 'SELECT * FROM tasks';
130
- if (conditions.length > 0) {
131
- sql += ' WHERE ' + conditions.join(' AND ');
132
- }
133
- let rows = db.prepare(sql).all(...values);
134
- // Filter by tag in-memory (JSON column)
135
- if (params.tag) {
136
- rows = rows.filter(r => {
137
- const tags = JSON.parse(r.tags);
138
- return tags.includes(params.tag);
139
- });
140
- }
141
- return successResponse(rows.map(parseTaskCompact));
142
- }
143
- export async function getTask(params) {
144
- const db = getDb();
145
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.id);
146
- if (!row)
147
- return errorResponse('Task not found', 'NOT_FOUND');
148
- const timeRow = db.prepare(`SELECT
149
- COUNT(*) AS session_count,
150
- COALESCE(SUM((julianday(COALESCE(end, datetime('now'))) - julianday(start)) * 86400000), 0) AS total_time
151
- FROM sessions WHERE task_id = ?`).get(params.id);
152
- return successResponse({
153
- ...parseTask(row),
154
- total_time: Math.round(timeRow.total_time),
155
- session_count: timeRow.session_count,
156
- });
157
- }
158
- export async function updateTask(params) {
159
- const db = getDb();
160
- const old = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.id);
161
- if (!old)
162
- return errorResponse('Task not found', 'NOT_FOUND');
163
- const oldParsed = parseTask(old);
164
- // Validate dependencies
165
- const newDeps = params.dependencies ?? oldParsed.dependencies;
166
- if (params.dependencies && params.dependencies.length > 0) {
167
- const placeholders = params.dependencies.map(() => '?').join(',');
168
- const existing = db.prepare(`SELECT id FROM tasks WHERE id IN (${placeholders})`).all(...params.dependencies);
169
- if (existing.length !== params.dependencies.length) {
170
- return errorResponse('One or more dependency task IDs do not exist', 'VALIDATION_ERROR');
171
- }
172
- // Cycle detection
173
- if (detectCycle(params.id, params.dependencies)) {
174
- return errorResponse('Adding these dependencies would create a cycle', 'CYCLE_DETECTED');
175
- }
176
- }
177
- // Validate project_id if changing
178
- if (params.project_id != null) {
179
- const proj = db.prepare('SELECT id FROM projects WHERE id = ?').get(params.project_id);
180
- if (!proj)
181
- return errorResponse('Project not found', 'NOT_FOUND');
182
- }
183
- const ts = now();
184
- const updates = {};
185
- if (params.title !== undefined)
186
- updates.title = params.title;
187
- if (params.description !== undefined)
188
- updates.description = params.description;
189
- if (params.status !== undefined)
190
- updates.status = params.status;
191
- if (params.priority !== undefined)
192
- updates.priority = params.priority;
193
- if (params.project_id !== undefined)
194
- updates.project_id = params.project_id;
195
- if (params.dependencies !== undefined)
196
- updates.dependencies = JSON.stringify(params.dependencies);
197
- if (params.tags !== undefined)
198
- updates.tags = JSON.stringify(params.tags);
199
- if (params.links !== undefined)
200
- updates.links = JSON.stringify(params.links);
201
- if (params.due_date !== undefined)
202
- updates.due_date = params.due_date;
203
- if (params.estimated_time !== undefined)
204
- updates.estimated_time = params.estimated_time;
205
- if (params.allowed_tools !== undefined)
206
- updates.allowed_tools = JSON.stringify(params.allowed_tools);
207
- if (params.denied_tools !== undefined)
208
- updates.denied_tools = JSON.stringify(params.denied_tools);
209
- if (Object.keys(updates).length === 0) {
210
- return successResponse(oldParsed);
211
- }
212
- updates.updated_at = ts;
213
- const setClauses = Object.keys(updates).map(k => `${k} = ?`).join(', ');
214
- const vals = Object.values(updates);
215
- db.prepare(`UPDATE tasks SET ${setClauses} WHERE id = ?`).run(...vals, params.id);
216
- // Log granular changes for deps, tags, links
217
- if (params.dependencies !== undefined) {
218
- const added = params.dependencies.filter(d => !oldParsed.dependencies.includes(d));
219
- const removed = oldParsed.dependencies.filter(d => !params.dependencies.includes(d));
220
- for (const d of added) {
221
- logActivity('dependency_added', oldParsed.title, { detail: `Dependency ${d} added`, entityType: 'task', entityId: params.id });
222
- }
223
- for (const d of removed) {
224
- logActivity('dependency_removed', oldParsed.title, { detail: `Dependency ${d} removed`, entityType: 'task', entityId: params.id });
225
- }
226
- }
227
- if (params.tags !== undefined) {
228
- const added = params.tags.filter(t => !oldParsed.tags.includes(t));
229
- const removed = oldParsed.tags.filter(t => !params.tags.includes(t));
230
- for (const t of added) {
231
- logActivity('tag_added', oldParsed.title, { detail: `Tag "${t}" added`, entityType: 'task', entityId: params.id });
232
- }
233
- for (const t of removed) {
234
- logActivity('tag_removed', oldParsed.title, { detail: `Tag "${t}" removed`, entityType: 'task', entityId: params.id });
235
- }
236
- }
237
- if (params.links !== undefined) {
238
- const oldUrls = new Set(oldParsed.links.map(l => l.url));
239
- const newUrls = new Set(params.links.map(l => l.url));
240
- const added = params.links.filter(l => !oldUrls.has(l.url));
241
- const removed = oldParsed.links.filter(l => !newUrls.has(l.url));
242
- for (const l of added) {
243
- logActivity('link_added', oldParsed.title, { detail: `Link "${l.label}" added`, entityType: 'task', entityId: params.id });
244
- }
245
- for (const l of removed) {
246
- logActivity('task_unlinked', oldParsed.title, { detail: `Link "${l.label}" removed`, entityType: 'task', entityId: params.id });
247
- }
248
- }
249
- const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.id);
250
- const updatedTask = parseTask(updated);
251
- broadcastChange('task', 'task_updated', updatedTask);
252
- return successResponse(updatedTask);
253
- }
254
- export async function updateTaskStatus(params) {
255
- const db = getDb();
256
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.id);
257
- if (!row)
258
- return errorResponse('Task not found', 'NOT_FOUND');
259
- const currentStatus = row.status;
260
- const allowed = VALID_TRANSITIONS[currentStatus];
261
- if (!allowed || !allowed.includes(params.status)) {
262
- return errorResponse(`Cannot transition from "${currentStatus}" to "${params.status}"`, 'INVALID_TRANSITION');
263
- }
264
- const ts = now();
265
- // Auto-start a timer session when entering in_progress
266
- if (params.status === 'in_progress') {
267
- const existingOpen = db.prepare('SELECT id FROM sessions WHERE task_id = ? AND end IS NULL').get(params.id);
268
- if (!existingOpen) {
269
- const result = db.prepare('INSERT INTO sessions (task_id, start, end) VALUES (?, ?, ?)').run(params.id, ts, null);
270
- const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(result.lastInsertRowid);
271
- logActivity('timer_started', row.title, { entityType: 'task', entityId: params.id });
272
- broadcastChange('timer', 'timer_started', { task_id: params.id, session, task_status: 'in_progress' });
273
- }
274
- }
275
- // Auto-close any active timer session when leaving an active state
276
- const terminalStatuses = ['done', 'partial_done', 'blocked', 'paused'];
277
- if (terminalStatuses.includes(params.status)) {
278
- const openSession = db.prepare('SELECT * FROM sessions WHERE task_id = ? AND end IS NULL').get(params.id);
279
- if (openSession) {
280
- db.prepare('UPDATE sessions SET end = ? WHERE id = ?').run(ts, openSession.id);
281
- const duration = new Date(ts).getTime() - new Date(openSession.start).getTime();
282
- logActivity('timer_stopped', row.title, {
283
- detail: `Auto-stopped: duration ${duration}ms, status → ${params.status}`,
284
- entityType: 'task',
285
- entityId: params.id,
286
- });
287
- broadcastChange('timer', 'timer_stopped', { task_id: params.id, session: { ...openSession, end: ts, duration }, task_status: params.status });
288
- }
289
- }
290
- db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?').run(params.status, ts, params.id);
291
- // Log appropriate action
292
- let statusAction;
293
- if (params.status === 'done') {
294
- logActivity('task_completed', row.title, { entityType: 'task', entityId: params.id });
295
- statusAction = 'task_completed';
296
- }
297
- else if (params.status === 'partial_done') {
298
- logActivity('task_partial_done', row.title, { entityType: 'task', entityId: params.id });
299
- statusAction = 'task_partial_done';
300
- }
301
- else {
302
- logActivity('task_status_changed', row.title, {
303
- detail: `${currentStatus} -> ${params.status}`,
304
- entityType: 'task',
305
- entityId: params.id,
306
- });
307
- statusAction = 'task_status_changed';
308
- }
309
- const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.id);
310
- broadcastChange('task', statusAction, parseTask(updated));
311
- return successResponse(parseTaskCompact(updated));
312
- }
313
- export async function deleteTask(params) {
314
- const db = getDb();
315
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(params.id);
316
- if (!row)
317
- return errorResponse('Task not found', 'NOT_FOUND');
318
- db.prepare('DELETE FROM tasks WHERE id = ?').run(params.id);
319
- logActivity('task_deleted', row.title, { entityType: 'task', entityId: params.id });
320
- broadcastChange('task', 'task_deleted', { id: params.id });
321
- return successResponse({ deleted: true, id: params.id });
322
- }
323
- export async function bulkCreateTasks(params) {
324
- const db = getDb();
325
- const ts = now();
326
- const created = [];
327
- const insertStmt = db.prepare(`INSERT INTO tasks (title, description, status, priority, project_id, dependencies, links, tags, due_date, estimated_time, created_at, updated_at)
328
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
329
- const transaction = db.transaction(() => {
330
- for (const t of params.tasks) {
331
- const result = insertStmt.run(t.title, t.description ?? null, t.status ?? 'not_started', t.priority ?? 'medium', t.project_id ?? null, JSON.stringify(t.dependencies ?? []), '[]', JSON.stringify(t.tags ?? []), null, null, ts, ts);
332
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid);
333
- created.push(parseTask(row));
334
- }
335
- });
336
- transaction();
337
- logActivity('tasks_bulk_created', `${created.length} tasks created`, {
338
- detail: created.map(t => t.title).join(', '),
339
- });
340
- const createdTasks = created;
341
- broadcastChange('task', 'tasks_bulk_created', { tasks: createdTasks });
342
- return successResponse(createdTasks);
343
- }
344
- export async function searchTasks(params) {
345
- const db = getDb();
346
- const like = `%${params.query}%`;
347
- const rows = db.prepare(`SELECT * FROM tasks WHERE title LIKE ? COLLATE NOCASE OR description LIKE ? COLLATE NOCASE`).all(like, like);
348
- return successResponse(rows.map(parseTaskCompact));
349
- }
350
- // ─── MCP registration ─────────────────────────────────────────────────
351
- export function registerTaskTools(server) {
352
- server.tool('create_task', 'Create a new task. Use this when starting new work to keep TaskFlow in sync. Supports dependencies, tags, links, and time estimates.', {
353
- title: z.string(),
354
- description: z.string().optional(),
355
- status: TaskStatus.optional(),
356
- priority: TaskPriority.optional(),
357
- project_id: z.number().optional(),
358
- dependencies: z.array(z.number()).optional(),
359
- tags: z.array(z.string()).optional(),
360
- links: z.array(LinkSchema).optional(),
361
- due_date: z.string().optional(),
362
- estimated_time: z.number().optional(),
363
- }, { readOnlyHint: false }, async (params) => createTask(params));
364
- server.tool('list_tasks', 'List tasks with optional filters. Use at conversation start to see what is in progress or blocked. Filter by status, project, priority, or tag.', {
365
- status: TaskStatus.optional(),
366
- project_id: z.number().optional(),
367
- priority: TaskPriority.optional(),
368
- tag: z.string().optional(),
369
- }, { readOnlyHint: true }, async (params) => listTasks(params));
370
- server.tool('get_task', 'Get a task by ID with time tracking info. Read the description carefully — it often contains implementation details and acceptance criteria.', { id: z.number() }, { readOnlyHint: true }, async (params) => getTask(params));
371
- server.tool('update_task', 'Update task fields. Use this to add details, update descriptions with progress notes, or adjust priority as you learn more.', {
372
- id: z.number(),
373
- title: z.string().optional(),
374
- description: z.string().optional(),
375
- status: TaskStatus.optional(),
376
- priority: TaskPriority.optional(),
377
- project_id: z.number().optional(),
378
- dependencies: z.array(z.number()).optional(),
379
- tags: z.array(z.string()).optional(),
380
- links: z.array(LinkSchema).optional(),
381
- due_date: z.string().optional(),
382
- estimated_time: z.number().optional(),
383
- allowed_tools: z.array(z.string()).optional().describe('Allowlist of MCP tool names for this task. If set, only these tools can be used while timer is active.'),
384
- denied_tools: z.array(z.string()).optional().describe('Denylist of MCP tool names for this task. These tools are blocked while timer is active.'),
385
- }, { readOnlyHint: false }, async (params) => updateTask(params));
386
- server.tool('update_task_status', 'Update task status with transition validation. Use when a task becomes blocked, is partially done, or needs to be reopened.', {
387
- id: z.number(),
388
- status: TaskStatus,
389
- }, { readOnlyHint: false }, async (params) => updateTaskStatus(params));
390
- server.tool('delete_task', 'Delete a task by ID. Use sparingly — prefer updating status to "done" instead of deleting.', { id: z.number() }, { destructiveHint: true }, async (params) => deleteTask(params));
391
- server.tool('bulk_create_tasks', 'Create multiple tasks in a single transaction. Useful when breaking down a feature into subtasks.', {
392
- tasks: z.array(z.object({
393
- title: z.string(),
394
- description: z.string().optional(),
395
- priority: TaskPriority.optional(),
396
- project_id: z.number().optional(),
397
- status: TaskStatus.optional(),
398
- dependencies: z.array(z.number()).optional(),
399
- tags: z.array(z.string()).optional(),
400
- })),
401
- }, { readOnlyHint: false }, async (params) => bulkCreateTasks(params));
402
- server.tool('search_tasks', 'Search tasks by title or description. Use this to find tasks related to your current work before creating duplicates.', { query: z.string() }, { readOnlyHint: true }, async (params) => searchTasks(params));
403
- }
@@ -1,4 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- /** Validate keys before sending — blocks shell escape patterns */
3
- export declare function validateKeys(keys: string): string | null;
4
- export declare function registerTerminalTools(server: McpServer): void;