@dalmasonto/taskflow-mcp 1.0.19 → 1.0.21

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.
Files changed (2) hide show
  1. package/dist/sse.js +113 -18
  2. package/package.json +1 -1
package/dist/sse.js CHANGED
@@ -142,6 +142,78 @@ function pollAgentTerminals(promptState, contentHashes) {
142
142
  }
143
143
  }
144
144
  }
145
+ // ─── Relay command executor ─────────────────────────────────────────
146
+ // Executes commands received from the relay's command queue locally.
147
+ async function executeRelayCommand(type, payload) {
148
+ const db = getDb();
149
+ try {
150
+ switch (type) {
151
+ case 'send-keys': {
152
+ const { agentName, keys, enter = true, literal = true } = payload;
153
+ const { validateKeys } = await import('./tools/terminal.js');
154
+ const violation = validateKeys(keys);
155
+ if (violation)
156
+ return { status: 'error', result: violation };
157
+ const agent = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(agentName);
158
+ if (!agent?.tmux_pane)
159
+ return { status: 'error', result: `Agent "${agentName}" not found or has no pane` };
160
+ if (agent.status !== 'connected')
161
+ return { status: 'error', result: `Agent "${agentName}" is disconnected` };
162
+ const pane = agent.tmux_pane;
163
+ if (literal) {
164
+ execFileSync('tmux', ['send-keys', '-t', pane, '-l', keys], { stdio: 'ignore', timeout: 5000 });
165
+ if (enter)
166
+ execFileSync('tmux', ['send-keys', '-t', pane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
167
+ }
168
+ else {
169
+ const args = ['send-keys', '-t', pane, keys];
170
+ if (enter)
171
+ args.push('Enter');
172
+ execFileSync('tmux', args, { stdio: 'ignore', timeout: 5000 });
173
+ }
174
+ logActivity('terminal_send_keys', `[relay] Sent keys to ${agentName}: ${keys.slice(0, 50)}`, { entityType: 'agent' });
175
+ return { status: 'done', result: { agent: agentName, keys, sent: true } };
176
+ }
177
+ case 'respond-message': {
178
+ const { messageId, response } = payload;
179
+ const ts = new Date().toISOString();
180
+ const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(messageId);
181
+ if (!msg)
182
+ return { status: 'error', result: `Message ${messageId} not found` };
183
+ if (msg.status !== 'pending')
184
+ return { status: 'error', result: `Message ${messageId} is already ${msg.status}` };
185
+ db.prepare('UPDATE agent_messages SET response = ?, status = ?, answered_at = ? WHERE id = ?')
186
+ .run(response, 'answered', ts, messageId);
187
+ const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(messageId);
188
+ broadcast('agent_question_answered', { entity: 'agent_message', action: 'agent_question_answered', payload: updated });
189
+ return { status: 'done', result: { id: messageId, status: 'answered' } };
190
+ }
191
+ case 'dismiss-message': {
192
+ const { messageId } = payload;
193
+ const ts = new Date().toISOString();
194
+ db.prepare("UPDATE agent_messages SET status = 'dismissed', answered_at = ? WHERE id = ?").run(ts, payload.messageId);
195
+ const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(messageId);
196
+ broadcast('agent_question_answered', { entity: 'agent_message', action: 'agent_question_answered', payload: updated });
197
+ return { status: 'done', result: { id: messageId, status: 'dismissed' } };
198
+ }
199
+ case 'send-to-agent': {
200
+ const { recipient, message, projectId } = payload;
201
+ const ts = new Date().toISOString();
202
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, sender_name, recipient_name, source, status, created_at)
203
+ VALUES (?, ?, 'user', ?, 'ui', 'pending', ?)`).run(projectId ?? null, message, recipient, ts);
204
+ const id = result.lastInsertRowid;
205
+ const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
206
+ broadcast('agent_question', { entity: 'agent_message', action: 'agent_question', payload: msg });
207
+ return { status: 'done', result: { id, recipient } };
208
+ }
209
+ default:
210
+ return { status: 'error', result: `Unknown command type: ${type}` };
211
+ }
212
+ }
213
+ catch (err) {
214
+ return { status: 'error', result: err.message };
215
+ }
216
+ }
145
217
  export async function startSSEServer() {
146
218
  const server = createServer(async (req, res) => {
147
219
  // CORS headers for all requests
@@ -591,27 +663,50 @@ export async function startSSEServer() {
591
663
  else {
592
664
  console.log(`[SSE] listening on port ${port}`);
593
665
  }
594
- // Register with remote relay so it knows where to proxy commands
666
+ // Relay command polling + state pushing (if relay is configured)
595
667
  if (RELAY_URL && RELAY_PUSH_TOKEN) {
596
- const localUrl = `http://${cfg.host}:${port}`;
597
- fetch(`${RELAY_URL}/register`, {
598
- method: 'POST',
599
- headers: {
600
- 'Content-Type': 'application/json',
601
- 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}`,
602
- },
603
- body: JSON.stringify({ url: localUrl }),
604
- }).then(async (res) => {
605
- if (res.ok) {
606
- console.log(`[SSE] registered with relay at ${RELAY_URL}`);
668
+ console.log(`[relay] command polling started → ${RELAY_URL}`);
669
+ // Push lightweight sync state — only what the remote app needs
670
+ // SSE events handle real-time updates after this initial load
671
+ const pushState = () => {
672
+ try {
673
+ const db = getDb();
674
+ const state = {
675
+ agentRegistry: db.prepare('SELECT * FROM agent_registry').all(),
676
+ agentMessages: db.prepare('SELECT * FROM agent_messages ORDER BY id DESC LIMIT 100').all(),
677
+ notifications: db.prepare('SELECT * FROM notifications ORDER BY id DESC LIMIT 50').all(),
678
+ projects: db.prepare('SELECT * FROM projects').all(),
679
+ };
680
+ fetch(`${RELAY_URL}/push/state`, {
681
+ method: 'POST',
682
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}` },
683
+ body: JSON.stringify(state),
684
+ }).catch(() => { });
607
685
  }
608
- else {
609
- const body = await res.text().catch(() => '');
610
- console.error(`[SSE] relay registration rejected: ${res.status} ${body}`);
686
+ catch { /* ignore */ }
687
+ };
688
+ pushState();
689
+ setInterval(pushState, 60_000);
690
+ // Poll for commands every 2s
691
+ setInterval(async () => {
692
+ try {
693
+ const res = await fetch(`${RELAY_URL}/commands/pending`, {
694
+ headers: { 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}` },
695
+ });
696
+ if (!res.ok)
697
+ return;
698
+ const commands = await res.json();
699
+ for (const cmd of commands) {
700
+ const result = await executeRelayCommand(cmd.type, cmd.payload);
701
+ fetch(`${RELAY_URL}/commands/${cmd.id}/done`, {
702
+ method: 'POST',
703
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}` },
704
+ body: JSON.stringify(result),
705
+ }).catch(() => { });
706
+ }
611
707
  }
612
- }).catch((err) => {
613
- console.error(`[SSE] relay registration failed: ${err.message}`);
614
- });
708
+ catch { /* relay may be down */ }
709
+ }, 2_000);
615
710
  }
616
711
  resolve();
617
712
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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",