@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.
- 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 +124 -184
- 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 -27
- package/dist/agent-registry.js +0 -153
- 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/sse.d.ts +0 -10
- package/dist/sse.js +0 -766
- package/dist/tmux-bridge.d.ts +0 -12
- package/dist/tmux-bridge.js +0 -157
- 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 -257
- 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 -82
- package/dist/types.js +0 -30
package/dist/tmux-bridge.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
interface BridgeOptions {
|
|
2
|
-
/** Returns the current agent name — must be a getter so renames are picked up */
|
|
3
|
-
getAgentName: () => string;
|
|
4
|
-
agentPid: number;
|
|
5
|
-
tmuxPane: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Start the tmux bridge: SSE listener for instant message delivery.
|
|
9
|
-
* Returns a cleanup function for graceful shutdown.
|
|
10
|
-
*/
|
|
11
|
-
export declare function startTmuxBridge(options: BridgeOptions): () => void;
|
|
12
|
-
export {};
|
package/dist/tmux-bridge.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import { getDb } from './db.js';
|
|
3
|
-
import { getActivePort } from './sse.js';
|
|
4
|
-
import http from 'http';
|
|
5
|
-
// ─── SSE Listener (replaces the 3s poller) ───────────────────────────
|
|
6
|
-
function startSSEListener(options) {
|
|
7
|
-
const port = getActivePort();
|
|
8
|
-
function connect() {
|
|
9
|
-
const req = http.get(`http://localhost:${port}/events`, (res) => {
|
|
10
|
-
let buffer = '';
|
|
11
|
-
let eventType = ''; // persists across chunks — SSE fields may split across TCP segments
|
|
12
|
-
res.on('data', (chunk) => {
|
|
13
|
-
buffer += chunk.toString();
|
|
14
|
-
const lines = buffer.split('\n');
|
|
15
|
-
buffer = lines.pop() || '';
|
|
16
|
-
for (const line of lines) {
|
|
17
|
-
if (line.startsWith('event: ')) {
|
|
18
|
-
eventType = line.slice(7).trim();
|
|
19
|
-
}
|
|
20
|
-
else if (line.startsWith('data: ') && eventType) {
|
|
21
|
-
try {
|
|
22
|
-
const data = JSON.parse(line.slice(6));
|
|
23
|
-
handleSSEEvent(eventType, data, options);
|
|
24
|
-
}
|
|
25
|
-
catch { /* malformed JSON */ }
|
|
26
|
-
eventType = '';
|
|
27
|
-
}
|
|
28
|
-
else if (line === '') {
|
|
29
|
-
// SSE event delimiter — reset state for next event
|
|
30
|
-
eventType = '';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
res.on('end', () => {
|
|
35
|
-
console.error('[bridge] SSE connection closed, reconnecting in 3s...');
|
|
36
|
-
setTimeout(connect, 3000);
|
|
37
|
-
});
|
|
38
|
-
res.on('error', () => {
|
|
39
|
-
console.error('[bridge] SSE connection error, reconnecting in 3s...');
|
|
40
|
-
setTimeout(connect, 3000);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
req.on('error', () => {
|
|
44
|
-
console.error('[bridge] SSE connect failed, retrying in 3s...');
|
|
45
|
-
setTimeout(connect, 3000);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
// Initial sweep: deliver any undelivered messages from before SSE connected
|
|
49
|
-
deliverUndelivered(options);
|
|
50
|
-
connect();
|
|
51
|
-
}
|
|
52
|
-
function handleSSEEvent(event, data, options) {
|
|
53
|
-
const { getAgentName, agentPid, tmuxPane } = options;
|
|
54
|
-
const agentName = getAgentName();
|
|
55
|
-
const payload = data.payload;
|
|
56
|
-
if (!payload)
|
|
57
|
-
return;
|
|
58
|
-
if (event === 'agent_question') {
|
|
59
|
-
const recipient = payload.recipient_name;
|
|
60
|
-
const sender = payload.sender_name;
|
|
61
|
-
const status = payload.status;
|
|
62
|
-
const id = payload.id;
|
|
63
|
-
const delivered = payload.delivered;
|
|
64
|
-
if (recipient !== agentName || status !== 'pending' || delivered === 1)
|
|
65
|
-
return;
|
|
66
|
-
let text;
|
|
67
|
-
if (sender === 'user') {
|
|
68
|
-
text = `[Message from User]: ${payload.question}`;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
text = `[Message from ${sender}]: ${payload.question}`;
|
|
72
|
-
}
|
|
73
|
-
injectAndMarkDelivered(id, text, tmuxPane);
|
|
74
|
-
}
|
|
75
|
-
if (event === 'agent_question_answered') {
|
|
76
|
-
const sender = payload.sender_name;
|
|
77
|
-
const recipient = payload.recipient_name;
|
|
78
|
-
const status = payload.status;
|
|
79
|
-
const id = payload.id;
|
|
80
|
-
const delivered = payload.delivered;
|
|
81
|
-
const agentPidField = payload.agent_pid;
|
|
82
|
-
const isOurs = sender === agentName || agentPidField === agentPid;
|
|
83
|
-
if (!isOurs || status !== 'answered' || delivered === 1)
|
|
84
|
-
return;
|
|
85
|
-
const question = (payload.question || '').slice(0, 60);
|
|
86
|
-
const response = payload.response;
|
|
87
|
-
const text = `[Inbox Response] to "${question}": ${response}`;
|
|
88
|
-
injectAndMarkDelivered(id, text, tmuxPane);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function injectAndMarkDelivered(id, text, tmuxPane) {
|
|
92
|
-
const db = getDb();
|
|
93
|
-
db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
|
|
94
|
-
try {
|
|
95
|
-
// Send text literally (-l) so special chars are safe and tmux doesn't interpret them
|
|
96
|
-
execFileSync('tmux', ['send-keys', '-t', tmuxPane, '-l', text], { stdio: 'ignore', timeout: 5000 });
|
|
97
|
-
// Delay before Enter — gives the CLI time to fully process the bracketed paste.
|
|
98
|
-
// Without this, Codex (and similar TUIs) may only show partial text because
|
|
99
|
-
// the Enter arrives inside the paste bracket and gets swallowed.
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
try {
|
|
102
|
-
execFileSync('tmux', ['send-keys', '-t', tmuxPane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
|
|
103
|
-
}
|
|
104
|
-
catch { /* pane may have closed */ }
|
|
105
|
-
// Second Enter after another delay — catches CLIs that need an extra nudge
|
|
106
|
-
// after bracketed paste ends (e.g. long messages that trigger paste mode)
|
|
107
|
-
setTimeout(() => {
|
|
108
|
-
try {
|
|
109
|
-
execFileSync('tmux', ['send-keys', '-t', tmuxPane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
|
|
110
|
-
}
|
|
111
|
-
catch { /* pane may have closed */ }
|
|
112
|
-
}, 500);
|
|
113
|
-
}, 300);
|
|
114
|
-
console.error(`[bridge] delivered message ${id} to tmux pane ${tmuxPane}`);
|
|
115
|
-
}
|
|
116
|
-
catch (err) {
|
|
117
|
-
console.error(`[bridge] tmux send-keys failed for message ${id}:`, err);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function deliverUndelivered(options) {
|
|
121
|
-
const { getAgentName, agentPid, tmuxPane } = options;
|
|
122
|
-
const agentName = getAgentName();
|
|
123
|
-
const db = getDb();
|
|
124
|
-
const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
|
|
125
|
-
(recipient_name = ? AND status = 'pending') OR
|
|
126
|
-
(sender_name = ? AND status = 'answered') OR
|
|
127
|
-
(agent_pid = ? AND status = 'answered')
|
|
128
|
-
)`).all(agentName, agentName, agentPid);
|
|
129
|
-
for (const msg of incoming) {
|
|
130
|
-
let text;
|
|
131
|
-
if (msg.recipient_name === agentName && msg.sender_name === 'user') {
|
|
132
|
-
text = `[Message from User]: ${msg.question}`;
|
|
133
|
-
}
|
|
134
|
-
else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
|
|
135
|
-
text = `[Message from ${msg.sender_name}]: ${msg.question}`;
|
|
136
|
-
}
|
|
137
|
-
else if (msg.status === 'answered' && msg.response) {
|
|
138
|
-
text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
injectAndMarkDelivered(msg.id, text, tmuxPane);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// ─── Public API ──────────────────────────────────────────────────────
|
|
147
|
-
/**
|
|
148
|
-
* Start the tmux bridge: SSE listener for instant message delivery.
|
|
149
|
-
* Returns a cleanup function for graceful shutdown.
|
|
150
|
-
*/
|
|
151
|
-
export function startTmuxBridge(options) {
|
|
152
|
-
startSSEListener(options);
|
|
153
|
-
console.error(`[bridge] tmux bridge active for agent "${options.getAgentName()}" on pane ${options.tmuxPane}`);
|
|
154
|
-
return () => {
|
|
155
|
-
// SSE connection will close when process exits
|
|
156
|
-
};
|
|
157
|
-
}
|
package/dist/tools/activity.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
export declare function getActivityLog(params: {
|
|
3
|
-
limit?: number;
|
|
4
|
-
action?: string;
|
|
5
|
-
entity_type?: string;
|
|
6
|
-
}): Promise<{
|
|
7
|
-
content: {
|
|
8
|
-
type: "text";
|
|
9
|
-
text: string;
|
|
10
|
-
}[];
|
|
11
|
-
}>;
|
|
12
|
-
export declare function clearActivityLog(): Promise<{
|
|
13
|
-
content: {
|
|
14
|
-
type: "text";
|
|
15
|
-
text: string;
|
|
16
|
-
}[];
|
|
17
|
-
}>;
|
|
18
|
-
export declare function compactActivityLog(params: {
|
|
19
|
-
preserve_recent?: number;
|
|
20
|
-
max_entries?: number;
|
|
21
|
-
dry_run?: boolean;
|
|
22
|
-
}): Promise<{
|
|
23
|
-
content: {
|
|
24
|
-
type: "text";
|
|
25
|
-
text: string;
|
|
26
|
-
}[];
|
|
27
|
-
}>;
|
|
28
|
-
export declare function logDebug(params: {
|
|
29
|
-
message: string;
|
|
30
|
-
task_id?: number;
|
|
31
|
-
project_id?: number;
|
|
32
|
-
detail?: string;
|
|
33
|
-
}): Promise<{
|
|
34
|
-
content: {
|
|
35
|
-
type: "text";
|
|
36
|
-
text: string;
|
|
37
|
-
}[];
|
|
38
|
-
}>;
|
|
39
|
-
export declare function registerActivityTools(server: McpServer): void;
|
package/dist/tools/activity.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getDb } from '../db.js';
|
|
3
|
-
import { successResponse, broadcastChange } from '../helpers.js';
|
|
4
|
-
// ─── exported handler functions ───────────────────────────────────────
|
|
5
|
-
export async function getActivityLog(params) {
|
|
6
|
-
const db = getDb();
|
|
7
|
-
const limit = params.limit ?? 50;
|
|
8
|
-
const conditions = [];
|
|
9
|
-
const values = [];
|
|
10
|
-
if (params.action) {
|
|
11
|
-
conditions.push('action = ?');
|
|
12
|
-
values.push(params.action);
|
|
13
|
-
}
|
|
14
|
-
if (params.entity_type) {
|
|
15
|
-
conditions.push('entity_type = ?');
|
|
16
|
-
values.push(params.entity_type);
|
|
17
|
-
}
|
|
18
|
-
let sql = 'SELECT * FROM activity_logs';
|
|
19
|
-
if (conditions.length > 0) {
|
|
20
|
-
sql += ' WHERE ' + conditions.join(' AND ');
|
|
21
|
-
}
|
|
22
|
-
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
23
|
-
values.push(limit);
|
|
24
|
-
const rows = db.prepare(sql).all(...values);
|
|
25
|
-
return successResponse(rows);
|
|
26
|
-
}
|
|
27
|
-
export async function clearActivityLog() {
|
|
28
|
-
const db = getDb();
|
|
29
|
-
const countRow = db.prepare('SELECT COUNT(*) AS count FROM activity_logs').get();
|
|
30
|
-
db.prepare('DELETE FROM activity_logs').run();
|
|
31
|
-
broadcastChange('activity', 'activity_cleared', {});
|
|
32
|
-
return successResponse({ deleted: countRow.count, message: 'Activity log cleared' });
|
|
33
|
-
}
|
|
34
|
-
// ─── compaction ──────────────────────────────────────────────────────
|
|
35
|
-
const DEFAULT_PRESERVE_RECENT = 50;
|
|
36
|
-
const DEFAULT_MAX_LOG_ENTRIES = 500;
|
|
37
|
-
/** Actions that are always preserved in full (never compacted) */
|
|
38
|
-
const MILESTONE_ACTIONS = new Set([
|
|
39
|
-
'task_created', 'task_completed', 'task_partial_done',
|
|
40
|
-
'project_created', 'project_deleted',
|
|
41
|
-
'agent_connected', 'agent_disconnected', 'agent_renamed',
|
|
42
|
-
]);
|
|
43
|
-
export async function compactActivityLog(params) {
|
|
44
|
-
const db = getDb();
|
|
45
|
-
const preserveRecent = params.preserve_recent ?? DEFAULT_PRESERVE_RECENT;
|
|
46
|
-
const maxEntries = params.max_entries ?? DEFAULT_MAX_LOG_ENTRIES;
|
|
47
|
-
const totalCount = db.prepare('SELECT COUNT(*) AS count FROM activity_logs').get().count;
|
|
48
|
-
if (totalCount <= maxEntries) {
|
|
49
|
-
return successResponse({
|
|
50
|
-
compacted: false,
|
|
51
|
-
total_entries: totalCount,
|
|
52
|
-
message: `Log has ${totalCount} entries (threshold: ${maxEntries}) — no compaction needed.`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Get all entries sorted by date
|
|
56
|
-
const allEntries = db.prepare('SELECT * FROM activity_logs ORDER BY created_at ASC').all();
|
|
57
|
-
// Split into: entries to compact vs entries to preserve
|
|
58
|
-
const cutoffIndex = allEntries.length - preserveRecent;
|
|
59
|
-
const toCompact = allEntries.slice(0, cutoffIndex);
|
|
60
|
-
const toPreserve = allEntries.slice(cutoffIndex);
|
|
61
|
-
if (toCompact.length === 0) {
|
|
62
|
-
return successResponse({
|
|
63
|
-
compacted: false,
|
|
64
|
-
total_entries: totalCount,
|
|
65
|
-
message: 'All entries are within the preserve window — nothing to compact.',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
// Generate summary of compacted entries
|
|
69
|
-
const actionCounts = {};
|
|
70
|
-
const milestones = [];
|
|
71
|
-
const timeRange = {
|
|
72
|
-
start: toCompact[0].created_at,
|
|
73
|
-
end: toCompact[toCompact.length - 1].created_at,
|
|
74
|
-
};
|
|
75
|
-
for (const entry of toCompact) {
|
|
76
|
-
actionCounts[entry.action] = (actionCounts[entry.action] || 0) + 1;
|
|
77
|
-
if (MILESTONE_ACTIONS.has(entry.action)) {
|
|
78
|
-
milestones.push(`[${entry.created_at.slice(0, 16)}] ${entry.action}: ${entry.title}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const summaryDetail = [
|
|
82
|
-
`## Compaction Summary`,
|
|
83
|
-
`**Period:** ${timeRange.start} → ${timeRange.end}`,
|
|
84
|
-
`**Entries compacted:** ${toCompact.length}`,
|
|
85
|
-
``,
|
|
86
|
-
`### Action Counts`,
|
|
87
|
-
...Object.entries(actionCounts)
|
|
88
|
-
.sort((a, b) => b[1] - a[1])
|
|
89
|
-
.map(([action, count]) => `- \`${action}\`: ${count}`),
|
|
90
|
-
``,
|
|
91
|
-
`### Key Milestones`,
|
|
92
|
-
...(milestones.length > 0
|
|
93
|
-
? milestones.slice(-20).map(m => `- ${m}`)
|
|
94
|
-
: ['- (none)']),
|
|
95
|
-
].join('\n');
|
|
96
|
-
if (params.dry_run) {
|
|
97
|
-
return successResponse({
|
|
98
|
-
compacted: false,
|
|
99
|
-
dry_run: true,
|
|
100
|
-
would_compact: toCompact.length,
|
|
101
|
-
would_preserve: toPreserve.length,
|
|
102
|
-
summary_preview: summaryDetail,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
// Delete compacted entries and insert summary
|
|
106
|
-
const compactIds = toCompact.map(e => e.id);
|
|
107
|
-
const placeholders = compactIds.map(() => '?').join(',');
|
|
108
|
-
db.prepare(`DELETE FROM activity_logs WHERE id IN (${placeholders})`).run(...compactIds);
|
|
109
|
-
const now = new Date().toISOString();
|
|
110
|
-
db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
|
|
111
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run('compaction_summary', `Compacted ${toCompact.length} activity log entries (${timeRange.start.slice(0, 10)} → ${timeRange.end.slice(0, 10)})`, summaryDetail, null, null, now);
|
|
112
|
-
broadcastChange('activity', 'activity_compacted', { compacted: toCompact.length, preserved: toPreserve.length });
|
|
113
|
-
return successResponse({
|
|
114
|
-
compacted: true,
|
|
115
|
-
entries_removed: toCompact.length,
|
|
116
|
-
entries_preserved: toPreserve.length,
|
|
117
|
-
summary_inserted: true,
|
|
118
|
-
new_total: toPreserve.length + 1,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
export async function logDebug(params) {
|
|
122
|
-
const db = getDb();
|
|
123
|
-
const now = new Date().toISOString();
|
|
124
|
-
// task_id takes priority; fall back to project_id
|
|
125
|
-
const entityType = params.task_id ? 'task' : params.project_id ? 'project' : null;
|
|
126
|
-
const entityId = params.task_id ?? params.project_id ?? null;
|
|
127
|
-
const result = db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
|
|
128
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run('debug_log', params.message, params.detail ?? null, entityType, entityId, now);
|
|
129
|
-
const entry = db.prepare('SELECT * FROM activity_logs WHERE id = ?').get(result.lastInsertRowid);
|
|
130
|
-
broadcastChange('activity', 'activity_logged', entry);
|
|
131
|
-
return successResponse({ id: result.lastInsertRowid, message: params.message });
|
|
132
|
-
}
|
|
133
|
-
// ─── MCP registration ─────────────────────────────────────────────────
|
|
134
|
-
export function registerActivityTools(server) {
|
|
135
|
-
server.tool('get_activity_log', 'Retrieve recent activity log entries. Shows what has changed — task completions, timer events, status transitions. Filter by action or entity type.', {
|
|
136
|
-
limit: z.number().optional(),
|
|
137
|
-
action: z.string().optional(),
|
|
138
|
-
entity_type: z.string().optional(),
|
|
139
|
-
}, { readOnlyHint: true }, async (params) => getActivityLog(params));
|
|
140
|
-
server.tool('clear_activity_log', 'Delete all activity log entries. Use with caution — this is irreversible.', {}, { destructiveHint: true }, async () => clearActivityLog());
|
|
141
|
-
server.tool('log_debug', 'Log a debug entry to the activity log. Use this to record your work process — what you investigated, commands you ran, decisions you made, and findings. Entries appear in the Activity Pulse and on the project page. Link to a task (task_id) or project (project_id) for context.', {
|
|
142
|
-
message: z.string().describe('Short summary of what you are doing or found'),
|
|
143
|
-
detail: z.string().optional().describe('Longer explanation — stack traces, error messages, hypotheses, commands run, what you tried'),
|
|
144
|
-
task_id: z.number().optional().describe('Link this debug log to a specific task'),
|
|
145
|
-
project_id: z.number().optional().describe('Link this debug log to a project (used when no specific task applies)'),
|
|
146
|
-
}, { readOnlyHint: false }, async (params) => logDebug(params));
|
|
147
|
-
server.tool('compact_activity_log', 'Compact old activity log entries into a summary. Keeps the most recent entries (default 50) and summarizes the rest. Use dry_run=true to preview what would be compacted. Runs automatically when log exceeds max_entries threshold.', {
|
|
148
|
-
preserve_recent: z.number().optional().describe('Number of recent entries to keep verbatim (default: 50)'),
|
|
149
|
-
max_entries: z.number().optional().describe('Only compact if log exceeds this count (default: 500)'),
|
|
150
|
-
dry_run: z.boolean().optional().describe('If true, show what would be compacted without doing it'),
|
|
151
|
-
}, { readOnlyHint: false }, async (params) => compactActivityLog(params));
|
|
152
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
/** The name assigned to this agent after registration */
|
|
3
|
-
declare let myAgentName: string | null;
|
|
4
|
-
/** Set the agent name from external registration (index.ts).
|
|
5
|
-
* This prevents double-registration when index.ts registers at startup
|
|
6
|
-
* and ensureRegistered() would register again on first tool call.
|
|
7
|
-
*/
|
|
8
|
-
export declare function setAgentName(name: string): void;
|
|
9
|
-
export { myAgentName };
|
|
10
|
-
/** Get the current agent name — used by tmux bridge to track renames */
|
|
11
|
-
export declare function getAgentName(): string;
|
|
12
|
-
export declare function registerAgentInboxTools(server: McpServer): void;
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
import { getDb } from '../db.js';
|
|
4
|
-
import { logActivity, errorResponse, successResponse, now, broadcastChange } from '../helpers.js';
|
|
5
|
-
import { registerAgent as doRegister, getAgent, listAgents } from '../agent-registry.js';
|
|
6
|
-
/** The name assigned to this agent after registration */
|
|
7
|
-
let myAgentName = null;
|
|
8
|
-
/** Set the agent name from external registration (index.ts).
|
|
9
|
-
* This prevents double-registration when index.ts registers at startup
|
|
10
|
-
* and ensureRegistered() would register again on first tool call.
|
|
11
|
-
*/
|
|
12
|
-
export function setAgentName(name) {
|
|
13
|
-
myAgentName = name;
|
|
14
|
-
}
|
|
15
|
-
/** Get or auto-register the agent name.
|
|
16
|
-
* Verifies the cached registration is still valid — if the liveness
|
|
17
|
-
* checker disconnected us while sleeping, re-register to reclaim identity.
|
|
18
|
-
*/
|
|
19
|
-
function ensureRegistered() {
|
|
20
|
-
if (myAgentName) {
|
|
21
|
-
// Verify our registration is still active
|
|
22
|
-
const agent = getAgent(myAgentName);
|
|
23
|
-
if (agent && agent.status === 'connected' && agent.pid === process.ppid) {
|
|
24
|
-
return myAgentName;
|
|
25
|
-
}
|
|
26
|
-
// Stale — re-register
|
|
27
|
-
myAgentName = null;
|
|
28
|
-
}
|
|
29
|
-
myAgentName = doRegister();
|
|
30
|
-
return myAgentName;
|
|
31
|
-
}
|
|
32
|
-
export { myAgentName };
|
|
33
|
-
/** Get the current agent name — used by tmux bridge to track renames */
|
|
34
|
-
export function getAgentName() {
|
|
35
|
-
return myAgentName || 'unknown';
|
|
36
|
-
}
|
|
37
|
-
export function registerAgentInboxTools(server) {
|
|
38
|
-
server.tool('register_agent', 'Register this agent with a custom name. Optional — agents auto-register on startup using the project folder name. Call this only if you want a specific name.', {
|
|
39
|
-
name: z.string().optional().describe('Custom agent name. If omitted, uses the project folder name.'),
|
|
40
|
-
}, { readOnlyHint: false, idempotentHint: true }, async (params) => {
|
|
41
|
-
myAgentName = doRegister({ customName: params.name });
|
|
42
|
-
return successResponse({ name: myAgentName, message: `Registered as "${myAgentName}"` });
|
|
43
|
-
});
|
|
44
|
-
server.tool('ask_user', 'Post a question to the TaskFlow Agent Inbox for the user to answer remotely. Returns immediately with the message ID. The question appears in the Agent Inbox UI with full context and optional quick-tap choices. After posting, use check_response to retrieve the user\'s answer. Always tell the user you posted a question so they know to check the inbox.', {
|
|
45
|
-
project_id: z.number().describe('Project ID to attach the question to'),
|
|
46
|
-
question: z.string().describe('The question to ask the user'),
|
|
47
|
-
context: z.string().optional().describe('Markdown context — proposals, trade-offs, code snippets shown before the question'),
|
|
48
|
-
choices: z.array(z.string()).optional().describe('Optional quick-tap choices, e.g. ["Yes", "No", "Skip"]'),
|
|
49
|
-
}, { readOnlyHint: false }, async (params) => {
|
|
50
|
-
const db = getDb();
|
|
51
|
-
const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(params.project_id);
|
|
52
|
-
if (!project)
|
|
53
|
-
return errorResponse(`Project ${params.project_id} not found`, 'NOT_FOUND');
|
|
54
|
-
const senderName = ensureRegistered();
|
|
55
|
-
const ts = now();
|
|
56
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, created_at)
|
|
57
|
-
VALUES (?, ?, ?, ?, ?, 'user', ?, 'mcp', 'pending', ?)`).run(params.project_id, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, process.ppid, ts);
|
|
58
|
-
const id = result.lastInsertRowid;
|
|
59
|
-
const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
60
|
-
broadcastChange('agent_message', 'agent_question', message);
|
|
61
|
-
logActivity('agent_question', params.question, { entityType: 'agent_message', entityId: id });
|
|
62
|
-
return successResponse({
|
|
63
|
-
id,
|
|
64
|
-
status: 'pending',
|
|
65
|
-
sender: senderName,
|
|
66
|
-
message: `Question posted to Agent Inbox (id: ${id}). Use check_response(${id}) to retrieve the user's answer.`,
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
server.tool('check_response', 'Check if a previously posted question (via ask_user or ask_agent) has been answered. Returns the response if answered, or status "pending" if still waiting.', {
|
|
70
|
-
message_id: z.number().describe('The message ID returned by ask_user or ask_agent'),
|
|
71
|
-
}, { readOnlyHint: true }, async (params) => {
|
|
72
|
-
const db = getDb();
|
|
73
|
-
const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
|
|
74
|
-
if (!message)
|
|
75
|
-
return errorResponse(`Message ${params.message_id} not found`, 'NOT_FOUND');
|
|
76
|
-
if (message.status === 'answered') {
|
|
77
|
-
return successResponse({
|
|
78
|
-
id: message.id, status: 'answered', response: message.response,
|
|
79
|
-
respondedBy: message.recipient_name,
|
|
80
|
-
question: message.question, answered_at: message.answered_at,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return successResponse({
|
|
84
|
-
id: message.id, status: 'pending', question: message.question,
|
|
85
|
-
recipient: message.recipient_name,
|
|
86
|
-
message: 'No response yet. Try again later or continue with other work.',
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
server.tool('send_to_agent', 'Send a message to another agent by name. Returns immediately. The recipient agent will receive the message in their terminal (if running in tmux).', {
|
|
90
|
-
recipient: z.string().describe('Name of the target agent (e.g. "backend", "task_flow:2")'),
|
|
91
|
-
message: z.string().describe('The message to send'),
|
|
92
|
-
context: z.string().optional().describe('Optional markdown context'),
|
|
93
|
-
}, { readOnlyHint: false }, async (params) => {
|
|
94
|
-
const db = getDb();
|
|
95
|
-
const senderName = ensureRegistered();
|
|
96
|
-
const recipient = getAgent(params.recipient);
|
|
97
|
-
if (!recipient)
|
|
98
|
-
return errorResponse(`Agent "${params.recipient}" not found`, 'NOT_FOUND');
|
|
99
|
-
const ts = now();
|
|
100
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, source, status, created_at)
|
|
101
|
-
VALUES (NULL, ?, ?, ?, ?, 'mcp', 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
|
|
102
|
-
const id = result.lastInsertRowid;
|
|
103
|
-
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
104
|
-
broadcastChange('agent_message', 'agent_question', msg);
|
|
105
|
-
return successResponse({ id, sender: senderName, recipient: params.recipient, status: 'pending' });
|
|
106
|
-
});
|
|
107
|
-
server.tool('ask_agent', 'Ask another agent a question and wait for their response. Like ask_user but targets an agent. Returns the message ID — use check_response to poll for the answer. The recipient agent receives the question in their terminal (if in tmux) and can respond with respond_to_message.', {
|
|
108
|
-
recipient: z.string().describe('Name of the target agent (e.g. "backend", "task_flow:2")'),
|
|
109
|
-
question: z.string().describe('The question to ask'),
|
|
110
|
-
context: z.string().optional().describe('Markdown context — background info, code snippets, proposals'),
|
|
111
|
-
choices: z.array(z.string()).optional().describe('Optional quick-tap choices, e.g. ["Yes", "No", "Skip"]'),
|
|
112
|
-
project_id: z.number().optional().describe('Optional project ID to attach the question to'),
|
|
113
|
-
}, { readOnlyHint: false }, async (params) => {
|
|
114
|
-
const db = getDb();
|
|
115
|
-
const senderName = ensureRegistered();
|
|
116
|
-
const recipient = getAgent(params.recipient);
|
|
117
|
-
if (!recipient)
|
|
118
|
-
return errorResponse(`Agent "${params.recipient}" not found or not connected`, 'NOT_FOUND');
|
|
119
|
-
const ts = now();
|
|
120
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, created_at)
|
|
121
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'mcp', 'pending', ?)`).run(params.project_id ?? null, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, params.recipient, process.ppid, ts);
|
|
122
|
-
const id = result.lastInsertRowid;
|
|
123
|
-
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
124
|
-
broadcastChange('agent_message', 'agent_question', msg);
|
|
125
|
-
logActivity('agent_question', `Asked ${params.recipient}: ${params.question}`, { entityType: 'agent_message', entityId: id });
|
|
126
|
-
return successResponse({
|
|
127
|
-
id,
|
|
128
|
-
status: 'pending',
|
|
129
|
-
sender: senderName,
|
|
130
|
-
recipient: params.recipient,
|
|
131
|
-
message: `Question sent to "${params.recipient}" (id: ${id}). Use check_response(${id}) to retrieve their answer.`,
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
server.tool('respond_to_message', 'Respond to a pending message addressed to this agent. Use check_messages to see incoming messages, then respond by message ID.', {
|
|
135
|
-
message_id: z.number().describe('The message ID to respond to (from check_messages)'),
|
|
136
|
-
response: z.string().describe('Your response text'),
|
|
137
|
-
}, { readOnlyHint: false }, async (params) => {
|
|
138
|
-
const db = getDb();
|
|
139
|
-
const name = ensureRegistered();
|
|
140
|
-
const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
|
|
141
|
-
if (!message)
|
|
142
|
-
return errorResponse(`Message ${params.message_id} not found`, 'NOT_FOUND');
|
|
143
|
-
if (message.recipient_name !== name)
|
|
144
|
-
return errorResponse(`Message ${params.message_id} is not addressed to you`, 'VALIDATION_ERROR');
|
|
145
|
-
if (message.status !== 'pending')
|
|
146
|
-
return errorResponse(`Message ${params.message_id} is already ${message.status}`, 'VALIDATION_ERROR');
|
|
147
|
-
const ts = now();
|
|
148
|
-
db.prepare('UPDATE agent_messages SET response = ?, status = ?, answered_at = ?, delivered = NULL WHERE id = ?')
|
|
149
|
-
.run(params.response, 'answered', ts, params.message_id);
|
|
150
|
-
const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
|
|
151
|
-
broadcastChange('agent_message', 'agent_question_answered', updated);
|
|
152
|
-
logActivity('agent_question_answered', `Responded to ${message.sender_name}: ${params.response.slice(0, 80)}`, { entityType: 'agent_message', entityId: params.message_id });
|
|
153
|
-
return successResponse({
|
|
154
|
-
id: params.message_id,
|
|
155
|
-
status: 'answered',
|
|
156
|
-
sender: message.sender_name,
|
|
157
|
-
message: `Response sent to "${message.sender_name}".`,
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
server.tool('check_messages', 'Check for incoming messages from users or other agents addressed to this agent.', {}, { readOnlyHint: true }, async () => {
|
|
161
|
-
const db = getDb();
|
|
162
|
-
const name = ensureRegistered();
|
|
163
|
-
const messages = db.prepare(`SELECT * FROM agent_messages WHERE recipient_name = ? AND status = 'pending' ORDER BY created_at ASC`).all(name);
|
|
164
|
-
return successResponse({
|
|
165
|
-
agent: name,
|
|
166
|
-
count: messages.length,
|
|
167
|
-
messages: messages.map(m => ({
|
|
168
|
-
id: m.id, sender: m.sender_name, question: m.question,
|
|
169
|
-
context: m.context, choices: m.choices ? JSON.parse(m.choices) : null,
|
|
170
|
-
created_at: m.created_at,
|
|
171
|
-
})),
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
server.tool('list_agents', 'List registered agents with their status, project path, and connection info.', {
|
|
175
|
-
status: z.enum(['connected', 'disconnected']).optional().describe('Filter by status. Omit for all agents.'),
|
|
176
|
-
}, { readOnlyHint: true }, async (params) => {
|
|
177
|
-
const agents = listAgents(params.status);
|
|
178
|
-
return successResponse(agents);
|
|
179
|
-
});
|
|
180
|
-
server.tool('broadcast_agents', 'Send a question to multiple agents simultaneously. Creates a group message — each agent gets their own copy linked by a shared broadcast ID. Use check_broadcast to see all responses. Optional: omit agents list to broadcast to ALL connected agents.', {
|
|
181
|
-
question: z.string().describe('The question to broadcast'),
|
|
182
|
-
agents: z.array(z.string()).optional().describe('Agent names to send to. If omitted, sends to all connected agents.'),
|
|
183
|
-
context: z.string().optional().describe('Markdown context shown before the question'),
|
|
184
|
-
choices: z.array(z.string()).optional().describe('Optional quick-tap choices'),
|
|
185
|
-
project_id: z.number().optional().describe('Optional project ID to attach the messages to'),
|
|
186
|
-
}, { readOnlyHint: false }, async (params) => {
|
|
187
|
-
const db = getDb();
|
|
188
|
-
const senderName = ensureRegistered();
|
|
189
|
-
// Resolve recipients
|
|
190
|
-
let recipients;
|
|
191
|
-
if (params.agents && params.agents.length > 0) {
|
|
192
|
-
for (const name of params.agents) {
|
|
193
|
-
const agent = getAgent(name);
|
|
194
|
-
if (!agent)
|
|
195
|
-
return errorResponse(`Agent "${name}" not found`, 'NOT_FOUND');
|
|
196
|
-
}
|
|
197
|
-
recipients = params.agents;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
const connected = listAgents('connected');
|
|
201
|
-
recipients = connected
|
|
202
|
-
.map((a) => a.name)
|
|
203
|
-
.filter((n) => n !== senderName);
|
|
204
|
-
if (recipients.length === 0)
|
|
205
|
-
return errorResponse('No other connected agents to broadcast to', 'VALIDATION_ERROR');
|
|
206
|
-
}
|
|
207
|
-
if (params.project_id) {
|
|
208
|
-
const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(params.project_id);
|
|
209
|
-
if (!project)
|
|
210
|
-
return errorResponse(`Project ${params.project_id} not found`, 'NOT_FOUND');
|
|
211
|
-
}
|
|
212
|
-
const broadcastId = crypto.randomUUID();
|
|
213
|
-
const ts = now();
|
|
214
|
-
const messageIds = [];
|
|
215
|
-
for (const recipient of recipients) {
|
|
216
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, broadcast_id, created_at)
|
|
217
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'mcp', 'pending', ?, ?)`).run(params.project_id ?? null, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, recipient, process.ppid, broadcastId, ts);
|
|
218
|
-
const id = result.lastInsertRowid;
|
|
219
|
-
messageIds.push(id);
|
|
220
|
-
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
221
|
-
broadcastChange('agent_message', 'agent_question', msg);
|
|
222
|
-
}
|
|
223
|
-
logActivity('agent_broadcast', `Broadcast to ${recipients.length} agents: ${params.question.slice(0, 80)}`, { entityType: 'agent_message' });
|
|
224
|
-
return successResponse({
|
|
225
|
-
broadcastId,
|
|
226
|
-
recipients,
|
|
227
|
-
messageIds,
|
|
228
|
-
count: recipients.length,
|
|
229
|
-
message: `Broadcast sent to ${recipients.length} agents. Use check_broadcast("${broadcastId}") to see responses.`,
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
server.tool('check_broadcast', 'Check the status of a broadcast message — shows which agents have responded and their answers.', {
|
|
233
|
-
broadcast_id: z.string().describe('The broadcast ID returned by broadcast_agents'),
|
|
234
|
-
}, { readOnlyHint: true }, async (params) => {
|
|
235
|
-
const db = getDb();
|
|
236
|
-
const messages = db.prepare('SELECT * FROM agent_messages WHERE broadcast_id = ? ORDER BY created_at ASC').all(params.broadcast_id);
|
|
237
|
-
if (messages.length === 0)
|
|
238
|
-
return errorResponse(`No messages found for broadcast ${params.broadcast_id}`, 'NOT_FOUND');
|
|
239
|
-
const total = messages.length;
|
|
240
|
-
const answered = messages.filter(m => m.status === 'answered').length;
|
|
241
|
-
const pending = messages.filter(m => m.status === 'pending').length;
|
|
242
|
-
return successResponse({
|
|
243
|
-
broadcastId: params.broadcast_id,
|
|
244
|
-
question: messages[0].question,
|
|
245
|
-
total,
|
|
246
|
-
answered,
|
|
247
|
-
pending,
|
|
248
|
-
responses: messages.map(m => ({
|
|
249
|
-
id: m.id,
|
|
250
|
-
recipient: m.recipient_name,
|
|
251
|
-
status: m.status,
|
|
252
|
-
response: m.response ?? null,
|
|
253
|
-
answered_at: m.answered_at ?? null,
|
|
254
|
-
})),
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
}
|
package/dist/tools/agent.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
export declare function getAgentInstructions(): Promise<{
|
|
3
|
-
content: {
|
|
4
|
-
type: "text";
|
|
5
|
-
text: string;
|
|
6
|
-
}[];
|
|
7
|
-
}>;
|
|
8
|
-
export declare function clearData(): Promise<{
|
|
9
|
-
content: {
|
|
10
|
-
type: "text";
|
|
11
|
-
text: string;
|
|
12
|
-
}[];
|
|
13
|
-
}>;
|
|
14
|
-
export declare function registerAgentTools(server: McpServer): void;
|