@dalmasonto/taskflow-mcp 1.0.7 → 1.0.9
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 +134 -0
- package/dist/tools/agent-inbox.js +62 -7
- 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 message delivery.
|
|
8
|
+
* Returns a cleanup function for graceful shutdown.
|
|
9
|
+
*/
|
|
10
|
+
export declare function startTmuxBridge(options: BridgeOptions): () => void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { getDb } from './db.js';
|
|
3
|
+
import { getActivePort } from './sse.js';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
// ─── SSE Listener (replaces the 3s poller) ───────────────────────────
|
|
6
|
+
function startSSEListener(options) {
|
|
7
|
+
const { agentName, agentPid, tmuxPane } = options;
|
|
8
|
+
const port = getActivePort();
|
|
9
|
+
function connect() {
|
|
10
|
+
const req = http.get(`http://localhost:${port}/events`, (res) => {
|
|
11
|
+
let buffer = '';
|
|
12
|
+
res.on('data', (chunk) => {
|
|
13
|
+
buffer += chunk.toString();
|
|
14
|
+
const lines = buffer.split('\n');
|
|
15
|
+
buffer = lines.pop() || '';
|
|
16
|
+
let eventType = '';
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (line.startsWith('event: ')) {
|
|
19
|
+
eventType = line.slice(7).trim();
|
|
20
|
+
}
|
|
21
|
+
else if (line.startsWith('data: ') && eventType) {
|
|
22
|
+
try {
|
|
23
|
+
const data = JSON.parse(line.slice(6));
|
|
24
|
+
handleSSEEvent(eventType, data, options);
|
|
25
|
+
}
|
|
26
|
+
catch { /* malformed JSON */ }
|
|
27
|
+
eventType = '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
res.on('end', () => {
|
|
32
|
+
console.error('[bridge] SSE connection closed, reconnecting in 3s...');
|
|
33
|
+
setTimeout(connect, 3000);
|
|
34
|
+
});
|
|
35
|
+
res.on('error', () => {
|
|
36
|
+
console.error('[bridge] SSE connection error, reconnecting in 3s...');
|
|
37
|
+
setTimeout(connect, 3000);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
req.on('error', () => {
|
|
41
|
+
console.error('[bridge] SSE connect failed, retrying in 3s...');
|
|
42
|
+
setTimeout(connect, 3000);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Initial sweep: deliver any undelivered messages from before SSE connected
|
|
46
|
+
deliverUndelivered(options);
|
|
47
|
+
connect();
|
|
48
|
+
}
|
|
49
|
+
function handleSSEEvent(event, data, options) {
|
|
50
|
+
const { agentName, agentPid, tmuxPane } = options;
|
|
51
|
+
const payload = data.payload;
|
|
52
|
+
if (!payload)
|
|
53
|
+
return;
|
|
54
|
+
if (event === 'agent_question') {
|
|
55
|
+
const recipient = payload.recipient_name;
|
|
56
|
+
const sender = payload.sender_name;
|
|
57
|
+
const status = payload.status;
|
|
58
|
+
const id = payload.id;
|
|
59
|
+
const delivered = payload.delivered;
|
|
60
|
+
if (recipient !== agentName || status !== 'pending' || delivered === 1)
|
|
61
|
+
return;
|
|
62
|
+
let text;
|
|
63
|
+
if (sender === 'user') {
|
|
64
|
+
text = `[Message from User]: ${payload.question}`;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
text = `[Message from ${sender}]: ${payload.question}`;
|
|
68
|
+
}
|
|
69
|
+
injectAndMarkDelivered(id, text, tmuxPane);
|
|
70
|
+
}
|
|
71
|
+
if (event === 'agent_question_answered') {
|
|
72
|
+
const sender = payload.sender_name;
|
|
73
|
+
const recipient = payload.recipient_name;
|
|
74
|
+
const status = payload.status;
|
|
75
|
+
const id = payload.id;
|
|
76
|
+
const delivered = payload.delivered;
|
|
77
|
+
const agentPidField = payload.agent_pid;
|
|
78
|
+
const isOurs = (sender === agentName || agentPidField === agentPid) && recipient === 'user';
|
|
79
|
+
if (!isOurs || status !== 'answered' || delivered === 1)
|
|
80
|
+
return;
|
|
81
|
+
const question = (payload.question || '').slice(0, 60);
|
|
82
|
+
const response = payload.response;
|
|
83
|
+
const text = `[Inbox Response] to "${question}": ${response}`;
|
|
84
|
+
injectAndMarkDelivered(id, text, tmuxPane);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function injectAndMarkDelivered(id, text, tmuxPane) {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
|
|
90
|
+
try {
|
|
91
|
+
execSync(`tmux send-keys -t ${tmuxPane} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
|
|
92
|
+
console.error(`[bridge] delivered message ${id} to tmux pane ${tmuxPane}`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error(`[bridge] tmux send-keys failed for message ${id}:`, err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function deliverUndelivered(options) {
|
|
99
|
+
const { agentName, agentPid, tmuxPane } = options;
|
|
100
|
+
const db = getDb();
|
|
101
|
+
const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
|
|
102
|
+
(recipient_name = ? AND status = 'pending') OR
|
|
103
|
+
(sender_name = ? AND recipient_name = 'user' AND status = 'answered') OR
|
|
104
|
+
(agent_pid = ? AND recipient_name = 'user' AND status = 'answered')
|
|
105
|
+
)`).all(agentName, agentName, agentPid);
|
|
106
|
+
for (const msg of incoming) {
|
|
107
|
+
let text;
|
|
108
|
+
if (msg.recipient_name === agentName && msg.sender_name === 'user') {
|
|
109
|
+
text = `[Message from User]: ${msg.question}`;
|
|
110
|
+
}
|
|
111
|
+
else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
|
|
112
|
+
text = `[Message from ${msg.sender_name}]: ${msg.question}`;
|
|
113
|
+
}
|
|
114
|
+
else if (msg.status === 'answered' && msg.response) {
|
|
115
|
+
text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
injectAndMarkDelivered(msg.id, text, tmuxPane);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ─── Public API ──────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Start the tmux bridge: SSE listener for instant message delivery.
|
|
126
|
+
* Returns a cleanup function for graceful shutdown.
|
|
127
|
+
*/
|
|
128
|
+
export function startTmuxBridge(options) {
|
|
129
|
+
startSSEListener(options);
|
|
130
|
+
console.error(`[bridge] tmux bridge active for agent "${options.agentName}" on pane ${options.tmuxPane}`);
|
|
131
|
+
return () => {
|
|
132
|
+
// SSE connection will close when process exits
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -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);
|
|
@@ -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
|
|
48
|
-
message_id: z.number().describe('The
|
|
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
|
-
|
|
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).', {
|
|
@@ -73,13 +75,66 @@ export function registerAgentInboxTools(server) {
|
|
|
73
75
|
if (!recipient)
|
|
74
76
|
return errorResponse(`Agent "${params.recipient}" not found`, 'NOT_FOUND');
|
|
75
77
|
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);
|
|
78
|
+
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, source, status, created_at)
|
|
79
|
+
VALUES (NULL, ?, ?, ?, ?, 'mcp', 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
|
|
78
80
|
const id = result.lastInsertRowid;
|
|
79
81
|
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
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 = ? 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();
|