@hailer/mcp 0.1.6 → 0.1.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.
Files changed (137) hide show
  1. package/.claude/agents/agent-dmitri-activity-crud.md +3 -1
  2. package/.claude/agents/agent-giuseppe-app-builder.md +11 -12
  3. package/.claude/agents/agent-kenji-data-reader.md +5 -3
  4. package/.claude/hooks/sync-marketplace-agents.cjs +117 -56
  5. package/.claude/skills/hailer-app-builder/SKILL.md +506 -0
  6. package/.claude/skills/publish-hailer-app/SKILL.md +169 -0
  7. package/.claude/skills/tool-parameter-usage/SKILL.md +112 -0
  8. package/CHANGELOG.md +20 -0
  9. package/CLAUDE.md +37 -16
  10. package/REFACTOR_STATUS.md +127 -0
  11. package/dist/cli.js +0 -0
  12. package/dist/client/agents/base.d.ts +202 -0
  13. package/dist/client/agents/base.js +737 -0
  14. package/dist/client/agents/definitions.d.ts +53 -0
  15. package/dist/client/agents/definitions.js +178 -0
  16. package/dist/client/agents/orchestrator.d.ts +119 -0
  17. package/dist/client/agents/orchestrator.js +760 -0
  18. package/dist/client/agents/specialist.d.ts +86 -0
  19. package/dist/client/agents/specialist.js +340 -0
  20. package/dist/client/bot-manager.d.ts +44 -0
  21. package/dist/client/bot-manager.js +173 -0
  22. package/dist/client/chat-agent-daemon.d.ts +464 -0
  23. package/dist/client/chat-agent-daemon.js +1774 -0
  24. package/dist/client/daemon-factory.d.ts +106 -0
  25. package/dist/client/daemon-factory.js +301 -0
  26. package/dist/client/factory.d.ts +107 -0
  27. package/dist/client/factory.js +304 -0
  28. package/dist/client/index.d.ts +17 -0
  29. package/dist/client/index.js +38 -0
  30. package/dist/client/multi-bot-manager.d.ts +18 -0
  31. package/dist/client/multi-bot-manager.js +88 -1
  32. package/dist/client/orchestrator-daemon.d.ts +87 -0
  33. package/dist/client/orchestrator-daemon.js +444 -0
  34. package/dist/client/services/agent-registry.d.ts +108 -0
  35. package/dist/client/services/agent-registry.js +630 -0
  36. package/dist/client/services/conversation-manager.d.ts +50 -0
  37. package/dist/client/services/conversation-manager.js +136 -0
  38. package/dist/client/services/mcp-client.d.ts +48 -0
  39. package/dist/client/services/mcp-client.js +105 -0
  40. package/dist/client/services/message-classifier.d.ts +37 -0
  41. package/dist/client/services/message-classifier.js +187 -0
  42. package/dist/client/services/message-formatter.d.ts +84 -0
  43. package/dist/client/services/message-formatter.js +353 -0
  44. package/dist/client/services/session-logger.d.ts +106 -0
  45. package/dist/client/services/session-logger.js +446 -0
  46. package/dist/client/services/tool-executor.d.ts +41 -0
  47. package/dist/client/services/tool-executor.js +169 -0
  48. package/dist/client/services/workspace-schema-cache.d.ts +149 -0
  49. package/dist/client/services/workspace-schema-cache.js +732 -0
  50. package/dist/client/specialist-daemon.d.ts +77 -0
  51. package/dist/client/specialist-daemon.js +197 -0
  52. package/dist/client/specialists.d.ts +53 -0
  53. package/dist/client/specialists.js +178 -0
  54. package/dist/client/tool-schema-loader.d.ts +4 -3
  55. package/dist/client/tool-schema-loader.js +54 -8
  56. package/dist/client/types.d.ts +283 -55
  57. package/dist/client/types.js +113 -2
  58. package/dist/config.d.ts +1 -1
  59. package/dist/config.js +1 -1
  60. package/dist/core.d.ts +10 -2
  61. package/dist/core.js +43 -27
  62. package/dist/lib/logger.js +15 -3
  63. package/dist/mcp/UserContextCache.js +2 -2
  64. package/dist/mcp/hailer-clients.js +5 -5
  65. package/dist/mcp/signal-handler.js +27 -5
  66. package/dist/mcp/tools/activity.js +137 -65
  67. package/dist/mcp/tools/app-core.js +4 -140
  68. package/dist/mcp/tools/app-marketplace.js +15 -260
  69. package/dist/mcp/tools/app-member.js +2 -73
  70. package/dist/mcp/tools/app-scaffold.js +146 -87
  71. package/dist/mcp/tools/discussion.js +348 -73
  72. package/dist/mcp/tools/insight.js +74 -190
  73. package/dist/mcp/tools/workflow.js +20 -94
  74. package/dist/mcp/utils/hailer-api-client.d.ts +4 -2
  75. package/dist/mcp/utils/hailer-api-client.js +24 -10
  76. package/dist/mcp-server.d.ts +4 -0
  77. package/dist/mcp-server.js +24 -4
  78. package/dist/routes/agents.d.ts +44 -0
  79. package/dist/routes/agents.js +311 -0
  80. package/dist/services/agent-credential-store.d.ts +73 -0
  81. package/dist/services/agent-credential-store.js +212 -0
  82. package/lineup-manager/dist/assets/index-8ce6041d.css +1 -0
  83. package/lineup-manager/dist/assets/index-e168f265.js +600 -0
  84. package/lineup-manager/dist/index.html +15 -0
  85. package/lineup-manager/dist/manifest.json +17 -0
  86. package/lineup-manager/dist/vite.svg +1 -0
  87. package/package.json +1 -1
  88. package/dist/client/adaptive-documentation-bot.d.ts +0 -106
  89. package/dist/client/adaptive-documentation-bot.js +0 -464
  90. package/dist/client/adaptive-documentation-types.d.ts +0 -66
  91. package/dist/client/adaptive-documentation-types.js +0 -9
  92. package/dist/client/agent-activity-bot.d.ts +0 -51
  93. package/dist/client/agent-activity-bot.js +0 -166
  94. package/dist/client/agent-tracker.d.ts +0 -499
  95. package/dist/client/agent-tracker.js +0 -659
  96. package/dist/client/description-updater.d.ts +0 -56
  97. package/dist/client/description-updater.js +0 -259
  98. package/dist/client/log-parser.d.ts +0 -72
  99. package/dist/client/log-parser.js +0 -387
  100. package/dist/client/mcp-assistant.d.ts +0 -21
  101. package/dist/client/mcp-assistant.js +0 -58
  102. package/dist/client/mcp-client.d.ts +0 -50
  103. package/dist/client/mcp-client.js +0 -538
  104. package/dist/client/message-processor.d.ts +0 -35
  105. package/dist/client/message-processor.js +0 -357
  106. package/dist/client/providers/anthropic-provider.d.ts +0 -19
  107. package/dist/client/providers/anthropic-provider.js +0 -645
  108. package/dist/client/providers/assistant-provider.d.ts +0 -17
  109. package/dist/client/providers/assistant-provider.js +0 -51
  110. package/dist/client/providers/llm-provider.d.ts +0 -47
  111. package/dist/client/providers/llm-provider.js +0 -367
  112. package/dist/client/providers/openai-provider.d.ts +0 -23
  113. package/dist/client/providers/openai-provider.js +0 -630
  114. package/dist/client/simple-llm-caller.d.ts +0 -19
  115. package/dist/client/simple-llm-caller.js +0 -100
  116. package/dist/client/skill-generator.d.ts +0 -81
  117. package/dist/client/skill-generator.js +0 -386
  118. package/dist/client/test-adaptive-bot.d.ts +0 -9
  119. package/dist/client/test-adaptive-bot.js +0 -82
  120. package/dist/client/token-pricing.d.ts +0 -38
  121. package/dist/client/token-pricing.js +0 -127
  122. package/dist/client/token-tracker.d.ts +0 -232
  123. package/dist/client/token-tracker.js +0 -457
  124. package/dist/client/token-usage-bot.d.ts +0 -53
  125. package/dist/client/token-usage-bot.js +0 -153
  126. package/dist/client/tool-executor.d.ts +0 -69
  127. package/dist/client/tool-executor.js +0 -159
  128. package/dist/lib/materialize.d.ts +0 -3
  129. package/dist/lib/materialize.js +0 -101
  130. package/dist/lib/normalizedName.d.ts +0 -7
  131. package/dist/lib/normalizedName.js +0 -48
  132. package/dist/lib/terminal-prompt.d.ts +0 -9
  133. package/dist/lib/terminal-prompt.js +0 -108
  134. package/dist/mcp/tools/skill.d.ts +0 -10
  135. package/dist/mcp/tools/skill.js +0 -279
  136. package/dist/mcp/tools/workflow-template.d.ts +0 -19
  137. package/dist/mcp/tools/workflow-template.js +0 -822
