@dalmasonto/taskflow-mcp 1.0.7 → 1.0.9

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/dist/db.js CHANGED
@@ -103,6 +103,7 @@ function initSchema(db) {
103
103
  sender_name TEXT NOT NULL DEFAULT 'unknown',
104
104
  recipient_name TEXT NOT NULL DEFAULT 'user',
105
105
  status TEXT NOT NULL DEFAULT 'pending',
106
+ source TEXT NOT NULL DEFAULT 'mcp',
106
107
  created_at TEXT NOT NULL,
107
108
  answered_at TEXT
108
109
  );
@@ -146,6 +147,9 @@ function initSchema(db) {
146
147
  if (!colNames.has('recipient_name')) {
147
148
  db.exec("ALTER TABLE agent_messages ADD COLUMN recipient_name TEXT NOT NULL DEFAULT 'user'");
148
149
  }
150
+ if (!colNames.has('source')) {
151
+ db.exec("ALTER TABLE agent_messages ADD COLUMN source TEXT NOT NULL DEFAULT 'mcp'");
152
+ }
149
153
  // Migration: make agent_messages.project_id nullable if it was NOT NULL
150
154
  try {
151
155
  const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'agent_messages'").get();
@@ -163,13 +167,14 @@ function initSchema(db) {
163
167
  sender_name TEXT NOT NULL DEFAULT 'unknown',
164
168
  recipient_name TEXT NOT NULL DEFAULT 'user',
165
169
  status TEXT NOT NULL DEFAULT 'pending',
170
+ source TEXT NOT NULL DEFAULT 'mcp',
166
171
  created_at TEXT NOT NULL,
167
172
  answered_at TEXT
168
173
  );
169
174
  INSERT INTO agent_messages_new SELECT
170
175
  id, project_id, question, context, choices, response, agent_pid, delivered,
171
176
  COALESCE(sender_name, 'unknown'), COALESCE(recipient_name, 'user'),
172
- status, created_at, answered_at
177
+ status, COALESCE(source, 'mcp'), created_at, answered_at
173
178
  FROM agent_messages;
174
179
  DROP TABLE agent_messages;
175
180
  ALTER TABLE agent_messages_new RENAME TO agent_messages;
package/dist/index.js CHANGED
@@ -72,17 +72,15 @@ if (!httpOnly) {
72
72
  const { registerAgent, unregisterAgent } = await import('./agent-registry.js');
73
73
  // Auto-register this agent
74
74
  const agentName = registerAgent();
75
+ const agentPid = process.ppid;
75
76
  console.error(`[agent] registered as "${agentName}"`);
76
- // Graceful shutdown mark agent as disconnected
77
- const cleanup = () => { try {
77
+ let cleanup = () => { try {
78
78
  unregisterAgent(agentName);
79
79
  }
80
80
  catch { } process.exit(0); };
81
81
  process.on('SIGINT', cleanup);
82
82
  process.on('SIGTERM', cleanup);
83
- // Background poller: deliver messages to this agent's terminal via tmux
84
- const POLL_INTERVAL = 3000;
85
- const agentPid = process.ppid;
83
+ // Tmux bridge: SSE listener for instant delivery + capture for terminal→chat
86
84
  let tmuxTarget = null;
87
85
  try {
88
86
  const { execSync: exec } = await import('child_process');
@@ -95,53 +93,25 @@ if (!httpOnly) {
95
93
  break;
96
94
  }
97
95
  }
98
- if (tmuxTarget)
99
- console.error(`[inject] tmux pane ${tmuxTarget} for agent "${agentName}"`);
100
- else
101
- console.error('[inject] agent not in tmux — terminal injection disabled');
102
96
  }
103
97
  catch {
104
- console.error('[inject] tmux not available');
98
+ console.error('[bridge] tmux not available');
105
99
  }
106
100
  if (tmuxTarget) {
107
- const { execSync: exec } = await import('child_process');
108
- const target = tmuxTarget;
109
- setInterval(() => {
110
- try {
111
- const db = getDb();
112
- // Check for messages addressed to this agent (from user or other agents)
113
- // AND for answered questions this agent sent (inbox responses)
114
- // Check by agent name AND by agent_pid (backward compat with old messages)
115
- const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
116
- (recipient_name = ? AND status = 'pending') OR
117
- (sender_name = ? AND recipient_name = 'user' AND status = 'answered') OR
118
- (agent_pid = ? AND recipient_name = 'user' AND status = 'answered')
119
- )`).all(agentName, agentName, agentPid);
120
- for (const msg of incoming) {
121
- db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(msg.id);
122
- let text;
123
- if (msg.recipient_name === agentName && msg.sender_name === 'user') {
124
- text = `[Message from User]: ${msg.question}`;
125
- }
126
- else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
127
- text = `[Message from ${msg.sender_name}]: ${msg.question}`;
128
- }
129
- else if (msg.status === 'answered' && msg.response) {
130
- text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
131
- }
132
- else {
133
- continue;
134
- }
135
- try {
136
- exec(`tmux send-keys -t ${target} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
137
- console.error(`[inject] delivered message ${msg.id} to tmux pane ${target}`);
138
- }
139
- catch (err) {
140
- console.error(`[inject] tmux send-keys failed for message ${msg.id}:`, err);
141
- }
142
- }
143
- }
144
- catch { /* ignore */ }
145
- }, POLL_INTERVAL);
101
+ const { startTmuxBridge } = await import('./tmux-bridge.js');
102
+ const stopBridge = startTmuxBridge({
103
+ agentName,
104
+ agentPid,
105
+ tmuxPane: tmuxTarget,
106
+ });
107
+ const originalCleanup = cleanup;
108
+ cleanup = () => { stopBridge(); originalCleanup(); };
109
+ process.removeListener('SIGINT', originalCleanup);
110
+ process.removeListener('SIGTERM', originalCleanup);
111
+ process.on('SIGINT', cleanup);
112
+ process.on('SIGTERM', cleanup);
113
+ }
114
+ else {
115
+ console.error('[bridge] agent not in tmux bridge disabled');
146
116
  }
147
117
  }
