@link-assistant/hive-mind 1.4.0 → 1.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2d41edb: Add /accept_invites command to Telegram bot for automatically accepting GitHub repository and organization invitations via gh CLI
8
+
3
9
  ## 1.4.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Telegram /accept_invites command implementation
3
+ *
4
+ * This module provides the /accept_invites command functionality for the Telegram bot,
5
+ * allowing users to accept all pending GitHub repository and organization invitations.
6
+ *
7
+ * Features:
8
+ * - Accepts all pending repository invitations
9
+ * - Accepts all pending organization invitations
10
+ * - Provides detailed feedback on accepted invitations
11
+ * - Error handling with detailed error messages
12
+ *
13
+ * @see https://docs.github.com/en/rest/collaborators/invitations
14
+ * @see https://docs.github.com/en/rest/orgs/members
15
+ */
16
+
17
+ import { promisify } from 'util';
18
+ import { exec as execCallback } from 'child_process';
19
+
20
+ const exec = promisify(execCallback);
21
+
22
+ /**
23
+ * Escapes special characters in text for Telegram Markdown formatting
24
+ * @param {string} text - The text to escape
25
+ * @returns {string} The escaped text
26
+ */
27
+ function escapeMarkdown(text) {
28
+ return String(text).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
29
+ }
30
+
31
+ /**
32
+ * Registers the /accept_invites command handler with the bot
33
+ * @param {Object} bot - The Telegraf bot instance
34
+ * @param {Object} options - Options object
35
+ * @param {boolean} options.VERBOSE - Whether to enable verbose logging
36
+ * @param {Function} options.isOldMessage - Function to check if message is old
37
+ * @param {Function} options.isForwardedOrReply - Function to check if message is forwarded/reply
38
+ * @param {Function} options.isGroupChat - Function to check if chat is a group
39
+ * @param {Function} options.isChatAuthorized - Function to check if chat is authorized
40
+ * @param {Function} options.addBreadcrumb - Function to add breadcrumbs for monitoring
41
+ */
42
+ export function registerAcceptInvitesCommand(bot, options) {
43
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb } = options;
44
+
45
+ bot.command(/^accept[_-]?invites$/i, async ctx => {
46
+ VERBOSE && console.log('[VERBOSE] /accept-invites command received');
47
+ await addBreadcrumb({
48
+ category: 'telegram.command',
49
+ message: '/accept-invites command received',
50
+ level: 'info',
51
+ data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
52
+ });
53
+ if (isOldMessage(ctx) || isForwardedOrReply(ctx)) return;
54
+ if (!isGroupChat(ctx))
55
+ return await ctx.reply('❌ The /accept_invites command only works in group chats. Please add this bot to a group and make it an admin.', {
56
+ reply_to_message_id: ctx.message.message_id,
57
+ });
58
+ const chatId = ctx.chat.id;
59
+ if (!isChatAuthorized(chatId))
60
+ return await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, {
61
+ reply_to_message_id: ctx.message.message_id,
62
+ });
63
+
64
+ const fetchingMessage = await ctx.reply('🔄 Fetching pending GitHub invitations...', { reply_to_message_id: ctx.message.message_id });
65
+ const accepted = [];
66
+ const errors = [];
67
+
68
+ try {
69
+ // Fetch repository invitations
70
+ const { stdout: repoInvJson } = await exec('gh api /user/repository_invitations 2>/dev/null || echo "[]"');
71
+ const repoInvitations = JSON.parse(repoInvJson.trim() || '[]');
72
+ VERBOSE && console.log(`[VERBOSE] Found ${repoInvitations.length} pending repo invitations`);
73
+
74
+ // Accept each repo invitation
75
+ for (const inv of repoInvitations) {
76
+ const repoName = inv.repository?.full_name || 'unknown';
77
+ try {
78
+ await exec(`gh api -X PATCH /user/repository_invitations/${inv.id}`);
79
+ accepted.push(`📦 Repository: ${repoName}`);
80
+ VERBOSE && console.log(`[VERBOSE] Accepted repo invitation: ${repoName}`);
81
+ } catch (e) {
82
+ errors.push(`📦 ${repoName}: ${e.message}`);
83
+ VERBOSE && console.log(`[VERBOSE] Failed to accept repo invitation ${repoName}: ${e.message}`);
84
+ }
85
+ }
86
+
87
+ // Fetch organization invitations
88
+ const { stdout: orgMemJson } = await exec('gh api /user/memberships/orgs 2>/dev/null || echo "[]"');
89
+ const orgMemberships = JSON.parse(orgMemJson.trim() || '[]');
90
+ const pendingOrgs = orgMemberships.filter(m => m.state === 'pending');
91
+ VERBOSE && console.log(`[VERBOSE] Found ${pendingOrgs.length} pending org invitations`);
92
+
93
+ // Accept each org invitation
94
+ for (const membership of pendingOrgs) {
95
+ const orgName = membership.organization?.login || 'unknown';
96
+ try {
97
+ await exec(`gh api -X PATCH /user/memberships/orgs/${orgName} -f state=active`);
98
+ accepted.push(`🏢 Organization: ${orgName}`);
99
+ VERBOSE && console.log(`[VERBOSE] Accepted org invitation: ${orgName}`);
100
+ } catch (e) {
101
+ errors.push(`🏢 ${orgName}: ${e.message}`);
102
+ VERBOSE && console.log(`[VERBOSE] Failed to accept org invitation ${orgName}: ${e.message}`);
103
+ }
104
+ }
105
+
106
+ // Build response message
107
+ let message = '✅ *GitHub Invitations Processed*\n\n';
108
+ if (accepted.length === 0 && errors.length === 0) {
109
+ message += 'No pending invitations found.';
110
+ } else {
111
+ if (accepted.length > 0) {
112
+ message += '*Accepted:*\n' + accepted.map(a => ` • ${escapeMarkdown(a)}`).join('\n') + '\n\n';
113
+ }
114
+ if (errors.length > 0) {
115
+ message += '*Errors:*\n' + errors.map(e => ` • ${escapeMarkdown(e)}`).join('\n');
116
+ }
117
+ if (accepted.length > 0 && errors.length === 0) {
118
+ message += `\n🎉 Successfully accepted ${accepted.length} invitation(s)!`;
119
+ }
120
+ }
121
+
122
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
123
+ } catch (error) {
124
+ console.error('Error in /accept-invites:', error);
125
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ Error fetching invitations: ${escapeMarkdown(error.message)}\n\nMake sure \`gh\` CLI is installed and authenticated.`, { parse_mode: 'Markdown' });
126
+ }
127
+ });
128
+ }
@@ -761,8 +761,9 @@ bot.command('help', async ctx => {
761
761
 
762
762
  message += '*/limits* - Show usage limits\n';
763
763
  message += '*/version* - Show bot and runtime versions\n';
764
+ message += '*/accept\\_invites* - Accept all pending GitHub invitations\n';
764
765
  message += '*/help* - Show this help message\n\n';
765
- message += '⚠️ *Note:* /solve, /hive, /limits and /version commands only work in group chats.\n\n';
766
+ message += '⚠️ *Note:* /solve, /hive, /limits, /version and /accept\\_invites commands only work in group chats.\n\n';
766
767
  message += '🔧 *Common Options:*\n';
767
768
  message += '• `--model <model>` or `-m` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
768
769
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
@@ -883,6 +884,19 @@ bot.command('version', async ctx => {
883
884
  if (!result.success) return await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2' });
884
885
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, '🤖 *Version Information*\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
885
886
  });
887
+
888
+ // Register /accept_invites command from separate module
889
+ // This keeps telegram-bot.mjs under the 1500 line limit
890
+ const { registerAcceptInvitesCommand } = await import('./telegram-accept-invitations.lib.mjs');
891
+ registerAcceptInvitesCommand(bot, {
892
+ VERBOSE,
893
+ isOldMessage,
894
+ isForwardedOrReply,
895
+ isGroupChat,
896
+ isChatAuthorized,
897
+ addBreadcrumb,
898
+ });
899
+
886
900
  bot.command(/^solve$/i, async ctx => {
887
901
  if (VERBOSE) {
888
902
  console.log('[VERBOSE] /solve command received');