@dalmasonto/taskflow-mcp 1.0.8 → 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.
@@ -4,7 +4,7 @@ interface BridgeOptions {
4
4
  tmuxPane: string;
5
5
  }
6
6
  /**
7
- * Start the tmux bridge: SSE listener for instant delivery + capture for terminal→chat.
7
+ * Start the tmux bridge: SSE listener for instant message delivery.
8
8
  * Returns a cleanup function for graceful shutdown.
9
9
  */
10
10
  export declare function startTmuxBridge(options: BridgeOptions): () => void;
@@ -1,21 +1,7 @@
1
1
  import { execSync } from 'child_process';
2
- import { createReadStream, writeFileSync, unlinkSync, watchFile, unwatchFile, existsSync } from 'fs';
3
2
  import { getDb } from './db.js';
4
3
  import { getActivePort } from './sse.js';
5
4
  import http from 'http';
6
- // ─── UI-injected message prefixes (skip these in capture) ────────────
7
- const INJECTED_PREFIXES = [
8
- '[Message from ',
9
- '[Inbox Response]',
10
- ];
11
- function isInjectedLine(line) {
12
- return INJECTED_PREFIXES.some(prefix => line.startsWith(prefix));
13
- }
14
- // ─── Strip ANSI escape codes ─────────────────────────────────────────
15
- const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
16
- function stripAnsi(text) {
17
- return text.replace(ANSI_RE, '');
18
- }
19
5
  // ─── SSE Listener (replaces the 3s poller) ───────────────────────────