package/dist/sse.js CHANGED
@@ -317,18 +317,20 @@ export async function startSSEServer() {
317
317
  jsonResponse(res, 200, updated);
318
318
  return;
319
319
  }
320
- // POST /api/agent-messages/send — user sends a message to an agent
320
+ // POST /api/agent-messages/send — send a message (from user UI or capture system)
321
321
  if (req.url === '/api/agent-messages/send' && req.method === 'POST') {
322
322
  const db = getDb();
323
323
  const body = JSON.parse(await readBody(req));
324
- const { recipient, message: msgText, projectId } = body;
324
+ const { recipient, message: msgText, projectId, source: msgSource, senderName } = body;
325
325
  if (!recipient || !msgText) {
326
326
  jsonResponse(res, 400, { error: 'recipient and message are required' });
327
327
  return;
328
328
  }
329
+ const source = msgSource || 'ui';
330
+ const sender = senderName || 'user';
329
331
  const ts = new Date().toISOString();
330
- const result = db.prepare(`INSERT INTO agent_messages (project_id, question, sender_name, recipient_name, status, created_at)
331
- VALUES (?, ?, 'user', ?, 'pending', ?)`).run(projectId ?? null, msgText, recipient, ts);
332
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, sender_name, recipient_name, source, status, created_at)
333
+ VALUES (?, ?, ?, ?, ?, 'pending', ?)`).run(projectId ?? null, msgText, sender, recipient, source, ts);
332
334
  const id = result.lastInsertRowid;
333
335
  const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
334
336
  broadcast('agent_question', { entity: 'agent_message', action: 'agent_question', payload: msg });
@@ -0,0 +1,11 @@
1
+ interface BridgeOptions {
2
+ agentName: string;
3
+ agentPid: number;
4
+ tmuxPane: string;
5
+ }
6
+ /**
7
+ * Start the tmux bridge: SSE listener for instant message delivery.
8
+ * Returns a cleanup function for graceful shutdown.
9
+ */
10
+ export declare function startTmuxBridge(options: BridgeOptions): () => void;
11
+ export {};
@@ -0,0 +1,134 @@
1
+ import { execSync } 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 { agentName, agentPid, tmuxPane } = options;
8
+ const port = getActivePort();
9
+ function connect() {
10
+ const req = http.get(`http://localhost:${port}/events`, (res) => {
11
+ let buffer = '';
12
+ res.on('data', (chunk) => {
13
+ buffer += chunk.toString();
14
+ const lines = buffer.split('\n');
15
+ buffer = lines.pop() || '';
16
+ let eventType = '';
17
+ for (const line of lines) {
18
+ if (line.startsWith('event: ')) {
19
+ eventType = line.slice(7).trim();
20
+ }
21
+ else if (line.startsWith('data: ') && eventType) {
22
+ try {
23
+ const data = JSON.parse(line.slice(6));
24
+ handleSSEEvent(eventType, data, options);
25
+ }
26
+ catch { /* malformed JSON */ }
27
+ eventType = '';
28
+ }
29
+ }
30
+ });
31
+ res.on('end', () => {
32
+ console.error('[bridge] SSE connection closed, reconnecting in 3s...');
33
+ setTimeout(connect, 3000);
34
+ });
35
+ res.on('error', () => {
36
+ console.error('[bridge] SSE connection error, reconnecting in 3s...');
37
+ setTimeout(connect, 3000);
38
+ });
39
+ });
40
+ req.on('error', () => {
41
+ console.error('[bridge] SSE connect failed, retrying in 3s...');
42
+ setTimeout(connect, 3000);
43
+ });
44
+ }
45
+ // Initial sweep: deliver any undelivered messages from before SSE connected
46
+ deliverUndelivered(options);
47
+ connect();
48
+ }
49
+ function handleSSEEvent(event, data, options) {
50
+ const { agentName, agentPid, tmuxPane } = options;
51
+ const payload = data.payload;
52
+ if (!payload)
53
+ return;
54
+ if (event === 'agent_question') {
55
+ const recipient = payload.recipient_name;
56
+ const sender = payload.sender_name;
57
+ const status = payload.status;
58
+ const id = payload.id;
59
+ const delivered = payload.delivered;
60
+ if (recipient !== agentName || status !== 'pending' || delivered === 1)
61
+ return;
62
+ let text;
63
+ if (sender === 'user') {
64
+ text = `[Message from User]: ${payload.question}`;
65
+ }
66
+ else {
67
+ text = `[Message from ${sender}]: ${payload.question}`;
68
+ }
69
+ injectAndMarkDelivered(id, text, tmuxPane);
70
+ }
71
+ if (event === 'agent_question_answered') {
72
+ const sender = payload.sender_name;
73
+ const recipient = payload.recipient_name;
74
+ const status = payload.status;
75
+ const id = payload.id;
76
+ const delivered = payload.delivered;
77
+ const agentPidField = payload.agent_pid;
78
+ const isOurs = (sender === agentName || agentPidField === agentPid) && recipient === 'user';
79
+ if (!isOurs || status !== 'answered' || delivered === 1)
80
+ return;
81
+ const question = (payload.question || '').slice(0, 60);
82
+ const response = payload.response;
83
+ const text = `[Inbox Response] to "${question}": ${response}`;
84
+ injectAndMarkDelivered(id, text, tmuxPane);
85
+ }
86
+ }
87
+ function injectAndMarkDelivered(id, text, tmuxPane) {
88
+ const db = getDb();
89
+ db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
90
+ try {
91
+ execSync(`tmux send-keys -t ${tmuxPane} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
92
+ console.error(`[bridge] delivered message ${id} to tmux pane ${tmuxPane}`);
93
+ }
94
+ catch (err) {
95
+ console.error(`[bridge] tmux send-keys failed for message ${id}:`, err);
96
+ }
97
+ }
98
+ function deliverUndelivered(options) {
99
+ const { agentName, agentPid, tmuxPane } = options;
100
+ const db = getDb();
101
+ const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
102
+ (recipient_name = ? AND status = 'pending') OR
103
+ (sender_name = ? AND recipient_name = 'user' AND status = 'answered') OR
104
+ (agent_pid = ? AND recipient_name = 'user' AND status = 'answered')
105
+ )`).all(agentName, agentName, agentPid);
106
+ for (const msg of incoming) {
107
+ let text;
108
+ if (msg.recipient_name === agentName && msg.sender_name === 'user') {
109
+ text = `[Message from User]: ${msg.question}`;
110
+ }
111
+ else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
112
+ text = `[Message from ${msg.sender_name}]: ${msg.question}`;
113
+ }
114
+ else if (msg.status === 'answered' && msg.response) {
115
+ text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
116
+ }
117
+ else {
118
+ continue;
119
+ }
120
+ injectAndMarkDelivered(msg.id, text, tmuxPane);
121
+ }
122
+ }
123
+ // ─── Public API ──────────────────────────────────────────────────────
124
+ /**
125
+ * Start the tmux bridge: SSE listener for instant message delivery.
126
+ * Returns a cleanup function for graceful shutdown.
127
+ */
128
+ export function startTmuxBridge(options) {
129
+ startSSEListener(options);
130
+ console.error(`[bridge] tmux bridge active for agent "${options.agentName}" on pane ${options.tmuxPane}`);
131
+ return () => {
132
+ // SSE connection will close when process exits
133
+ };
134
+ }
@@ -31,8 +31,8 @@ export function registerAgentInboxTools(server) {
31
31
  return errorResponse(`Project ${params.project_id} not found`, 'NOT_FOUND');
32
32
  const senderName = ensureRegistered();
33
33
  const ts = now();
34
- const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, status, created_at)
35
- VALUES (?, ?, ?, ?, ?, 'user', ?, 'pending', ?)`).run(params.project_id, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, process.ppid, ts);
34
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, created_at)
35
+ VALUES (?, ?, ?, ?, ?, 'user', ?, 'mcp', 'pending', ?)`).run(params.project_id, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, process.ppid, ts);
36
36
  const id = result.lastInsertRowid;
