@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.
- package/README.md +138 -193
- package/dist/attachment-download.d.ts +74 -0
- package/dist/attachment-download.js +193 -0
- package/dist/attachment-download.js.map +1 -0
- package/dist/attachments.d.ts +23 -0
- package/dist/attachments.js +66 -0
- package/dist/attachments.js.map +1 -0
- package/dist/client.d.ts +206 -0
- package/dist/client.js +279 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +137 -18
- package/dist/config.js +187 -106
- package/dist/config.js.map +1 -0
- package/dist/connect.d.ts +89 -0
- package/dist/connect.js +269 -0
- package/dist/connect.js.map +1 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.js +120 -0
- package/dist/doctor.js.map +1 -0
- package/dist/events.d.ts +186 -0
- package/dist/events.js +407 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +121 -218
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +114 -0
- package/dist/instructions.js.map +1 -0
- package/dist/mint.d.ts +62 -0
- package/dist/mint.js +135 -0
- package/dist/mint.js.map +1 -0
- package/dist/mirror.d.ts +68 -0
- package/dist/mirror.js +103 -0
- package/dist/mirror.js.map +1 -0
- package/dist/pane-queue.d.ts +29 -0
- package/dist/pane-queue.js +35 -0
- package/dist/pane-queue.js.map +1 -0
- package/dist/prompts.d.ts +79 -0
- package/dist/prompts.js +211 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resolve.d.ts +72 -0
- package/dist/resolve.js +89 -0
- package/dist/resolve.js.map +1 -0
- package/dist/runtime.d.ts +54 -0
- package/dist/runtime.js +321 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +56 -0
- package/dist/server.js +793 -0
- package/dist/server.js.map +1 -0
- package/dist/session-identifier.d.ts +48 -0
- package/dist/session-identifier.js +44 -0
- package/dist/session-identifier.js.map +1 -0
- package/dist/sessions-store.d.ts +38 -0
- package/dist/sessions-store.js +88 -0
- package/dist/sessions-store.js.map +1 -0
- package/dist/tmux.d.ts +200 -0
- package/dist/tmux.js +580 -0
- package/dist/tmux.js.map +1 -0
- package/hooks/metadata.mjs +99 -0
- package/hooks/permission-prompt.mjs +100 -0
- package/hooks/taskflow-hook.mjs +499 -0
- package/hooks/tool-logging.mjs +63 -0
- package/package.json +38 -29
- package/dist/agent-registry.d.ts +0 -28
- package/dist/agent-registry.js +0 -158
- package/dist/db.d.ts +0 -5
- package/dist/db.js +0 -220
- package/dist/helpers.d.ts +0 -21
- package/dist/helpers.js +0 -27
- package/dist/resources.d.ts +0 -2
- package/dist/resources.js +0 -89
- package/dist/retry.d.ts +0 -34
- package/dist/retry.js +0 -94
- package/dist/sse.d.ts +0 -10
- package/dist/sse.js +0 -824
- package/dist/tmux-bridge.d.ts +0 -13
- package/dist/tmux-bridge.js +0 -217
- package/dist/tools/activity.d.ts +0 -39
- package/dist/tools/activity.js +0 -152
- package/dist/tools/agent-inbox.d.ts +0 -12
- package/dist/tools/agent-inbox.js +0 -272
- package/dist/tools/agent.d.ts +0 -14
- package/dist/tools/agent.js +0 -168
- package/dist/tools/analytics.d.ts +0 -21
- package/dist/tools/analytics.js +0 -191
- package/dist/tools/checkpoint.d.ts +0 -27
- package/dist/tools/checkpoint.js +0 -105
- package/dist/tools/notifications.d.ts +0 -31
- package/dist/tools/notifications.js +0 -59
- package/dist/tools/projects.d.ts +0 -55
- package/dist/tools/projects.js +0 -112
- package/dist/tools/settings.d.ts +0 -19
- package/dist/tools/settings.js +0 -73
- package/dist/tools/tasks.d.ts +0 -105
- package/dist/tools/tasks.js +0 -403
- package/dist/tools/terminal.d.ts +0 -4
- package/dist/tools/terminal.js +0 -98
- package/dist/tools/timer.d.ts +0 -37
- package/dist/tools/timer.js +0 -154
- package/dist/types.d.ts +0 -83
- package/dist/types.js +0 -30
package/dist/tools/settings.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/tasks.d.ts
DELETED
|
@@ -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;
|
package/dist/tools/tasks.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/terminal.d.ts
DELETED
|
@@ -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;
|