@dalmasonto/taskflow-mcp 1.0.19 → 1.0.20

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 +114 -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,51 @@ 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 state snapshot on startup and every 30s
670
+ const pushState = () => {
671
+ try {
672
+ const db = getDb();
673
+ const state = {
674
+ tasks: db.prepare('SELECT * FROM tasks').all(),
675
+ projects: db.prepare('SELECT * FROM projects').all(),
676
+ sessions: db.prepare('SELECT * FROM sessions').all(),
677
+ activityLogs: db.prepare('SELECT * FROM activity_logs ORDER BY id DESC LIMIT 100').all(),
678
+ agentMessages: db.prepare('SELECT * FROM agent_messages ORDER BY id DESC LIMIT 200').all(),
679
+ agentRegistry: db.prepare('SELECT * FROM agent_registry').all(),
680
+ };
681
+ fetch(`${RELAY_URL}/push/state`, {
682
+ method: 'POST',
683
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}` },
684
+ body: JSON.stringify(state),
685
+ }).catch(() => { });
607
686
  }
608
- else {
609
- const body = await res.text().catch(() => '');
610
- console.error(`[SSE] relay registration rejected: ${res.status} ${body}`);
687
+ catch { /* ignore */ }
688
+ };
689
+ pushState();
690
+ setInterval(pushState, 30_000);
691
+ // Poll for commands every 2s
692
+ setInterval(async () => {
693
+ try {
694
+ const res = await fetch(`${RELAY_URL}/commands/pending`, {
695
+ headers: { 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}` },
696
+ });
697
+ if (!res.ok)
698
+ return;
699
+ const commands = await res.json();
700
+ for (const cmd of commands) {
701
+ const result = await executeRelayCommand(cmd.type, cmd.payload);
702
+ fetch(`${RELAY_URL}/commands/${cmd.id}/done`, {
703
+ method: 'POST',
704
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}` },
705
+ body: JSON.stringify(result),
706
+ }).catch(() => { });
707
+ }
611
708
  }
612
- }).catch((err) => {
613
- console.error(`[SSE] relay registration failed: ${err.message}`);
614
- });
709
+ catch { /* relay may be down */ }
710
+ }, 2_000);
615
711
  }
616
712
  resolve();
617
713
  });
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.20",
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",