37
37
  const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
38
38
  broadcastChange('agent_message', 'agent_question', message);
@@ -44,8 +44,8 @@ export function registerAgentInboxTools(server) {
44
44
  message: `Question posted to Agent Inbox (id: ${id}). Use check_response(${id}) to retrieve the user's answer.`,
45
45
  });
46
46
  });
47
- server.tool('check_response', 'Check if the user has responded to a previously posted agent question. Returns the response if answered, or status "pending" if still waiting.', {
48
- message_id: z.number().describe('The agent message ID returned by ask_user'),
47
+ 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.', {
48
+ message_id: z.number().describe('The message ID returned by ask_user or ask_agent'),
49
49
  }, async (params) => {
50
50
  const db = getDb();
51
51
  const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
@@ -54,12 +54,14 @@ export function registerAgentInboxTools(server) {
54
54
  if (message.status === 'answered') {
55
55
  return successResponse({
56
56
  id: message.id, status: 'answered', response: message.response,
57
+ respondedBy: message.recipient_name,
57
58
  question: message.question, answered_at: message.answered_at,
58
59
  });
59
60
  }
60
61
  return successResponse({
61
62
  id: message.id, status: 'pending', question: message.question,
62
- message: 'User has not responded yet. Try again later or continue with other work.',
63
+ recipient: message.recipient_name,
64
+ message: 'No response yet. Try again later or continue with other work.',
63
65
  });
64
66
  });
65
67
  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).', {
