@dalmasonto/taskflow-mcp 1.0.7 → 1.0.8
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/db.js +6 -1
- package/dist/index.js +19 -49
- package/dist/sse.js +6 -4
- package/dist/tmux-bridge.d.ts +11 -0
- package/dist/tmux-bridge.js +280 -0
- package/dist/tools/agent-inbox.js +4 -4
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -103,6 +103,7 @@ function initSchema(db) {
|
|
|
103
103
|
sender_name TEXT NOT NULL DEFAULT 'unknown',
|
|
104
104
|
recipient_name TEXT NOT NULL DEFAULT 'user',
|
|
105
105
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
106
|
+
source TEXT NOT NULL DEFAULT 'mcp',
|
|
106
107
|
created_at TEXT NOT NULL,
|
|
107
108
|
answered_at TEXT
|
|
108
109
|
);
|
|
@@ -146,6 +147,9 @@ function initSchema(db) {
|
|
|
146
147
|
if (!colNames.has('recipient_name')) {
|
|
147
148
|
db.exec("ALTER TABLE agent_messages ADD COLUMN recipient_name TEXT NOT NULL DEFAULT 'user'");
|
|
148
149
|
}
|
|
150
|
+
if (!colNames.has('source')) {
|
|
151
|
+
db.exec("ALTER TABLE agent_messages ADD COLUMN source TEXT NOT NULL DEFAULT 'mcp'");
|
|
152
|
+
}
|
|
149
153
|
// Migration: make agent_messages.project_id nullable if it was NOT NULL
|
|
150
154
|
try {
|
|
151
155
|
const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'agent_messages'").get();
|
|
@@ -163,13 +167,14 @@ function initSchema(db) {
|
|
|
163
167
|
sender_name TEXT NOT NULL DEFAULT 'unknown',
|
|
164
168
|
recipient_name TEXT NOT NULL DEFAULT 'user',
|
|
165
169
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
170
|
+
source TEXT NOT NULL DEFAULT 'mcp',
|
|
166
171
|
created_at TEXT NOT NULL,
|
|
167
172
|
answered_at TEXT
|
|
168
173
|
);
|
|
169
174
|
INSERT INTO agent_messages_new SELECT
|
|
170
175
|
id, project_id, question, context, choices, response, agent_pid, delivered,
|
|
171
176
|
COALESCE(sender_name, 'unknown'), COALESCE(recipient_name, 'user'),
|
|
172
|
-
status, created_at, answered_at
|
|
177
|
+
status, COALESCE(source, 'mcp'), created_at, answered_at
|
|
173
178
|
FROM agent_messages;
|
|
174
179
|
DROP TABLE agent_messages;
|
|
175
180
|
ALTER TABLE agent_messages_new RENAME TO agent_messages;
|
package/dist/index.js
CHANGED
|
@@ -72,17 +72,15 @@ if (!httpOnly) {
|
|
|
72
72
|
const { registerAgent, unregisterAgent } = await import('./agent-registry.js');
|
|
73
73
|
// Auto-register this agent
|
|
74
74
|
const agentName = registerAgent();
|
|
75
|
+
const agentPid = process.ppid;
|
|
75
76
|
console.error(`[agent] registered as "${agentName}"`);
|
|
76
|
-
|
|
77
|
-
const cleanup = () => { try {
|
|
77
|
+
let cleanup = () => { try {
|
|
78
78
|
unregisterAgent(agentName);
|
|
79
79
|
}
|
|
80
80
|
catch { } process.exit(0); };
|
|
81
81
|
process.on('SIGINT', cleanup);
|
|
82
82
|
process.on('SIGTERM', cleanup);
|
|
83
|
-
//
|
|
84
|
-
const POLL_INTERVAL = 3000;
|
|
85
|
-
const agentPid = process.ppid;
|
|
83
|
+
// Tmux bridge: SSE listener for instant delivery + capture for terminal→chat
|
|
86
84
|
let tmuxTarget = null;
|
|
87
85
|
try {
|
|
88
86
|
const { execSync: exec } = await import('child_process');
|
|
@@ -95,53 +93,25 @@ if (!httpOnly) {
|
|
|
95
93
|
break;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
|
-
if (tmuxTarget)
|
|
99
|
-
console.error(`[inject] tmux pane ${tmuxTarget} for agent "${agentName}"`);
|
|
100
|
-
else
|
|
101
|
-
console.error('[inject] agent not in tmux — terminal injection disabled');
|
|
102
96
|
}
|
|
103
97
|
catch {
|
|
104
|
-
console.error('[
|
|
98
|
+
console.error('[bridge] tmux not available');
|
|
105
99
|
}
|
|
106
100
|
if (tmuxTarget) {
|
|
107
|
-
const {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let text;
|
|
123
|
-
if (msg.recipient_name === agentName && msg.sender_name === 'user') {
|
|
124
|
-
text = `[Message from User]: ${msg.question}`;
|
|
125
|
-
}
|
|
126
|
-
else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
|
|
127
|
-
text = `[Message from ${msg.sender_name}]: ${msg.question}`;
|
|
128
|
-
}
|
|
129
|
-
else if (msg.status === 'answered' && msg.response) {
|
|
130
|
-
text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
exec(`tmux send-keys -t ${target} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
|
|
137
|
-
console.error(`[inject] delivered message ${msg.id} to tmux pane ${target}`);
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
console.error(`[inject] tmux send-keys failed for message ${msg.id}:`, err);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch { /* ignore */ }
|
|
145
|
-
}, POLL_INTERVAL);
|
|
101
|
+
const { startTmuxBridge } = await import('./tmux-bridge.js');
|
|
102
|
+
const stopBridge = startTmuxBridge({
|
|
103
|
+
agentName,
|
|
104
|
+
agentPid,
|
|
105
|
+
tmuxPane: tmuxTarget,
|
|
106
|
+
});
|
|
107
|
+
const originalCleanup = cleanup;
|
|
108
|
+
cleanup = () => { stopBridge(); originalCleanup(); };
|
|
109
|
+
process.removeListener('SIGINT', originalCleanup);
|
|
110
|
+
process.removeListener('SIGTERM', originalCleanup);
|
|
111
|
+
process.on('SIGINT', cleanup);
|
|
112
|
+
process.on('SIGTERM', cleanup);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.error('[bridge] agent not in tmux — bridge disabled');
|
|
146
116
|
}
|
|
147
117
|
}
|
package/dist/sse.js
CHANGED
|
@@ -317,18 +317,20 @@ export async function startSSEServer() {
|
|
|
317
317
|
jsonResponse(res, 200, updated);
|
|
318
318
|
return;
|
|
319
319
|
}
|
|
320
|
-
// POST /api/agent-messages/send —
|
|
320
|
+
// POST /api/agent-messages/send — send a message (from user UI or capture system)
|
|
321
321
|
if (req.url === '/api/agent-messages/send' && req.method === 'POST') {
|
|
322
322
|
const db = getDb();
|
|
323
323
|
const body = JSON.parse(await readBody(req));
|
|
324
|
-
const { recipient, message: msgText, projectId } = body;
|
|
324
|
+
const { recipient, message: msgText, projectId, source: msgSource, senderName } = body;
|
|
325
325
|
if (!recipient || !msgText) {
|
|
326
326
|
jsonResponse(res, 400, { error: 'recipient and message are required' });
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
|
+
const source = msgSource || 'ui';
|
|
330
|
+
const sender = senderName || 'user';
|
|
329
331
|
const ts = new Date().toISOString();
|
|
330
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, sender_name, recipient_name, status, created_at)
|
|
331
|
-
VALUES (?, ?,
|
|
332
|
+
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, sender_name, recipient_name, source, status, created_at)
|
|
333
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?)`).run(projectId ?? null, msgText, sender, recipient, source, ts);
|
|
332
334
|
const id = result.lastInsertRowid;
|
|
333
335
|
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
334
336
|
broadcast('agent_question', { entity: 'agent_message', action: 'agent_question', payload: msg });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface BridgeOptions {
|
|
2
|
+
agentName: string;
|
|
3
|
+
agentPid: number;
|
|
4
|
+
tmuxPane: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Start the tmux bridge: SSE listener for instant delivery + capture for terminal→chat.
|
|
8
|
+
* Returns a cleanup function for graceful shutdown.
|
|
9
|
+
*/
|
|
10
|
+
export declare function startTmuxBridge(options: BridgeOptions): () => void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { createReadStream, writeFileSync, unlinkSync, watchFile, unwatchFile, existsSync } from 'fs';
|
|
3
|
+
import { getDb } from './db.js';
|
|
4
|
+
import { getActivePort } from './sse.js';
|
|
5
|
+
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
|
+
// ─── SSE Listener (replaces the 3s poller) ───────────────────────────
|
|
20
|
+
function startSSEListener(options) {
|
|
21
|
+
const { agentName, agentPid, tmuxPane } = options;
|
|
22
|
+
const port = getActivePort();
|
|
23
|
+
function connect() {
|
|
24
|
+
const req = http.get(`http://localhost:${port}/events`, (res) => {
|
|
25
|
+
let buffer = '';
|
|
26
|
+
res.on('data', (chunk) => {
|
|
27
|
+
buffer += chunk.toString();
|
|
28
|
+
const lines = buffer.split('\n');
|
|
29
|
+
buffer = lines.pop() || '';
|
|
30
|
+
let eventType = '';
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (line.startsWith('event: ')) {
|
|
33
|
+
eventType = line.slice(7).trim();
|
|
34
|
+
}
|
|
35
|
+
else if (line.startsWith('data: ') && eventType) {
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(line.slice(6));
|
|
38
|
+
handleSSEEvent(eventType, data, options);
|
|
39
|
+
}
|
|
40
|
+
catch { /* malformed JSON */ }
|
|
41
|
+
eventType = '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
res.on('end', () => {
|
|
46
|
+
console.error('[bridge] SSE connection closed, reconnecting in 3s...');
|
|
47
|
+
setTimeout(connect, 3000);
|
|
48
|
+
});
|
|
49
|
+
res.on('error', () => {
|
|
50
|
+
console.error('[bridge] SSE connection error, reconnecting in 3s...');
|
|
51
|
+
setTimeout(connect, 3000);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
req.on('error', () => {
|
|
55
|
+
console.error('[bridge] SSE connect failed, retrying in 3s...');
|
|
56
|
+
setTimeout(connect, 3000);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Initial sweep: deliver any undelivered messages from before SSE connected
|
|
60
|
+
deliverUndelivered(options);
|
|
61
|
+
connect();
|
|
62
|
+
}
|
|
63
|
+
function handleSSEEvent(event, data, options) {
|
|
64
|
+
const { agentName, agentPid, tmuxPane } = options;
|
|
65
|
+
const payload = data.payload;
|
|
66
|
+
if (!payload)
|
|
67
|
+
return;
|
|
68
|
+
if (event === 'agent_question') {
|
|
69
|
+
// Incoming message addressed to this agent
|
|
70
|
+
const recipient = payload.recipient_name;
|
|
71
|
+
const sender = payload.sender_name;
|
|
72
|
+
const status = payload.status;
|
|
73
|
+
const id = payload.id;
|
|
74
|
+
const delivered = payload.delivered;
|
|
75
|
+
if (recipient !== agentName || status !== 'pending' || delivered === 1)
|
|
76
|
+
return;
|
|
77
|
+
let text;
|
|
78
|
+
if (sender === 'user') {
|
|
79
|
+
text = `[Message from User]: ${payload.question}`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
text = `[Message from ${sender}]: ${payload.question}`;
|
|
83
|
+
}
|
|
84
|
+
injectAndMarkDelivered(id, text, tmuxPane);
|
|
85
|
+
}
|
|
86
|
+
if (event === 'agent_question_answered') {
|
|
87
|
+
// A question this agent sent got answered
|
|
88
|
+
const sender = payload.sender_name;
|
|
89
|
+
const recipient = payload.recipient_name;
|
|
90
|
+
const status = payload.status;
|
|
91
|
+
const id = payload.id;
|
|
92
|
+
const delivered = payload.delivered;
|
|
93
|
+
const agentPidField = payload.agent_pid;
|
|
94
|
+
const isOurs = (sender === agentName || agentPidField === agentPid) && recipient === 'user';
|
|
95
|
+
if (!isOurs || status !== 'answered' || delivered === 1)
|
|
96
|
+
return;
|
|
97
|
+
const question = (payload.question || '').slice(0, 60);
|
|
98
|
+
const response = payload.response;
|
|
99
|
+
const text = `[Inbox Response] to "${question}": ${response}`;
|
|
100
|
+
injectAndMarkDelivered(id, text, tmuxPane);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function injectAndMarkDelivered(id, text, tmuxPane) {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
|
|
106
|
+
try {
|
|
107
|
+
execSync(`tmux send-keys -t ${tmuxPane} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
|
|
108
|
+
console.error(`[bridge] delivered message ${id} to tmux pane ${tmuxPane}`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.error(`[bridge] tmux send-keys failed for message ${id}:`, err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function deliverUndelivered(options) {
|
|
115
|
+
const { agentName, agentPid, tmuxPane } = options;
|
|
116
|
+
const db = getDb();
|
|
117
|
+
const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
|
|
118
|
+
(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')
|
|
121
|
+
)`).all(agentName, agentName, agentPid);
|
|
122
|
+
for (const msg of incoming) {
|
|
123
|
+
let text;
|
|
124
|
+
if (msg.recipient_name === agentName && msg.sender_name === 'user') {
|
|
125
|
+
text = `[Message from User]: ${msg.question}`;
|
|
126
|
+
}
|
|
127
|
+
else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
|
|
128
|
+
text = `[Message from ${msg.sender_name}]: ${msg.question}`;
|
|
129
|
+
}
|
|
130
|
+
else if (msg.status === 'answered' && msg.response) {
|
|
131
|
+
text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
injectAndMarkDelivered(msg.id, text, tmuxPane);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
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
|
+
// ─── Public API ──────────────────────────────────────────────────────
|
|
269
|
+
/**
|
|
270
|
+
* Start the tmux bridge: SSE listener for instant delivery + capture for terminal→chat.
|
|
271
|
+
* Returns a cleanup function for graceful shutdown.
|
|
272
|
+
*/
|
|
273
|
+
export function startTmuxBridge(options) {
|
|
274
|
+
startSSEListener(options);
|
|
275
|
+
const stopCapture = startCapture(options);
|
|
276
|
+
console.error(`[bridge] tmux bridge active for agent "${options.agentName}" on pane ${options.tmuxPane}`);
|
|
277
|
+
return () => {
|
|
278
|
+
stopCapture();
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -31,8 +31,8 @@ export function registerAgentInboxTools(server) {
|
|
|
31
31
|
return errorResponse(`Project ${params.project_id} not found`, 'NOT_FOUND');
|
|
32
32
|
const senderName = ensureRegistered();
|
|
33
33
|
const ts = now();
|
|
34
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, status, created_at)
|
|
35
|
-
VALUES (?, ?, ?, ?, ?, 'user', ?, 'pending', ?)`).run(params.project_id, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, process.ppid, ts);
|
|
34
|
+
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, created_at)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, 'user', ?, 'mcp', 'pending', ?)`).run(params.project_id, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, process.ppid, ts);
|
|
36
36
|
const id = result.lastInsertRowid;
|
|
37
37
|
const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
38
38
|
broadcastChange('agent_message', 'agent_question', message);
|
|
@@ -73,8 +73,8 @@ export function registerAgentInboxTools(server) {
|
|
|
73
73
|
if (!recipient)
|
|
74
74
|
return errorResponse(`Agent "${params.recipient}" not found`, 'NOT_FOUND');
|
|
75
75
|
const ts = now();
|
|
76
|
-
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, status, created_at)
|
|
77
|
-
VALUES (NULL, ?, ?, ?, ?, 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
|
|
76
|
+
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, source, status, created_at)
|
|
77
|
+
VALUES (NULL, ?, ?, ?, ?, 'mcp', 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
|
|
78
78
|
const id = result.lastInsertRowid;
|
|
79
79
|
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
80
80
|
broadcastChange('agent_message', 'agent_question', msg);
|