@dalmasonto/taskflow-mcp 1.0.8 → 1.0.10

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.
@@ -47,14 +47,19 @@ export function registerAgent(options) {
47
47
  const agentPid = process.ppid;
48
48
  const projectPath = process.cwd();
49
49
  const folderName = projectPath.split('/').pop() || 'unknown';
50
- // Check if this PID already has a registration — reuse it
50
+ // Check if this PID already has a registration — reuse it (or rename if customName provided)
51
51
  const existingByPid = db.prepare('SELECT * FROM agent_registry WHERE pid = ? AND status = ?').get(agentPid, 'connected');
52
52
  if (existingByPid) {
53
- // Update tmux pane in case it changed, but keep the same name
54
53
  const tmuxPane = detectTmuxPane(agentPid);
55
54
  if (tmuxPane !== existingByPid.tmux_pane) {
56
55
  db.prepare('UPDATE agent_registry SET tmux_pane = ? WHERE id = ?').run(tmuxPane, existingByPid.id);
57
56
  }
57
+ // Allow renaming via customName
58
+ if (options?.customName && options.customName !== existingByPid.name) {
59
+ db.prepare('UPDATE agent_registry SET name = ? WHERE id = ?').run(options.customName, existingByPid.id);
60
+ broadcast('agent_connected', { entity: 'agent', action: 'agent_connected', payload: { ...existingByPid, name: options.customName, tmux_pane: tmuxPane ?? existingByPid.tmux_pane } });
61
+ return options.customName;
62
+ }
58
63
  return existingByPid.name;
59
64
  }
60
65
  // Clean up dead agents first to free up names
package/dist/sse.js CHANGED
@@ -287,7 +287,7 @@ export async function startSSEServer() {
287
287
  return;
288
288
  }
289
289
  const ts = new Date().toISOString();
290
- db.prepare('UPDATE agent_messages SET response = ?, status = ?, answered_at = ? WHERE id = ?')
290
+ db.prepare('UPDATE agent_messages SET response = ?, status = ?, answered_at = ?, delivered = NULL WHERE id = ?')
291
291
  .run(response, 'answered', ts, id);
292
292
  const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
293
293
  broadcast('agent_question_answered', { entity: 'agent_message', action: 'agent_question_answered', payload: updated });
@@ -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,14 +69,13 @@ 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;
91
75
  const id = payload.id;
92
76
  const delivered = payload.delivered;
93
77
  const agentPidField = payload.agent_pid;
94
- const isOurs = (sender === agentName || agentPidField === agentPid) && recipient === 'user';
78
+ const isOurs = sender === agentName || agentPidField === agentPid;
95
79
  if (!isOurs || status !== 'answered' || delivered === 1)
96
80
  return;
97
81
  const question = (payload.question || '').slice(0, 60);
@@ -105,6 +89,8 @@ function injectAndMarkDelivered(id, text, tmuxPane) {
105
89
  db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
106
90
  try {
107
91
  execSync(`tmux send-keys -t ${tmuxPane} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
92
+ // Long text triggers tmux bracketed paste — delay then send extra Enter to confirm
93
+ execSync(`sleep 1 && tmux send-keys -t ${tmuxPane} Enter`, { stdio: 'ignore', timeout: 5000 });
108
94
  console.error(`[bridge] delivered message ${id} to tmux pane ${tmuxPane}`);
109
95
  }
110
96
  catch (err) {
@@ -116,8 +102,8 @@ function deliverUndelivered(options) {
116
102
  const db = getDb();
117
103
  const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
118
104
  (recipient_name = ? AND status = 'pending') OR
119
- (sender_name = ? AND recipient_name = 'user' AND status = 'answered') OR
120
- (agent_pid = ? AND recipient_name = 'user' AND status = 'answered')
105
+ (sender_name = ? AND status = 'answered') OR
106
+ (agent_pid = ? AND status = 'answered')
121
107
  )`).all(agentName, agentName, agentPid);
122
108
  for (const msg of incoming) {
123
109
  let text;
@@ -136,145 +122,15 @@ function deliverUndelivered(options) {
136
122
  injectAndMarkDelivered(msg.id, text, tmuxPane);
137
123
  }
138
124
  }
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
125
  // ─── Public API ──────────────────────────────────────────────────────
269
126
  /**
270
- * Start the tmux bridge: SSE listener for instant delivery + capture for terminal→chat.
127
+ * Start the tmux bridge: SSE listener for instant message delivery.
271
128
  * Returns a cleanup function for graceful shutdown.
272
129
  */
273
130
  export function startTmuxBridge(options) {
274
131
  startSSEListener(options);
275
- const stopCapture = startCapture(options);
276
132
  console.error(`[bridge] tmux bridge active for agent "${options.agentName}" on pane ${options.tmuxPane}`);
277
133
  return () => {
278
- stopCapture();
134
+ // SSE connection will close when process exits
279
135
  };
280
136
  }
@@ -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 = ?, delivered = NULL 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.10",
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",