@@ -73,13 +75,66 @@ export function registerAgentInboxTools(server) {
73
75
  if (!recipient)
74
76
  return errorResponse(`Agent "${params.recipient}" not found`, 'NOT_FOUND');
75
77
  const ts = now();
76
- const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, status, created_at)
77
- VALUES (NULL, ?, ?, ?, ?, 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
78
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, source, status, created_at)
79
+ VALUES (NULL, ?, ?, ?, ?, 'mcp', 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
78
80
  const id = result.lastInsertRowid;
79
81
  const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
80
82
  broadcastChange('agent_message', 'agent_question', msg);
81
83
  return successResponse({ id, sender: senderName, recipient: params.recipient, status: 'pending' });
82
84
  });
85
+ 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.', {
86
+ recipient: z.string().describe('Name of the target agent (e.g. "backend", "task_flow:2")'),
87
+ question: z.string().describe('The question to ask'),
88
+ context: z.string().optional().describe('Markdown context — background info, code snippets, proposals'),
89
+ choices: z.array(z.string()).optional().describe('Optional quick-tap choices, e.g. ["Yes", "No", "Skip"]'),
90
+ project_id: z.number().optional().describe('Optional project ID to attach the question to'),
91
+ }, async (params) => {
92
+ const db = getDb();
93
+ const senderName = ensureRegistered();
94
+ const recipient = getAgent(params.recipient);
95
+ if (!recipient)
96
+ return errorResponse(`Agent "${params.recipient}" not found or not connected`, 'NOT_FOUND');
97
+ const ts = now();
98
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, created_at)
99
+ 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);
100
+ const id = result.lastInsertRowid;
101
+ const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
102
+ broadcastChange('agent_message', 'agent_question', msg);
103
+ logActivity('agent_question', `Asked ${params.recipient}: ${params.question}`, { entityType: 'agent_message', entityId: id });
104
+ return successResponse({
105
+ id,
106
+ status: 'pending',
107
+ sender: senderName,
108
+ recipient: params.recipient,
109
+ message: `Question sent to "${params.recipient}" (id: ${id}). Use check_response(${id}) to retrieve their answer.`,
110
+ });
111
+ });
112
+ 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.', {
113
+ message_id: z.number().describe('The message ID to respond to (from check_messages)'),
114
+ response: z.string().describe('Your response text'),
115
+ }, async (params) => {
116
+ const db = getDb();
117
+ const name = ensureRegistered();
118
+ const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
119
+ if (!message)
120
+ return errorResponse(`Message ${params.message_id} not found`, 'NOT_FOUND');
121
+ if (message.recipient_name !== name)
122
+ return errorResponse(`Message ${params.message_id} is not addressed to you`, 'VALIDATION_ERROR');
123
+ if (message.status !== 'pending')
124
+ return errorResponse(`Message ${params.message_id} is already ${message.status}`, 'VALIDATION_ERROR');
125
+ const ts = now();
126
+ db.prepare('UPDATE agent_messages SET response = ?, status = ?, answered_at = ? WHERE id = ?')
127
+ .run(params.response, 'answered', ts, params.message_id);
128
+ const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
129
+ broadcastChange('agent_message', 'agent_question_answered', updated);
130
+ logActivity('agent_question_answered', `Responded to ${message.sender_name}: ${params.response.slice(0, 80)}`, { entityType: 'agent_message', entityId: params.message_id });
131
+ return successResponse({
132
+ id: params.message_id,
133
+ status: 'answered',
134
+ sender: message.sender_name,
135
+ message: `Response sent to "${message.sender_name}".`,
136
+ });
137
+ });
83
138
  server.tool('check_messages', 'Check for incoming messages from users or other agents addressed to this agent.', {}, async () => {
84
139
  const db = getDb();
85
140
  const name = ensureRegistered();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "MCP server for TaskFlow — manage projects, tasks, timers, analytics via AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",