@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.
- package/dist/sse.js +113 -18
- 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
|
-
//
|
|
666
|
+
// Relay command polling + state pushing (if relay is configured)
|
|
595
667
|
if (RELAY_URL && RELAY_PUSH_TOKEN) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
});
|
|
708
|
+
catch { /* relay may be down */ }
|
|
709
|
+
}, 2_000);
|
|
615
710
|
}
|
|
616
711
|
resolve();
|
|
617
712
|
});
|