agentmesh-ai 0.2.1 → 0.3.0

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 (79) hide show
  1. package/README.md +127 -144
  2. package/dist/cli/commands/init.d.ts.map +1 -1
  3. package/dist/cli/commands/init.js +41 -22
  4. package/dist/cli/commands/init.js.map +1 -1
  5. package/dist/cli/commands/join.d.ts +1 -5
  6. package/dist/cli/commands/join.d.ts.map +1 -1
  7. package/dist/cli/commands/join.js +99 -76
  8. package/dist/cli/commands/join.js.map +1 -1
  9. package/dist/cli/commands/serve.d.ts.map +1 -1
  10. package/dist/cli/commands/serve.js +8 -1
  11. package/dist/cli/commands/serve.js.map +1 -1
  12. package/dist/cli/commands/setup.d.ts.map +1 -1
  13. package/dist/cli/commands/setup.js +31 -10
  14. package/dist/cli/commands/setup.js.map +1 -1
  15. package/dist/cli/commands/status.d.ts.map +1 -1
  16. package/dist/cli/commands/status.js +15 -0
  17. package/dist/cli/commands/status.js.map +1 -1
  18. package/dist/cli/index.js +3 -4
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cloud/cloud-conversation.d.ts +83 -0
  21. package/dist/cloud/cloud-conversation.d.ts.map +1 -0
  22. package/dist/cloud/cloud-conversation.js +357 -0
  23. package/dist/cloud/cloud-conversation.js.map +1 -0
  24. package/dist/cloud/index.d.ts +4 -0
  25. package/dist/cloud/index.d.ts.map +1 -0
  26. package/dist/cloud/index.js +3 -0
  27. package/dist/cloud/index.js.map +1 -0
  28. package/dist/cloud/supabase-client.d.ts +29 -0
  29. package/dist/cloud/supabase-client.d.ts.map +1 -0
  30. package/dist/cloud/supabase-client.js +135 -0
  31. package/dist/cloud/supabase-client.js.map +1 -0
  32. package/dist/conversation/server.d.ts.map +1 -1
  33. package/dist/conversation/server.js +6 -0
  34. package/dist/conversation/server.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp/conversation-tools.d.ts +5 -1
  40. package/dist/mcp/conversation-tools.d.ts.map +1 -1
  41. package/dist/mcp/conversation-tools.js +202 -149
  42. package/dist/mcp/conversation-tools.js.map +1 -1
  43. package/dist/mcp/index.js +94 -36
  44. package/dist/mcp/index.js.map +1 -1
  45. package/dist/mcp/memory-tools.d.ts +20 -2
  46. package/dist/mcp/memory-tools.d.ts.map +1 -1
  47. package/dist/mcp/memory-tools.js +88 -44
  48. package/dist/mcp/memory-tools.js.map +1 -1
  49. package/dist/memory/index.d.ts +1 -0
  50. package/dist/memory/index.d.ts.map +1 -1
  51. package/dist/memory/index.js +1 -0
  52. package/dist/memory/index.js.map +1 -1
  53. package/dist/memory/merge-view.d.ts.map +1 -1
  54. package/dist/memory/merge-view.js +5 -4
  55. package/dist/memory/merge-view.js.map +1 -1
  56. package/dist/memory/reader.js +2 -2
  57. package/dist/memory/reader.js.map +1 -1
  58. package/dist/memory/schema.d.ts +3 -0
  59. package/dist/memory/schema.d.ts.map +1 -1
  60. package/dist/memory/schema.js +1 -0
  61. package/dist/memory/schema.js.map +1 -1
  62. package/dist/memory/types.d.ts +1 -0
  63. package/dist/memory/types.d.ts.map +1 -1
  64. package/dist/memory/write-utils.d.ts +30 -0
  65. package/dist/memory/write-utils.d.ts.map +1 -0
  66. package/dist/memory/write-utils.js +98 -0
  67. package/dist/memory/write-utils.js.map +1 -0
  68. package/dist/memory/writer.d.ts.map +1 -1
  69. package/dist/memory/writer.js +21 -84
  70. package/dist/memory/writer.js.map +1 -1
  71. package/dist/utils/id.d.ts +2 -0
  72. package/dist/utils/id.d.ts.map +1 -1
  73. package/dist/utils/id.js +4 -0
  74. package/dist/utils/id.js.map +1 -1
  75. package/dist/utils/notify.d.ts +2 -0
  76. package/dist/utils/notify.d.ts.map +1 -0
  77. package/dist/utils/notify.js +50 -0
  78. package/dist/utils/notify.js.map +1 -0
  79. package/package.json +5 -4