@@ -102,21 +102,29 @@ exports.listMyDiscussionsTool = {
102
102
  .map((d) => d.linked_activity);
103
103
  // Fetch activity names in parallel for better UX
104
104
  const activityNames = {};
105
+ const activityErrors = [];
105
106
  if (activityIds.length > 0) {
106
107
  const activityPromises = activityIds.map(async (activityId) => {
107
108
  try {
108
109
  const activity = await context.hailer.fetchActivityById(activityId);
109
- return { id: activityId, name: activity?.name || null };
110
+ return { id: activityId, name: activity?.name || null, error: null };
110
111
  }
111
112
  catch (error) {
112
- logger.debug('Failed to fetch activity name', { activityId, error });
113
- return { id: activityId, name: null };
113
+ const errorMsg = error instanceof Error ? error.message :
114
+ (typeof error === 'object' && error !== null && 'msg' in error) ? error.msg :
115
+ String(error);
116
+ logger.debug('Failed to fetch activity name', { activityId, error: errorMsg });
117
+ return { id: activityId, name: null, error: errorMsg };
114
118
  }
115
119
  });
116
120
  const results = await Promise.all(activityPromises);
117
- results.forEach(({ id, name }) => {
118
- if (name)
121
+ results.forEach(({ id, name, error }) => {
122
+ if (name) {
119
123
  activityNames[id] = name;
124
+ }
125
+ else if (error) {
126
+ activityErrors.push({ id, error });
127
+ }
120
128
  });
121
129
  }
122
130
  const discussionsByType = {
@@ -191,6 +199,14 @@ exports.listMyDiscussionsTool = {
191
199
  }
192
200
  }
193
201
  responseText += `- Total unread: ${totalUnread}\n`;
202
+ // Add warnings section if there were errors loading activities
203
+ if (activityErrors.length > 0) {
204
+ responseText += `\nāš ļø **WARNINGS** (${activityErrors.length} activity/activities failed to load):\n`;
205
+ activityErrors.forEach(({ id, error }) => {
206
+ responseText += `- Activity \`${id}\`: ${error}\n`;
207
+ });
208
+ responseText += `\nšŸ’” These activities may have broken function fields or permission issues.\n`;
209
+ }
194
210
  responseText += `\nšŸ’” **USAGE:**\n`;
195
211
  responseText += `- Use \`fetch_discussion_messages\` with discussion ID to read messages\n`;
196
212
  responseText += `- Use \`add_discussion_message\` with discussion ID to post a message\n`;
@@ -241,13 +257,46 @@ exports.fetchDiscussionMessagesTool = {
241
257
  }),
242
258
  async execute(args, context) {
243
259
  const limit = args.limit || 50;
260
+ let discussionId = args.discussionId;
244
261
  logger.debug('Fetching discussion messages', {
245
- discussionId: args.discussionId,
262
+ discussionId: discussionId,
246
263
  limit: limit,
247
264
  apiKey: context.apiKey.substring(0, 8) + '...'
248
265
  });
249
266
  try {
250
- const result = await context.hailer.fetchDiscussionMessages(args.discussionId, limit);
267
+ // Auto-detect: if this is an activity ID, get its discussion ID
268
+ // Try fetching as discussion first, if it fails, check if it's an activity
269
+ let result;
270
+ try {
271
+ result = await context.hailer.fetchDiscussionMessages(discussionId, limit);
272
+ }
273
+ catch (firstError) {
274
+ // If first attempt fails, try loading as activity to get discussion ID
275
+ logger.debug('First fetch failed, checking if this is an activity ID', {
276
+ providedId: discussionId,
277
+ error: firstError?.msg || firstError?.message
278
+ });
279
+ try {
280
+ const activity = await context.hailer.fetchActivityById(discussionId);
281
+ if (activity?.discussion) {
282
+ logger.info('Auto-converted activity ID to discussion ID', {
283
+ activityId: discussionId,
284
+ discussionId: activity.discussion,
285
+ activityName: activity.name
286
+ });
287
+ discussionId = activity.discussion;
288
+ result = await context.hailer.fetchDiscussionMessages(discussionId, limit);
289
+ }
290
+ else {
291
+ // Not an activity or no discussion linked, rethrow original error
292
+ throw firstError;
293
+ }
294
+ }
295
+ catch (activityError) {
296
+ // Activity lookup failed too, rethrow original error
297
+ throw firstError;
298
+ }
299
+ }
251
300
  const messages = result.messages || [];
252
301
  const messageCount = messages.length;
253
302
  const userMessages = messages.filter((m) => m.type === "user");
@@ -266,6 +315,7 @@ exports.fetchDiscussionMessagesTool = {
266
315
  }
267
316
  const optimizedMsg = {
268
317
  _id: msg._id,
318
+ uid: msg.uid, // Include user ID for conversation history
269
319
  username: username,
270
320
  created: new Date(msg.created).toLocaleString(),
271
321
  type: msg.type,
@@ -394,6 +444,7 @@ exports.fetchPreviousDiscussionMessagesTool = {
394
444
  }
395
445
  const optimizedMsg = {
396
446
  _id: msg._id,
447
+ uid: msg.uid, // Include user ID for conversation history
397
448
  username: username,
398
449
  created: new Date(msg.created).toLocaleString(),
399
450
  msg: cleanMsg
@@ -454,7 +505,18 @@ exports.fetchPreviousDiscussionMessagesTool = {
454
505
  // ============================================================================
455
506
  // TOOL 4: JOIN DISCUSSION
456
507
  // ============================================================================
457
- const joinDiscussionDescription = `Join a discussion to gain access to read and post messages - Use this for activity discussions (provide activityId) or general discussions (provide discussionId)`;
508
+ const joinDiscussionDescription = `<purpose>Join a discussion (bot joins) and optionally invite a user</purpose>
509
+ <when-to-use>
510
+ <case>Activity discussions: provide activityId</case>
511
+ <case>General discussions: provide discussionId</case>
512
+ </when-to-use>
513
+ <critical>
514
+ When user says "invite me" or asks to be added to a discussion:
515
+ - Extract user_id from the incoming message → inviteUserId
516
+ - Extract source activity from current discussion → sourceActivityId (for wormhole link back)
517
+ - Provide context explaining WHY → welcomeReason
518
+ - Bot joins FIRST, then invites user, then posts welcome message with wormhole
519
+ </critical>`;
458
520
  exports.joinDiscussionTool = {
459
521
  name: 'join_discussion',
460
522
  group: tool_registry_1.ToolGroup.WRITE,
@@ -469,7 +531,21 @@ exports.joinDiscussionTool = {
469
531
  .string()
470
532
  .min(24, "Discussion ID must be at least 24 characters")
471
533
  .optional()
472
- .describe("Discussion ID to join directly (use this for non-activity discussions)")
534
+ .describe("Discussion ID to join directly (use this for non-activity discussions)"),
535
+ inviteUserId: zod_1.z
536
+ .string()
537
+ .min(24, "User ID must be at least 24 characters")
538
+ .optional()
539
+ .describe("User ID to invite. Get from incoming message user_id attribute when user asks to be invited."),
540
+ welcomeReason: zod_1.z
541
+ .string()
542
+ .optional()
543
+ .describe("Context/reason for the invite - posted as welcome message in the discussion."),
544
+ sourceActivityId: zod_1.z
545
+ .string()
546
+ .min(24)
547
+ .optional()
548
+ .describe("IMPORTANT: The activity_id from the CURRENT discussion where the request was made. This creates a 'came from' link in the welcome message. Get this from the incoming message's activity_id attribute.")
473
549
  }).refine((data) => data.activityId || data.discussionId, {
474
550
  message: "Either activityId or discussionId must be provided"
475
551
  }),
@@ -477,57 +553,207 @@ exports.joinDiscussionTool = {
477
553
  logger.debug('Joining discussion', {
478
554
  activityId: args.activityId,
479
555
  discussionId: args.discussionId,
556
+ inviteUserId: args.inviteUserId,
480
557
  apiKey: context.apiKey.substring(0, 8) + '...'
481
558
  });
482
559
  try {
560
+ let discussionId;
561
+ let activityName;
562
+ let resolvedActivityId;
483
563
  if (args.activityId) {
484
564
  const result = await context.hailer.joinActivityDiscussion(args.activityId);
485
565
  const activity = await context.hailer.fetchActivityById(args.activityId);
486
- const discussionId = activity.discussion;
487
- return {
488
- content: [
489
- {
490
- type: "text",
491
- text: `āœ… Successfully joined activity discussion!\n\n` +
492
- `šŸ“‹ **Activity**: ${activity.name || args.activityId}\n` +
493
- `šŸ’¬ **Discussion ID**: ${discussionId}\n` +
494
- `šŸ“Š **Status**: ${result ? 'Now following this activity' : 'Already following'}\n\n` +
495
- `šŸŽ‰ You can now:\n` +
496
- `- Read messages with: fetch_discussion_messages(discussionId: "${discussionId}")\n` +
497
- `- Post messages with: add_discussion_message(discussionId: "${discussionId}", content: "your message")\n` +
498
- `- Leave with: leave_discussion(activityId: "${args.activityId}")\n\n` +
499
- `šŸ”— **Direct Links:**\n` +
500
- `- Activity: https://app.hailer.com/#/activities/${args.activityId}\n` +
501
- `- Discussion: https://app.hailer.com/#/discussions/${discussionId}`,
502
- },
503
- ],
504
- };
566
+ discussionId = activity.discussion;
567
+ activityName = activity.name;
568
+ resolvedActivityId = args.activityId;
569
+ logger.debug('Joined activity discussion', {
570
+ activityId: args.activityId,
571
+ discussionId,
572
+ activityName,
573
+ followResult: result
574
+ });
505
575
  }
506
- if (args.discussionId) {
507
- const result = await context.hailer.joinDiscussion(args.discussionId);
576
+ else if (args.discussionId) {
577
+ // Try joining as discussion first
578
+ try {
579
+ await context.hailer.joinDiscussion(args.discussionId);
580
+ discussionId = args.discussionId;
581
+ logger.debug('Joined discussion directly', {
582
+ discussionId
583
+ });
584
+ }
585
+ catch (directJoinError) {
586
+ // If direct join fails, try treating the ID as an activity ID
587
+ logger.debug('Direct discussion join failed, trying as activity ID', {
588
+ providedId: args.discussionId,
589
+ error: directJoinError?.msg || directJoinError?.message
590
+ });
591
+ try {
592
+ const activity = await context.hailer.fetchActivityById(args.discussionId);
593
+ if (activity?.discussion) {
594
+ // It's an activity! Join its discussion
595
+ await context.hailer.joinActivityDiscussion(args.discussionId);
596
+ discussionId = activity.discussion;
597
+ activityName = activity.name;
598
+ resolvedActivityId = args.discussionId;
599
+ logger.info('Auto-resolved activity ID to discussion', {
600
+ activityId: args.discussionId,
601
+ discussionId,
602
+ activityName
603
+ });
604
+ }
605
+ else {
606
+ // Not an activity either, rethrow original error
607
+ throw directJoinError;
608
+ }
609
+ }
610
+ catch (activityError) {
611
+ // Activity lookup also failed, rethrow original error
612
+ logger.debug('Activity lookup also failed', {
613
+ error: activityError?.msg || activityError?.message
614
+ });
615
+ throw directJoinError;
616
+ }
617
+ }
618
+ }
619
+ else {
508
620
  return {
509
- content: [
510
- {
621
+ content: [{
511
622
  type: "text",
512
- text: `āœ… ${result.message}\n\n` +
513
- `šŸ’¬ **Discussion ID**: ${args.discussionId}\n` +
514
- `šŸ“Š **Method**: ${result.method}\n\n` +
515
- `šŸŽ‰ You can now:\n` +
516
- `- Read messages with: fetch_discussion_messages(discussionId: "${args.discussionId}")\n` +
517
- `- Post messages with: add_discussion_message(discussionId: "${args.discussionId}", content: "your message")\n\n` +
518
- `šŸ’” **Note**: For activity discussions, prefer using activityId for better integration.\n\n` +
519
- `šŸ”— **Direct Link:** https://app.hailer.com/#/discussions/${args.discussionId}`,
520
- },
521
- ],
623
+ text: `āŒ Error: Either activityId or discussionId must be provided`,
624
+ }],
522
625
  };
523
626
  }
627
+ // Invite the requesting user if provided
628
+ let userInvited = false;
629
+ let userAlreadyMember = false;
630
+ let invitedUserName = '';
631
+ if (args.inviteUserId) {
632
+ try {
633
+ // Get user name first
634
+ const userInfo = (0, workspace_cache_1.getUserById)(context.workspaceCache, args.inviteUserId);
635
+ invitedUserName = userInfo
636
+ ? `${userInfo.firstname || ''} ${userInfo.lastname || ''}`.trim() || userInfo.fullName || args.inviteUserId
637
+ : args.inviteUserId;
638
+ // Check if user is already a member
639
+ const syncResponse = await context.hailer.request('messenger.load_discussions', [
640
+ [discussionId]
641
+ ]);
642
+ const discussion = syncResponse?.[discussionId];
643
+ const existingMembers = new Set(discussion?.participants || []);
644
+ if (existingMembers.has(args.inviteUserId)) {
645
+ userAlreadyMember = true;
646
+ logger.debug('User already a member, skipping invite', {
647
+ discussionId,
648
+ userId: args.inviteUserId,
649
+ userName: invitedUserName
650
+ });
651
+ }
652
+ else {
653
+ logger.debug('Inviting user to discussion', {
654
+ discussionId,
655
+ userId: args.inviteUserId
656
+ });
657
+ await context.hailer.request('messenger.invite_users', [
658
+ discussionId,
659
+ [args.inviteUserId]
660
+ ]);
661
+ userInvited = true;
662
+ logger.debug('User invited successfully', {
663
+ discussionId,
664
+ userId: args.inviteUserId,
665
+ userName: invitedUserName
666
+ });
667
+ }
668
+ // Post welcome message if reason provided
669
+ if (args.welcomeReason) {
670
+ try {
671
+ // Format user tag properly: [hailerTag|Name](userId) with ZWNBSP
672
+ const ZWNBSP = '\uFEFF';
673
+ const userTag = `${ZWNBSP}[hailerTag|${invitedUserName}](${args.inviteUserId})${ZWNBSP}`;
674
+ // Format source activity tag (where request came from)
675
+ // IMPORTANT: Ignore sourceActivityId if it equals the destination activityId
676
+ // (LLM sometimes confuses source/destination)
677
+ let sourceTag = '';
678
+ const validSourceId = args.sourceActivityId && args.sourceActivityId !== args.activityId
679
+ ? args.sourceActivityId
680
+ : null;
681
+ if (validSourceId) {
682
+ try {
683
+ const sourceActivity = await context.hailer.fetchActivityById(validSourceId);
684
+ const sourceActivityName = sourceActivity?.name || 'Unknown';
685
+ sourceTag = `${ZWNBSP}[hailerTag|${sourceActivityName}](${validSourceId})${ZWNBSP}`;
686
+ }
687
+ catch {
688
+ sourceTag = `activity ${validSourceId}`;
689
+ }
690
+ }
691
+ // Build welcome message - focus on WHERE they came from
692
+ let welcomeMsg;
693
+ if (sourceTag) {
694
+ welcomeMsg = `šŸ‘‹ ${userTag} joined from ${sourceTag}\n\n**Context:** ${args.welcomeReason}`;
695
+ }
696
+ else {
697
+ welcomeMsg = `šŸ‘‹ ${userTag} was added to this discussion.\n\n**Context:** ${args.welcomeReason}`;
698
+ }
699
+ // Build links metadata for tags
700
+ const links = [
701
+ { target: args.inviteUserId, targetType: 'user', type: 'linked-to' }
702
+ ];
703
+ if (args.activityId) {
704
+ links.push({ target: args.activityId, targetType: 'activity', type: 'linked-to' });
705
+ }
706
+ if (validSourceId) {
707
+ links.push({ target: validSourceId, targetType: 'activity', type: 'linked-to' });
708
+ }
709
+ await context.hailer.request('messenger.send', [
710
+ { msg: welcomeMsg, links },
711
+ discussionId
712
+ ]);
713
+ logger.debug('Welcome message posted', { discussionId, hadTags: true, hasWormhole: !!validSourceId });
714
+ }
715
+ catch (welcomeError) {
716
+ logger.warn('Failed to post welcome message', {
717
+ discussionId,
718
+ error: welcomeError instanceof Error ? welcomeError.message : String(welcomeError)
719
+ });
720
+ }
721
+ }
722
+ }
723
+ catch (inviteError) {
724
+ logger.warn('Failed to invite user', {
725
+ discussionId,
726
+ userId: args.inviteUserId,
727
+ error: inviteError instanceof Error ? inviteError.message : String(inviteError)
728
+ });
729
+ }
730
+ }
731
+ // Build response
732
+ let responseText = `āœ… Successfully joined discussion!\n\n`;
733
+ if (activityName) {
734
+ responseText += `šŸ“‹ **Activity**: ${activityName}\n`;
735
+ }
736
+ responseText += `šŸ’¬ **Discussion ID**: ${discussionId}\n`;
737
+ if (userInvited) {
738
+ responseText += `šŸ‘¤ **User Invited**: ${invitedUserName} āœ…\n`;
739
+ }
740
+ else if (userAlreadyMember) {
741
+ responseText += `šŸ‘¤ **User**: ${invitedUserName} (already a member)\n`;
742
+ }
743
+ responseText += `\nšŸŽ‰ You can now:\n`;
744
+ responseText += `- Read messages with: fetch_discussion_messages(discussionId: "${discussionId}")\n`;
745
+ responseText += `- Post messages with: add_discussion_message(discussionId: "${discussionId}", content: "your message")\n`;
746
+ if (resolvedActivityId) {
747
+ responseText += `- Leave with: leave_discussion(activityId: "${resolvedActivityId}")\n`;
748
+ responseText += `\nšŸ”— **Direct Links:**\n`;
749
+ responseText += `- Activity: https://app.hailer.com/#/activities/${resolvedActivityId}\n`;
750
+ }
751
+ responseText += `- Discussion: https://app.hailer.com/#/discussions/${discussionId}`;
524
752
  return {
525
- content: [
526
- {
753
+ content: [{
527
754
  type: "text",
528
- text: `āŒ Error: Either activityId or discussionId must be provided`,
529
- },
530
- ],
755
+ text: responseText,
756
+ }],
531
757
  };
532
758
  }
533
759
  catch (error) {
@@ -549,7 +775,8 @@ exports.joinDiscussionTool = {
549
775
  `- Some discussions may be private or require an invitation\n\n` +
550
776
  `šŸ“– Examples:\n` +
551
777
  `- Activity discussion: join_discussion(activityId: "68446dc05b30685f67c6fcd4")\n` +
552
- `- General discussion: join_discussion(discussionId: "683ef53087e3d8329abaa3ad")`,
778
+ `- General discussion: join_discussion(discussionId: "683ef53087e3d8329abaa3ad")\n` +
779
+ `- With user invite: join_discussion(activityId: "...", inviteUserId: "user-id-from-context")`,
553
780
  },
554
781
  ],
555
782
  };
@@ -725,35 +952,83 @@ exports.inviteDiscussionMembersTool = {
725
952
  apiKey: context.apiKey.substring(0, 8) + '...'
726
953
  });
727
954
  try {
728
- // Use messenger.invite_users endpoint
729
- const result = await context.hailer.request('messenger.invite_users', [
730
- args.discussionId,
731
- args.userIds
955
+ // First, check who is already a member
956
+ const syncResponse = await context.hailer.request('messenger.load_discussions', [
957
+ [args.discussionId]
732
958
  ]);
733
- logger.debug('Invite result', { result });
734
- // Get usernames for better feedback
735
- const invitedUsers = args.userIds.map((userId) => {
959
+ const discussion = syncResponse?.[args.discussionId];
960
+ // Block invites to private chats - this breaks Hailer
961
+ if (discussion?.private === true) {
962
+ return {
963
+ content: [{
964
+ type: "text",
965
+ text: `āŒ Cannot invite members to a private chat.\n\n` +
966
+ `Private chats are 1-on-1 conversations and cannot have additional members.\n` +
967
+ `šŸ’” To include more people, create a group discussion instead.`
968
+ }]
969
+ };
970
+ }
971
+ const existingMembers = new Set(discussion?.participants || []);
972
+ // Filter out users who are already members
973
+ const alreadyMembers = [];
974
+ const newUsers = [];
975
+ for (const userId of args.userIds) {
976
+ if (existingMembers.has(userId)) {
977
+ alreadyMembers.push(userId);
978
+ }
979
+ else {
980
+ newUsers.push(userId);
981
+ }
982
+ }
983
+ logger.debug('Membership check', {
984
+ discussionId: args.discussionId,
985
+ existingMemberCount: existingMembers.size,
986
+ alreadyMembers: alreadyMembers.length,
987
+ newUsers: newUsers.length
988
+ });
989
+ // Helper to get user display name
990
+ const getUserName = (userId) => {
736
991
  const userInfo = (0, workspace_cache_1.getUserById)(context.workspaceCache, userId);
737
- const fullName = userInfo
738
- ? `${userInfo.firstname || ''} ${userInfo.lastname || ''}`.trim() || userInfo.fullName
992
+ return userInfo
993
+ ? `${userInfo.firstname || ''} ${userInfo.lastname || ''}`.trim() || userInfo.fullName || userId
739
994
  : userId;
740
- return `- ${fullName} (${userId})`;
741
- }).join('\n');
995
+ };
996
+ // If all users are already members, return early
997
+ if (newUsers.length === 0) {
998
+ const alreadyMembersList = alreadyMembers.map((userId) => `- ${getUserName(userId)} (${userId})`).join('\n');
999
+ return {
1000
+ content: [{
1001
+ type: "text",
1002
+ text: `ā„¹ļø All users are already members of this discussion!\n\n` +
1003
+ `šŸ’¬ **Discussion ID**: ${args.discussionId}\n` +
1004
+ `šŸ‘„ **Already Members**:\n${alreadyMembersList}\n\n` +
1005
+ `šŸ’” No invites were sent - these users can already participate.`
1006
+ }]
1007
+ };
1008
+ }
1009
+ // Invite only the new users
1010
+ const result = await context.hailer.request('messenger.invite_users', [
1011
+ args.discussionId,
1012
+ newUsers
1013
+ ]);
1014
+ logger.debug('Invite result', { result, invitedCount: newUsers.length });
1015
+ // Build response
1016
+ const invitedUsersList = newUsers.map((userId) => `- ${getUserName(userId)} (${userId})`).join('\n');
1017
+ let responseText = `āœ… Successfully invited ${newUsers.length} member(s) to discussion!\n\n` +
1018
+ `šŸ’¬ **Discussion ID**: ${args.discussionId}\n` +
1019
+ `šŸ‘„ **Invited Members**:\n${invitedUsersList}\n`;
1020
+ // Note if some were already members
1021
+ if (alreadyMembers.length > 0) {
1022
+ const alreadyMembersList = alreadyMembers.map((userId) => `- ${getUserName(userId)}`).join('\n');
1023
+ responseText += `\nā„¹ļø **Already Members** (skipped):\n${alreadyMembersList}\n`;
1024
+ }
1025
+ responseText += `\nšŸŽ‰ The invited members can now:\n` +
1026
+ `- See all messages in the discussion\n` +
1027
+ `- Post messages and participate\n` +
1028
+ `- Receive notifications for new activity\n\n` +
1029
+ `šŸ”— **Discussion Link:** https://app.hailer.com/#/discussions/${args.discussionId}`;
742
1030
  return {
743
- content: [
744
- {
745
- type: "text",
746
- text: `āœ… Successfully invited ${args.userIds.length} member(s) to discussion!\n\n` +
747
- `šŸ’¬ **Discussion ID**: ${args.discussionId}\n` +
748
- `šŸ‘„ **Invited Members**:\n${invitedUsers}\n\n` +
749
- `šŸŽ‰ The invited members can now:\n` +
750
- `- See all messages in the discussion\n` +
751
- `- Post messages and participate\n` +
752
- `- Receive notifications for new activity\n\n` +
753
- `šŸ’” **Use Case**: Perfect for bringing specialized bots or team members into conversations!\n\n` +
754
- `šŸ”— **Discussion Link:** https://app.hailer.com/#/discussions/${args.discussionId}`,
755
- },
756
- ],
1031
+ content: [{ type: "text", text: responseText }],
757
1032
  };
758
1033
  }
759
1034
  catch (error) {