20
6
  function startSSEListener(options) {
21
7
  const { agentName, agentPid, tmuxPane } = options;
@@ -66,7 +52,6 @@ function handleSSEEvent(event, data, options) {
66
52
  if (!payload)
67
53
  return;
68
54
  if (event === 'agent_question') {
69
- // Incoming message addressed to this agent
70
55
  const recipient = payload.recipient_name;
71
56
  const sender = payload.sender_name;
72
57
  const status = payload.status;
@@ -84,7 +69,6 @@ function handleSSEEvent(event, data, options) {
84
69
  injectAndMarkDelivered(id, text, tmuxPane);
85
70
  }
86
71
  if (event === 'agent_question_answered') {
87
- // A question this agent sent got answered
88
72
  const sender = payload.sender_name;
89
73
  const recipient = payload.recipient_name;
90
74
  const status = payload.status;
@@ -136,145 +120,15 @@ function deliverUndelivered(options) {
136
120
  injectAndMarkDelivered(msg.id, text, tmuxPane);
137
121
  }
138
122
  }
139
- // ─── Tmux Capture (terminal output → chat) ───────────────────────────
140
- const MAX_MESSAGE_LENGTH = 10000;
141
- const FLUSH_DELAY_MS = 2000;
142
- function startCapture(options) {
143
- const { agentName, tmuxPane } = options;
144
- const tmpFile = `/tmp/taskflow-capture-${agentName}.pipe`;
145
- const port = getActivePort();
146
- // Truncate/create the temp file
147
- writeFileSync(tmpFile, '');
148
- // Start tmux pipe-pane
149
- try {
150
- execSync(`tmux pipe-pane -t ${tmuxPane} -o "cat >> ${tmpFile}"`, { stdio: 'ignore' });
151
- console.error(`[capture] started pipe-pane for ${tmuxPane} → ${tmpFile}`);
152
- }
153
- catch (err) {
154
- console.error('[capture] failed to start pipe-pane:', err);
155
- return () => { };
156
- }
157
- let buffer = '';
158
- let flushTimer = null;
159
- let readPosition = 0;
160
- function flushBuffer() {
161
- flushTimer = null;
162
- if (!buffer.trim()) {
163
- buffer = '';
164
- return;
165
- }
166
- // Split into chunks if needed
167
- const chunks = [];
168
- let remaining = buffer;
169
- while (remaining.length > MAX_MESSAGE_LENGTH) {
170
- chunks.push(remaining.slice(0, MAX_MESSAGE_LENGTH));
171
- remaining = remaining.slice(MAX_MESSAGE_LENGTH);
172
- }
173
- if (remaining.trim())
174
- chunks.push(remaining);
175
- buffer = '';
176
- for (const chunk of chunks) {
177
- postToChat(agentName, chunk, port);
178
- }
179
- }
180
- function processNewData() {
181
- if (!existsSync(tmpFile))
182
- return;
183
- const stream = createReadStream(tmpFile, {
184
- start: readPosition,
185
- encoding: 'utf-8',
186
- });
187
- let newData = '';
188
- stream.on('data', (chunk) => { newData += chunk.toString(); });
189
- stream.on('end', () => {
190
- if (!newData)
191
- return;
192
- readPosition += Buffer.byteLength(newData);
193
- const cleaned = stripAnsi(newData);
194
- const lines = cleaned.split('\n');
195
- const filtered = lines.filter(line => !isInjectedLine(line.trim()));
196
- const text = filtered.join('\n');
197
- if (text.trim()) {
198
- buffer += text;
199
- // Reset the flush timer on each new data
200
- if (flushTimer)
201
- clearTimeout(flushTimer);
202
- flushTimer = setTimeout(flushBuffer, FLUSH_DELAY_MS);
203
- }
204
- });
205
- stream.on('error', () => { });
206
- }
207
- // Watch for file changes
208
- watchFile(tmpFile, { interval: 500 }, () => {
209
- processNewData();
210
- });
211
- // Cleanup function
212
- return () => {
213
- if (flushTimer)
214
- clearTimeout(flushTimer);
215
- // Flush any remaining buffer
216
- if (buffer.trim()) {
217
- const chunks = [];
218
- let remaining = buffer;
219
- while (remaining.length > MAX_MESSAGE_LENGTH) {
220
- chunks.push(remaining.slice(0, MAX_MESSAGE_LENGTH));
221
- remaining = remaining.slice(MAX_MESSAGE_LENGTH);
222
- }
223
- if (remaining.trim())
224
- chunks.push(remaining);
225
- buffer = '';
226
- for (const chunk of chunks) {
227
- postToChat(agentName, chunk, port);
228
- }
229
- }
230
- unwatchFile(tmpFile);
231
- try {
232
- execSync(`tmux pipe-pane -t ${tmuxPane}`, { stdio: 'ignore' });
233
- }
234
- catch { }
235
- try {
236
- unlinkSync(tmpFile);
237
- }
238
- catch { }
239
- console.error(`[capture] stopped for ${tmuxPane}`);
240
- };
241
- }
242
- function postToChat(agentName, text, port) {
243
- const body = JSON.stringify({
244
- recipient: 'user',
245
- message: text,
246
- source: 'terminal',
247
- senderName: agentName,
248
- });
249
- const req = http.request({
250
- hostname: 'localhost',
251
- port,
252
- path: '/api/agent-messages/send',
253
- method: 'POST',
254
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
255
- }, (res) => {
256
- // Drain the response
257
- res.resume();
258
- if (res.statusCode !== 200) {
259
- console.error(`[capture] POST failed with status ${res.statusCode}`);
260
- }
261
- });
262
- req.on('error', (err) => {
263
- console.error('[capture] POST error:', err.message);
264
- });
265
- req.write(body);
266
- req.end();
267
- }
268
123
  // ─── Public API ──────────────────────────────────────────────────────
269
124
  /**
270
- * Start the tmux bridge: SSE listener for instant delivery + capture for terminal→chat.
125
+ * Start the tmux bridge: SSE listener for instant message delivery.
271
126
  * Returns a cleanup function for graceful shutdown.
272
127
  */
273
128
  export function startTmuxBridge(options) {
274
129
  startSSEListener(options);
275
- const stopCapture = startCapture(options);
276
130
  console.error(`[bridge] tmux bridge active for agent "${options.agentName}" on pane ${options.tmuxPane}`);
277
131
  return () => {
278
- stopCapture();
132
+ // SSE connection will close when process exits
279
133
  };
280
134
  }
@@ -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).', {
@@ -80,6 +82,59 @@ export function registerAgentInboxTools(server) {
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.8",
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",