@@ -1,11 +1,32 @@
1
1
  /**
2
2
  * MCP tool definitions for conversation space operations.
3
+ *
4
+ * Supports both legacy WebSocket client and cloud Supabase client.
5
+ * The cloud client uses push events (waitForMessage) instead of polling.
3
6
  */
4
7
  import { z } from 'zod';
5
8
  import { randomUUID } from 'node:crypto';
9
+ import { CloudConversationClient } from '../cloud/cloud-conversation.js';
6
10
  import { writeMemoryEntry } from '../memory/writer.js';
7
11
  import { readHubConfig } from '../memory/reader.js';
8
- export function registerConversationTools(server, getAgentHubDir, getAgentId, getOrConnectClient) {
12
+ import { cloudWriteMemory } from '../cloud/supabase-client.js';
13
+ /** Format ISO timestamp as local time + relative ("14:23:05 (3m ago)"). */
14
+ function formatTime(isoTimestamp) {
15
+ const date = new Date(isoTimestamp);
16
+ const diffMin = Math.floor((Date.now() - date.getTime()) / 60000);
17
+ const local = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
18
+ if (diffMin < 1)
19
+ return `${local} (just now)`;
20
+ if (diffMin < 60)
21
+ return `${local} (${diffMin}m ago)`;
22
+ if (diffMin < 1440)
23
+ return `${local} (${Math.floor(diffMin / 60)}h ago)`;
24
+ return `${local} (${Math.floor(diffMin / 1440)}d ago)`;
25
+ }
26
+ export function registerConversationTools(server, getAgentHubDir, getAgentId, getOrConnectClient, getTeamId) {
27
+ const noConnectionMsg = (teamId) => teamId
28
+ ? 'Cannot connect to cloud conversation. Check your network connection.'
29
+ : 'No conversation space is running. A human can start one with: npx agentmesh-ai serve';
9
30
  server.tool('start_meeting', `Start a meeting — check that all required team members are online before discussing.
10
31
 
11
32
  Use this BEFORE using discuss when you need a team decision. It ensures everyone
@@ -17,15 +38,10 @@ to come online. You can call start_meeting again to check if they've joined.`, {
17
38
  requiredAgents: z.array(z.string()).optional().describe('Agent IDs that must attend. If omitted, all agents from hub.yaml are required.'),
18
39
  }, async ({ topic, requiredAgents }) => {
19
40
  const client = await getOrConnectClient();
41
+ const teamId = getTeamId?.();
20
42
  if (!client?.isConnected) {
21
- return {
22
- content: [{
23
- type: 'text',
24
- text: 'Cannot start meeting: no conversation space is running. A human can start one with: npx agentmesh-ai serve'
25
- }]
26
- };
43
+ return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
27
44
  }
28
- // If no specific agents given, require all from hub.yaml
29
45
  let required = requiredAgents;
30
46
  if (!required || required.length === 0) {
31
47
  const dir = getAgentHubDir();
@@ -34,8 +50,10 @@ to come online. You can call start_meeting again to check if they've joined.`, {
34
50
  }
35
51
  const meetingId = `meeting-${randomUUID()}`;
36
52
  client.startMeeting(meetingId, topic, required);
37
- // Wait for server to process and send back status
38
- await new Promise(resolve => setTimeout(resolve, 2000));
53
+ // Cloud mode: meeting status is computed locally; legacy: wait for server broadcast
54
+ if (!(client instanceof CloudConversationClient)) {
55
+ await new Promise(resolve => setTimeout(resolve, 2000));
56
+ }
39
57
  const status = client.meetings.find(m => m.meetingId === meetingId);
40
58
  if (!status) {
41
59
  return {
@@ -67,18 +85,16 @@ to come online. You can call start_meeting again to check if they've joined.`, {
67
85
  Returns the list of online agents and what topics are being discussed.
68
86
  If no conversation space is running, tells you so.`, {}, async () => {
69
87
  const client = await getOrConnectClient();
88
+ const teamId = getTeamId?.();
70
89
  if (!client?.isConnected) {
71
- return {
72
- content: [{
73
- type: 'text',
74
- text: 'No conversation space is running. A human can start one with: npx agentmesh-ai serve'
75
- }]
76
- };
90
+ return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
77
91
  }
78
92
  const agents = client.agents;
79
93
  const topics = client.topics;
80
94
  const myId = getAgentId();
81
95
  const lines = [];
96
+ const mode = teamId ? '☁️ Cloud' : '🔌 Local';
97
+ lines.push(`${mode} conversation space\n`);
82
98
  lines.push(`Connected agents (${agents.length}):`);
83
99
  for (const a of agents) {
84
100
  lines.push(` - ${a.displayName} (${a.role}) using ${a.tool}`);
@@ -90,7 +106,7 @@ If no conversation space is running, tells you so.`, {}, async () => {
90
106
  const msgs = client.getTopicMessages(t);
91
107
  const participants = [...new Set(msgs.map(m => m.from.displayName))];
92
108
  const lastMsg = msgs[msgs.length - 1];
93
- const lastTime = lastMsg ? lastMsg.timestamp.slice(11, 19) : '?';
109
+ const lastTime = lastMsg ? formatTime(lastMsg.timestamp) : '?';
94
110
  const myParticipation = msgs.some(m => m.from.id === myId) ? '✓ you spoke' : '⏳ waiting for you';
95
111
  lines.push(` - "${t}" — ${msgs.length} msgs, participants: ${participants.join(', ')}, last: ${lastTime} (${myParticipation})`);
96
112
  }
@@ -109,25 +125,19 @@ Your message will be seen by all connected agents.`, {
109
125
  related_to: z.string().optional().describe('Memory topic or file path this relates to'),
110
126
  }, async ({ topic, message, related_to }) => {
111
127
  const client = await getOrConnectClient();
128
+ const teamId = getTeamId?.();
112
129
  if (!client?.isConnected) {
113
- return {
114
- content: [{
115
- type: 'text',
116
- text: 'Cannot send message: no conversation space is running. A human can start one with: npx agentmesh-ai serve'
117
- }]
118
- };
130
+ return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
119
131
  }
120
132
  client.sendMessage(topic, message, related_to);
121
- // Wait a moment for replies
122
- await new Promise(resolve => setTimeout(resolve, 2000));
123
- // Return recent messages on this topic
133
+ // Legacy mode: wait for server broadcast; cloud mode: skip
134
+ if (!(client instanceof CloudConversationClient)) {
135
+ await new Promise(resolve => setTimeout(resolve, 2000));
136
+ }
124
137
  const recent = client.getTopicMessages(topic).slice(-10);
125
138
  if (recent.length === 0) {
126
139
  return {
127
- content: [{
128
- type: 'text',
129
- text: `Message sent to topic "${topic}". No replies yet.`
130
- }]
140
+ content: [{ type: 'text', text: `Message sent to topic "${topic}". No replies yet.` }]
131
141
  };
132
142
  }
133
143
  const lines = [`Messages on "${topic}":\n`];
@@ -142,13 +152,9 @@ Use this to check if other agents have replied to your earlier message.`, {
142
152
  since: z.string().optional().describe('Only get messages after this ISO timestamp'),
143
153
  }, async ({ topic, since }) => {
144
154
  const client = await getOrConnectClient();
155
+ const teamId = getTeamId?.();
145
156
  if (!client?.isConnected) {
146
- return {
147
- content: [{
148
- type: 'text',
149
- text: 'No conversation space is running.'
150
- }]
151
- };
157
+ return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
152
158
  }
153
159
  let messages = client.getTopicMessages(topic);
154
160
  if (since) {
@@ -157,15 +163,12 @@ Use this to check if other agents have replied to your earlier message.`, {
157
163
  }
158
164
  if (messages.length === 0) {
159
165
  return {
160
- content: [{
161
- type: 'text',
162
- text: `No messages on "${topic}"${since ? ' since ' + since : ''}.`
163
- }]
166
+ content: [{ type: 'text', text: `No messages on "${topic}"${since ? ' since ' + since : ''}.` }]
164
167
  };
165
168
  }
166
169
  const lines = [`Messages on "${topic}" (${messages.length}):\n`];
167
170
  for (const m of messages) {
168
- const time = m.timestamp.slice(11, 19);
171
+ const time = formatTime(m.timestamp);
169
172
  lines.push(`[${time}] ${m.from.displayName}: ${m.content}`);
170
173
  }
171
174
  return { content: [{ type: 'text', text: lines.join('\n') }] };
@@ -177,9 +180,15 @@ when you want a real back-and-forth conversation.
177
180
 
178
181
  HOW IT WORKS:
179
182
  1. You send your message to the topic
180
- 2. The tool waits for other agents to reply (up to 60 seconds)
181
- 3. Returns ALL replies so you can see the full discussion
182
- 4. You then decide: call discuss again to continue, or save_to_memory to conclude
183
+ 2. The tool waits for other agents to reply
184
+ 3. If someone replies "hold on" / "wait" / "等一下", it automatically extends the wait
185
+ 4. Returns ALL replies so you can see the full discussion
186
+ 5. You then decide: call discuss again to continue, or save_to_memory to conclude
187
+
188
+ WAIT TIME:
189
+ - Default: 60 seconds. Max: 1800 seconds (30 minutes).
190
+ - If the human says something like "wait for 10 minutes", set wait_seconds to 600.
191
+ - If someone sends a "hold on" message, the wait is automatically extended by 5 minutes.
183
192
 
184
193
  WHEN TO USE:
185
194
  - You need team consensus on a decision (tech stack, architecture, conventions)
@@ -191,50 +200,32 @@ You drive the discussion loop yourself — keep calling discuss until consensus
191
200
  then call save_to_memory to record the conclusion.
192
201
 
193
202
  ASYNC FALLBACK:
194
- If the conversation space is not running or some members are offline,
195
- you can discuss asynchronously through shared memory:
203
+ If no other agents are online, you can discuss asynchronously through shared memory:
196
204
  1. Write your proposal with write_memory, topic prefix "Discussion: ", status "proposal"
197
205
  2. Other agents will see it when they read_memory (marked 📋)
198
206
  3. They can respond by writing their own entries on the same topic
199
207
  4. Once consensus forms in memory, upgrade the conclusion to status "protected"`, {
200
208
  topic: z.string().describe('Discussion topic'),
201
209
  message: z.string().describe('Your message — proposal, question, or response to others'),
202
- wait_seconds: z.number().optional().describe('How long to wait for replies (default: 30, max: 120)'),
210
+ wait_seconds: z.number().optional().describe('How long to wait for replies (default: 60, max: 1800 = 30 min). Set higher if the human asks to wait longer.'),
203
211
  related_to: z.string().optional().describe('Memory topic or file path this discussion relates to'),
204
212
  }, async ({ topic, message, wait_seconds, related_to }) => {
205
213
  const client = await getOrConnectClient();
214
+ const teamId = getTeamId?.();
206
215
  if (!client?.isConnected) {
207
- return {
208
- content: [{
209
- type: 'text',
210
- text: 'Cannot discuss: no conversation space is running. A human can start one with: npx agentmesh-ai serve'
211
- }]
212
- };
216
+ return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
213
217
  }
214
- // Record the timestamp before sending so we can filter for new replies
215
218
  const beforeSend = new Date().toISOString();
216
- // Send our message
217
219
  client.sendMessage(topic, message, related_to);
218
- // Wait for replies
219
- const waitMs = Math.min((wait_seconds ?? 30), 120) * 1000;
220
- const pollInterval = 3000; // check every 3 seconds
221
- const deadline = Date.now() + waitMs;
222
- let lastCount = 0;
223
- let silentSince = Date.now();
224
- // Poll for new messages, with early exit if replies stop coming
225
- while (Date.now() < deadline) {
226
- await new Promise(resolve => setTimeout(resolve, pollInterval));
227
- const newMessages = client.getTopicMessages(topic).filter(m => new Date(m.timestamp).getTime() > new Date(beforeSend).getTime()
228
- && m.from.id !== getAgentId());
229
- if (newMessages.length > lastCount) {
230
- // New replies came in — reset silence timer
231
- lastCount = newMessages.length;
232
- silentSince = Date.now();
233
- }
234
- else if (Date.now() - silentSince > 15000 && lastCount > 0) {
235
- // 15 seconds of silence after getting at least one reply — discussion round is done
236
- break;
237
- }
220
+ const waitMs = Math.min((wait_seconds ?? 60), 1800) * 1000;
221
+ const myId = getAgentId();
222
+ // Cloud mode: use push events (waitForMessage)
223
+ // Legacy mode: poll every 3 seconds
224
+ if (client instanceof CloudConversationClient) {
225
+ await waitForRepliesCloud(client, topic, myId, waitMs);
226
+ }
227
+ else {
228
+ await waitForRepliesPolling(client, topic, myId, beforeSend, waitMs);
238
229
  }
239
230
  // Collect all messages on this topic
240
231
  const allMessages = client.getTopicMessages(topic);
@@ -242,7 +233,7 @@ you can discuss asynchronously through shared memory:
242
233
  return {
243
234
  content: [{
244
235
  type: 'text',
245
- text: `Your message was sent to "${topic}". No other agents replied within ${wait_seconds ?? 30}s. They may be offline — the message will be there when they check.`
236
+ text: `Your message was sent to "${topic}". No other agents replied within ${wait_seconds ?? 60}s. They may be offline — the message will be there when they check.`
246
237
  }]
247
238
  };
248
239
  }
@@ -250,13 +241,12 @@ you can discuss asynchronously through shared memory:
250
241
  const lines = [];
251
242
  lines.push(`Discussion on "${topic}" (${allMessages.length} messages):\n`);
252
243
  for (const m of allMessages) {
253
- const time = m.timestamp.slice(11, 19);
254
- const isMe = m.from.id === getAgentId();
244
+ const time = formatTime(m.timestamp);
245
+ const isMe = m.from.id === myId;
255
246
  const prefix = isMe ? '(you)' : `${m.from.displayName} [${m.from.role}]`;
256
247
  lines.push(`[${time}] ${prefix}: ${m.content}`);
257
248
  }
258
- // Count unique participants (excluding self)
259
- const others = new Set(allMessages.filter(m => m.from.id !== getAgentId()).map(m => m.from.id));
249
+ const others = new Set(allMessages.filter(m => m.from.id !== myId).map(m => m.from.id));
260
250
  lines.push('');
261
251
  lines.push(`--- ${others.size} other agent(s) participated ---`);
262
252
  lines.push('');
@@ -278,13 +268,17 @@ This records the conclusion permanently so all agents (including offline ones) w
278
268
  const agentId = getAgentId();
279
269
  const hub = await readHubConfig(dir);
280
270
  const agentInfo = hub?.agents.find(a => a.id === agentId);
281
- // Write to memory
282
- await writeMemoryEntry(dir, agentId, agentInfo?.role ?? 'developer', agentInfo?.tool ?? 'unknown', { topic, content, tags, decided_with });
283
- // Also send CONCLUDE to conversation space
284
- const client = await getOrConnectClient();
285
- if (client?.isConnected) {
286
- client.conclude(topic, content, tags);
287
- }
271
+ const role = agentInfo?.role ?? 'developer';
272
+ const tool = agentInfo?.tool ?? 'unknown';
273
+ // Write to local + cloud in parallel, and send conclusion
274
+ const teamId = getTeamId?.() ?? hub?.team_id;
275
+ const writeOpts = { topic, content, tags, decided_with };
276
+ await Promise.all([
277
+ writeMemoryEntry(dir, agentId, role, tool, writeOpts),
278
+ teamId ? cloudWriteMemory(teamId, agentId, role, tool, writeOpts).catch(() => { }) : Promise.resolve(),
279
+ getOrConnectClient().then(client => { if (client?.isConnected)
280
+ client.conclude(topic, content, tags); }),
281
+ ]);
288
282
  return {
289
283
  content: [{
290
284
  type: 'text',
@@ -326,13 +320,10 @@ Flow:
326
320
  '',
327
321
  recommendation ? `💡 Recommendation: ${recommendation}` : '',
328
322
  ].filter(Boolean).join('\n');
329
- // Generate a unique decision ID
330
323
  const decisionId = `decision-${randomUUID()}`;
331
- // Broadcast proposal to conversation space (so other agents see it)
332
324
  if (client?.isConnected) {
333
325
  client.proposeDecision(decisionId, title, options, recommendation);
334
326
  }
335
- // Ask THIS human via elicitation
336
327
  try {
337
328
  const result = await server.server.elicitInput({
338
329
  mode: 'form',
@@ -369,7 +360,6 @@ Flow:
369
360
  const reason = result.content.reason;
370
361
  if (choice === '__none_of_the_above__') {
371
362
  const userDirection = reason || 'No specific direction given';
372
- // Vote reject and broadcast the reason to the conversation space
373
363
  if (client?.isConnected) {
374
364
  client.vote(decisionId, 'reject', undefined, userDirection);
375
365
  client.sendMessage(title, `❌ Human rejected all proposed options. Feedback: "${userDirection}". Discussion should restart with this new direction.`);
@@ -381,13 +371,11 @@ Flow:
381
371
  }]
382
372
  };
383
373
  }
384
- // Vote approve in the conversation space
385
374
  if (client?.isConnected) {
386
375
  client.vote(decisionId, 'approve', choice, reason);
387
376
  // Wait for other votes
388
- const waitMs = 60000; // wait up to 60s for all votes
377
+ const deadline = Date.now() + 60000;
389
378
  const pollInterval = 3000;
390
- const deadline = Date.now() + waitMs;
391
379
  while (Date.now() < deadline) {
392
380
  await new Promise(resolve => setTimeout(resolve, pollInterval));
393
381
  const resolved = client.resolvedDecisions.find(d => d.decisionId === decisionId);
@@ -411,7 +399,6 @@ Flow:
411
399
  }
412
400
  }
413
401
  }
414
- // Timeout — not all voted yet
415
402
  return {
416
403
  content: [{
417
404
  type: 'text',
@@ -419,7 +406,6 @@ Flow:
419
406
  }]
420
407
  };
421
408
  }
422
- // No conversation space — single user decision
423
409
  return {
424
410
  content: [{
425
411
  type: 'text',
@@ -429,20 +415,13 @@ Flow:
429
415
  }
430
416
  else {
431
417
  return {
432
- content: [{
433
- type: 'text',
434
- text: '⏸️ User declined to vote right now. Continue with other work and come back to this later.',
435
- }]
418
+ content: [{ type: 'text', text: '⏸️ User declined to vote right now. Continue with other work and come back to this later.' }]
436
419
  };
437
420
  }
438
421
  }
439
422
  catch {
440
- // Elicitation not supported — fall back to text
441
423
  return {
442
- content: [{
443
- type: 'text',
444
- text: `❓ Decision needed: ${title}\n\n${message}\n\nPlease tell me which option you prefer.`,
445
- }]
424
+ content: [{ type: 'text', text: `❓ Decision needed: ${title}\n\n${message}\n\nPlease tell me which option you prefer.` }]
446
425
  };
447
426
  }
448
427
  });
@@ -468,73 +447,147 @@ Flow:
468
447
  });
469
448
  server.tool('vote_on_decision', `Vote on a pending team decision that was proposed by another agent.
470
449
 
471
- When you see a DECISION_PROPOSED in the conversation space, use this tool to ask your human
472
- to vote. This shows them the options and sends their vote back to the conversation space.
473
- A decision is only finalized when ALL humans have voted.`, {
450
+ You can vote in two ways:
451
+ 1. If the user already told you their choice, pass it directly via the "choice" parameter
452
+ 2. If you need to ask the user, leave "choice" empty and this tool will try to show a form
453
+
454
+ Always ask the user which option they prefer BEFORE calling this tool, then pass their answer.`, {
474
455
  decisionId: z.string().describe('The decision ID from the proposal'),
475
- topic: z.string().describe('The decision topic (for display)'),
476
- options: z.array(z.object({
477
- label: z.string(),
478
- description: z.string(),
479
- })).describe('The options to vote on'),
480
- recommendation: z.string().optional().describe('The proposer\'s recommendation'),
481
- }, async ({ decisionId, topic, options, recommendation }) => {
456
+ topic: z.string().describe('The decision topic'),
457
+ choice: z.string().optional().describe('The chosen option (e.g. "PostgreSQL"). If provided, votes directly without showing a form.'),
458
+ vote: z.enum(['approve', 'reject']).optional().describe('approve or reject. Default: approve if choice is given.'),
459
+ reason: z.string().optional().describe('Reason for the vote'),
460
+ }, async ({ decisionId, topic, choice, vote, reason }) => {
482
461
  const client = await getOrConnectClient();
483
- const optionLabels = options.map(o => o.label);
484
- const message = [
485
- `Team decision requested: ${topic}`,
486
- '',
487
- 'Options:',
488
- ...options.map(o => `• ${o.label} — ${o.description}`),
489
- '',
490
- recommendation ? `💡 Proposer recommends: ${recommendation}` : '',
491
- ].filter(Boolean).join('\n');
462
+ if (choice) {
463
+ const voteType = (choice === '__none_of_the_above__' || vote === 'reject') ? 'reject' : 'approve';
464
+ client?.vote(decisionId, voteType, voteType === 'approve' ? choice : undefined, reason);
465
+ if (voteType === 'reject') {
466
+ return { content: [{ type: 'text', text: `❌ Vote: rejected. ${reason ? `Reason: ${reason}` : ''}` }] };
467
+ }
468
+ return {
469
+ content: [{ type: 'text', text: `✅ Vote: approved "${choice}". ${reason ? `Reason: ${reason}` : ''}\nWaiting for other team members...` }]
470
+ };
471
+ }
492
472
  try {
493
473
  const result = await server.server.elicitInput({
494
474
  mode: 'form',
495
- message,
475
+ message: `Team decision: ${topic}`,
496
476
  requestedSchema: {
497
477
  type: 'object',
498
478
  properties: {
499
- choice: {
500
- type: 'string',
501
- title: topic,
502
- oneOf: [
503
- ...optionLabels.map(label => ({ const: label, title: label })),
504
- { const: '__none_of_the_above__', title: 'None of the above — reject' },
505
- ],
506
- },
507
- reason: {
508
- type: 'string',
509
- title: 'Reason (optional)',
510
- },
479
+ choice: { type: 'string', title: topic },
480
+ reason: { type: 'string', title: 'Reason (optional)' },
511
481
  },
512
482
  required: ['choice'],
513
483
  },
514
484
  });
515
485
  if (result.action === 'accept' && result.content) {
516
- const choice = result.content.choice;
517
- const reason = result.content.reason;
518
- if (choice === '__none_of_the_above__') {
519
- client?.vote(decisionId, 'reject', undefined, reason || 'Rejected all options');
520
- return {
521
- content: [{ type: 'text', text: `❌ Vote: rejected. ${reason ? `Reason: ${reason}` : ''}` }]
522
- };
523
- }
524
- client?.vote(decisionId, 'approve', choice, reason);
486
+ const userChoice = result.content.choice;
487
+ const userReason = result.content.reason;
488
+ client?.vote(decisionId, 'approve', userChoice, userReason);
525
489
  return {
526
- content: [{ type: 'text', text: `✅ Vote: approved "${choice}". Waiting for other team members...` }]
490
+ content: [{ type: 'text', text: `✅ Vote: approved "${userChoice}". Waiting for other team members...` }]
527
491
  };
528
492
  }
529
493
  return {
530
- content: [{ type: 'text', text: '⏸️ User skipped voting for now.' }]
494
+ content: [{ type: 'text', text: '⏸️ User skipped voting. Ask the user which option they prefer, then call this tool again with their choice.' }]
531
495
  };
532
496
  }
533
497
  catch {
534
498
  return {
535
- content: [{ type: 'text', text: `❓ Vote needed on: ${topic}\n\n${message}\n\nTell me which option you prefer, or say "reject" if none work.` }]
499
+ content: [{ type: 'text', text: `❓ Could not show vote form. Please ask the user which option they prefer for "${topic}", then call vote_on_decision again with their choice.` }]
536
500
  };
537
501
  }
538
502
  });
539
503
  }
504
+ // ── Wait strategies ──────────────────────────────────────────────────────────
505
+ /** How long to extend when someone says "hold on" (5 minutes). */
506
+ const HOLD_ON_EXTENSION_MS = 5 * 60 * 1000;
507
+ /** Patterns that mean "wait for me, I'm coming". */
508
+ const HOLD_ON_PATTERNS = [
509
+ /hold\s*on/i,
510
+ /wait/i,
511
+ /one\s*(moment|minute|sec)/i,
512
+ /coming/i,
513
+ /be\s*right\s*(there|back)/i,
514
+ /brb/i,
515
+ /give\s*me\s*(a\s*)?(moment|minute|sec)/i,
516
+ /等一下/,
517
+ /稍等/,
518
+ /马上/,
519
+ /等等/,
520
+ /来了/,
521
+ /等我/,
522
+ ];
523
+ function isHoldOnMessage(content) {
524
+ return HOLD_ON_PATTERNS.some(p => p.test(content));
525
+ }
526
+ /**
527
+ * Cloud mode: use push events via waitForMessage.
528
+ * If someone says "hold on", automatically extends the deadline.
529
+ */
530
+ async function waitForRepliesCloud(client, topic, myId, waitMs) {
531
+ let deadline = Date.now() + waitMs;
532
+ let gotRealReply = false;
533
+ while (Date.now() < deadline) {
534
+ const remaining = deadline - Date.now();
535
+ if (remaining <= 0)
536
+ break;
537
+ // After getting a real reply, use shorter silence timeout (15s)
538
+ const timeout = gotRealReply ? Math.min(15000, remaining) : remaining;
539
+ try {
540
+ const msg = await client.waitForMessage(topic, timeout);
541
+ if (isHoldOnMessage(msg.content)) {
542
+ // Extend deadline — someone is coming
543
+ deadline = Math.max(deadline, Date.now() + HOLD_ON_EXTENSION_MS);
544
+ // Don't count "hold on" as a real reply for silence detection
545
+ }
546
+ else {
547
+ gotRealReply = true;
548
+ }
549
+ }
550
+ catch {
551
+ // Timeout
552
+ if (gotRealReply)
553
+ break;
554
+ break;
555
+ }
556
+ }
557
+ }
558
+ /**
559
+ * Legacy mode: poll every 3 seconds (for WebSocket ConversationClient).
560
+ * Also supports "hold on" detection.
561
+ */
562
+ async function waitForRepliesPolling(client, topic, myId, beforeSend, waitMs) {
563
+ const pollInterval = 3000;
564
+ let deadline = Date.now() + waitMs;
565
+ let lastCount = 0;
566
+ let silentSince = Date.now();
567
+ while (Date.now() < deadline) {
568
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
569
+ const newMessages = client.getTopicMessages(topic).filter(m => new Date(m.timestamp).getTime() > new Date(beforeSend).getTime()
570
+ && m.from.id !== myId);
571
+ if (newMessages.length > lastCount) {
572
+ const latestNew = newMessages.slice(lastCount);
573
+ lastCount = newMessages.length;
574
+ // Check if any new message is "hold on"
575
+ const hasHoldOn = latestNew.some(m => isHoldOnMessage(m.content));
576
+ if (hasHoldOn) {
577
+ deadline = Math.max(deadline, Date.now() + HOLD_ON_EXTENSION_MS);
578
+ }
579
+ // Check if there's a real (non-hold-on) reply
580
+ const hasRealReply = latestNew.some(m => !isHoldOnMessage(m.content));
581
+ if (hasRealReply) {
582
+ silentSince = Date.now();
583
+ }
584
+ }
585
+ else if (Date.now() - silentSince > 15000 && lastCount > 0) {
586
+ // 15 seconds of silence after real replies — round done
587
+ const hasAnyRealReply = newMessages.some(m => !isHoldOnMessage(m.content));
588
+ if (hasAnyRealReply)
589
+ break;
590
+ }
591
+ }
592
+ }
540
593
  //# sourceMappingURL=conversation-tools.js.map