@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,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;
@@ -1,98 +0,0 @@
1
- import { z } from 'zod';
2
- import { execFileSync } from 'child_process';
3
- import { getDb } from '../db.js';
4
- import { logActivity, errorResponse, successResponse } from '../helpers.js';
5
- const MAX_KEYS_LENGTH = 200;
6
- // Patterns that should never be sent via remote key injection
7
- const BLOCKED_PATTERNS = [
8
- /^\s*!/, // Claude Code shell escape (! command)
9
- /;\s*!/, // shell escape after semicolon
10
- ];
11
- /** Validate keys before sending — blocks shell escape patterns */
12
- export function validateKeys(keys) {
13
- if (keys.length > MAX_KEYS_LENGTH) {
14
- return `Keys too long (${keys.length} chars, max ${MAX_KEYS_LENGTH}) — potential injection`;
15
- }
16
- for (const pattern of BLOCKED_PATTERNS) {
17
- if (pattern.test(keys)) {
18
- return 'Blocked: shell escape pattern detected (! prefix). Use the terminal directly for shell commands.';
19
- }
20
- }
21
- return null; // valid
22
- }
23
- function getAgentPane(agentName, requireConnected = false) {
24
- const db = getDb();
25
- const agent = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(agentName);
26
- if (!agent)
27
- return { error: errorResponse(`Agent "${agentName}" not found`, 'NOT_FOUND') };
28
- if (!agent.tmux_pane)
29
- return { error: errorResponse(`Agent "${agentName}" has no tmux pane`, 'VALIDATION_ERROR') };
30
- if (requireConnected && agent.status !== 'connected') {
31
- return { error: errorResponse(`Agent "${agentName}" is disconnected — sending keys to a bare shell is blocked for security`, 'VALIDATION_ERROR') };
32
- }
33
- return { agent };
34
- }
35
- export function registerTerminalTools(server) {
36
- server.tool('capture_terminal', 'Capture the current terminal content of an agent\'s tmux pane. Returns the visible text on screen. Useful for seeing what prompts or output an agent is displaying.', {
37
- agent_name: z.string().describe('Name of the agent whose terminal to capture'),
38
- }, { readOnlyHint: true }, async (params) => {
39
- const result = getAgentPane(params.agent_name);
40
- if (result.error)
41
- return result.error;
42
- try {
43
- const output = execFileSync('tmux', ['capture-pane', '-p', '-S', '-', '-t', result.agent.tmux_pane], { timeout: 5000 }).toString();
44
- return successResponse({
45
- agent: params.agent_name,
46
- pane: result.agent.tmux_pane,
47
- content: output,
48
- });
49
- }
50
- catch (err) {
51
- return errorResponse(`Failed to capture pane: ${err.message}`, 'VALIDATION_ERROR');
52
- }
53
- });
54
- server.tool('send_keys', 'Send raw keystrokes to an agent\'s tmux pane. Use this to respond to interactive prompts (yes/no, numbered choices, permission approvals) displayed in an agent\'s terminal. By default appends Enter after the keys.', {
55
- agent_name: z.string().describe('Name of the agent whose terminal to send keys to'),
56
- keys: z.string().describe('The keys/text to send (e.g. "yes", "1", "y") or tmux key names (e.g. "Escape", "Up", "Down", "BTab")'),
57
- enter: z.boolean().optional().describe('Whether to press Enter after the keys (default: true)'),
58
- literal: z.boolean().optional().describe('Send as literal text with -l flag (default: true). Set false for tmux key names like Escape, Up, Down, Left, Right, BTab'),
59
- }, { destructiveHint: true }, async (params) => {
60
- const result = getAgentPane(params.agent_name, true);
61
- if (result.error)
62
- return result.error;
63
- // Validate keys for injection attacks
64
- const violation = validateKeys(params.keys);
65
- if (violation)
66
- return errorResponse(violation, 'VALIDATION_ERROR');
67
- const sendEnter = params.enter !== false;
68
- const isLiteral = params.literal !== false;
69
- try {
70
- const pane = result.agent.tmux_pane;
71
- if (isLiteral) {
72
- // Send as literal text (like typing on a keyboard)
73
- execFileSync('tmux', ['send-keys', '-t', pane, '-l', params.keys], { stdio: 'ignore', timeout: 5000 });
74
- if (sendEnter) {
75
- execFileSync('tmux', ['send-keys', '-t', pane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
76
- }
77
- }
78
- else {
79
- // Send as tmux key names (Escape, Up, Down, etc.)
80
- const args = ['send-keys', '-t', pane, params.keys];
81
- if (sendEnter)
82
- args.push('Enter');
83
- execFileSync('tmux', args, { stdio: 'ignore', timeout: 5000 });
84
- }
85
- logActivity('terminal_send_keys', `Sent keys to ${params.agent_name}: ${params.keys.slice(0, 50)}`, { entityType: 'agent' });
86
- return successResponse({
87
- agent: params.agent_name,
88
- pane: result.agent.tmux_pane,
89
- keys: params.keys,
90
- enter: sendEnter,
91
- sent: true,
92
- });
93
- }
94
- catch (err) {
95
- return errorResponse(`Failed to send keys: ${err.message}`, 'VALIDATION_ERROR');
96
- }
97
- });
98
- }
@@ -1,37 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function startTimer(params: {
3
- task_id: number;
4
- }): Promise<{
5
- content: {
6
- type: "text";
7
- text: string;
8
- }[];
9
- }>;
10
- export declare function pauseTimer(params: {
11
- task_id: number;
12
- }): Promise<{
13
- content: {
14
- type: "text";
15
- text: string;
16
- }[];
17
- }>;
18
- export declare function stopTimer(params: {
19
- task_id: number;
20
- final_status?: string;
21
- }): Promise<{
22
- content: {
23
- type: "text";
24
- text: string;
25
- }[];
26
- }>;
27
- export declare function listSessions(params: {
28
- task_id?: number;
29
- start_date?: string;
30
- end_date?: string;
31
- }): Promise<{
32
- content: {
33
- type: "text";
34
- text: string;
35
- }[];
36
- }>;
37
- export declare function registerTimerTools(server: McpServer): void;