@link-assistant/hive-mind 1.9.0 → 1.9.1

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,19 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 06da02c: Improve /accept_invites command output with grouped items and real-time updates
8
+
9
+ **Changes:**
10
+ - Group output by "Repositories:" and "Organizations:" instead of repeating "Repository:" for each item
11
+ - Add clickable GitHub links for each repository and organization
12
+ - Implement real-time message updates after each invitation is processed
13
+ - Show progress indicator (e.g., "Processing GitHub Invitations (3/10)") during processing
14
+
15
+ Fixes #1148
16
+
3
17
  ## 1.9.0
4
18
 
5
19
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -7,7 +7,9 @@
7
7
  * Features:
8
8
  * - Accepts all pending repository invitations
9
9
  * - Accepts all pending organization invitations
10
- * - Provides detailed feedback on accepted invitations
10
+ * - Groups output by Repositories and Organizations
11
+ * - Provides clickable links to repositories and organizations
12
+ * - Real-time progress updates during processing
11
13
  * - Error handling with detailed error messages
12
14
  *
13
15
  * @see https://docs.github.com/en/rest/collaborators/invitations
@@ -28,6 +30,83 @@ function escapeMarkdown(text) {
28
30
  return String(text).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
29
31
  }
30
32
 
33
+ /**
34
+ * Build progress message from current state
35
+ * @param {Object} state - Current state object
36
+ * @param {string[]} state.acceptedRepos - List of accepted repo names
37
+ * @param {string[]} state.acceptedOrgs - List of accepted org names
38
+ * @param {string[]} state.errors - List of errors
39
+ * @param {number} state.totalRepos - Total number of repo invitations
40
+ * @param {number} state.totalOrgs - Total number of org invitations
41
+ * @param {number} state.processedRepos - Number of processed repo invitations
42
+ * @param {number} state.processedOrgs - Number of processed org invitations
43
+ * @param {boolean} state.isComplete - Whether processing is complete
44
+ * @returns {string} Formatted message
45
+ */
46
+ function buildProgressMessage(state) {
47
+ const { acceptedRepos, acceptedOrgs, errors, totalRepos, totalOrgs, processedRepos, processedOrgs, isComplete } = state;
48
+
49
+ // Calculate totals
50
+ const totalInvitations = totalRepos + totalOrgs;
51
+ const processedTotal = processedRepos + processedOrgs;
52
+ const acceptedTotal = acceptedRepos.length + acceptedOrgs.length;
53
+
54
+ // Build header with progress indicator
55
+ let message = isComplete ? 'āœ… *GitHub Invitations Processed*\n\n' : `šŸ”„ *Processing GitHub Invitations* \\(${processedTotal}/${totalInvitations}\\)\n\n`;
56
+
57
+ // Show Repositories section if any
58
+ if (acceptedRepos.length > 0 || (!isComplete && totalRepos > 0)) {
59
+ message += '*Repositories:*\n';
60
+ for (const repoName of acceptedRepos) {
61
+ // Create clickable link: [owner/repo](https://github.com/owner/repo)
62
+ const escapedName = escapeMarkdown(repoName);
63
+ const escapedLink = escapeMarkdown(`https://github.com/${repoName}`);
64
+ message += ` • šŸ“¦ [${escapedName}](${escapedLink})\n`;
65
+ }
66
+ // Show pending indicator if still processing repos
67
+ if (!isComplete && processedRepos < totalRepos) {
68
+ const remaining = totalRepos - processedRepos;
69
+ message += ` • _\\.\\.\\. ${remaining} more pending_\n`;
70
+ }
71
+ message += '\n';
72
+ }
73
+
74
+ // Show Organizations section if any
75
+ if (acceptedOrgs.length > 0 || (!isComplete && totalOrgs > 0)) {
76
+ message += '*Organizations:*\n';
77
+ for (const orgName of acceptedOrgs) {
78
+ // Create clickable link: [org](https://github.com/org)
79
+ const escapedName = escapeMarkdown(orgName);
80
+ const escapedLink = escapeMarkdown(`https://github.com/${orgName}`);
81
+ message += ` • šŸ¢ [${escapedName}](${escapedLink})\n`;
82
+ }
83
+ // Show pending indicator if still processing orgs
84
+ if (!isComplete && processedOrgs < totalOrgs) {
85
+ const remaining = totalOrgs - processedOrgs;
86
+ message += ` • _\\.\\.\\. ${remaining} more pending_\n`;
87
+ }
88
+ message += '\n';
89
+ }
90
+
91
+ // Show errors if any
92
+ if (errors.length > 0) {
93
+ message += '*Errors:*\n' + errors.map(e => ` • ${escapeMarkdown(e)}`).join('\n') + '\n\n';
94
+ }
95
+
96
+ // Show summary
97
+ if (isComplete) {
98
+ if (acceptedTotal === 0 && errors.length === 0) {
99
+ message += 'No pending invitations found\\.';
100
+ } else if (acceptedTotal > 0 && errors.length === 0) {
101
+ message += `\nšŸŽ‰ Successfully accepted ${acceptedTotal} invitation\\(s\\)\\!`;
102
+ } else if (acceptedTotal > 0 && errors.length > 0) {
103
+ message += `\nāš ļø Accepted ${acceptedTotal} invitation\\(s\\), ${errors.length} error\\(s\\)\\.`;
104
+ }
105
+ }
106
+
107
+ return message;
108
+ }
109
+
31
110
  /**
32
111
  * Registers the /accept_invites command handler with the bot
33
112
  * @param {Object} bot - The Telegraf bot instance
@@ -61,68 +140,97 @@ export function registerAcceptInvitesCommand(bot, options) {
61
140
  reply_to_message_id: ctx.message.message_id,
62
141
  });
63
142
 
64
- const fetchingMessage = await ctx.reply('šŸ”„ Fetching pending GitHub invitations...', { reply_to_message_id: ctx.message.message_id });
65
- const accepted = [];
66
- const errors = [];
143
+ const fetchingMessage = await ctx.reply('šŸ”„ Fetching pending GitHub invitations\\.\\.\\.', {
144
+ reply_to_message_id: ctx.message.message_id,
145
+ parse_mode: 'MarkdownV2',
146
+ });
147
+
148
+ // State for tracking progress
149
+ const state = {
150
+ acceptedRepos: [],
151
+ acceptedOrgs: [],
152
+ errors: [],
153
+ totalRepos: 0,
154
+ totalOrgs: 0,
155
+ processedRepos: 0,
156
+ processedOrgs: 0,
157
+ isComplete: false,
158
+ };
159
+
160
+ // Helper to update the message safely
161
+ const updateMessage = async () => {
162
+ try {
163
+ const message = buildProgressMessage(state);
164
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'MarkdownV2' });
165
+ } catch (err) {
166
+ // Ignore "message not modified" errors
167
+ if (!err.message?.includes('message is not modified')) {
168
+ VERBOSE && console.log(`[VERBOSE] /accept-invites: Error updating message: ${err.message}`);
169
+ }
170
+ }
171
+ };
67
172
 
68
173
  try {
69
174
  // Fetch repository invitations
70
175
  const { stdout: repoInvJson } = await exec('gh api /user/repository_invitations 2>/dev/null || echo "[]"');
71
176
  const repoInvitations = JSON.parse(repoInvJson.trim() || '[]');
177
+ state.totalRepos = repoInvitations.length;
72
178
  VERBOSE && console.log(`[VERBOSE] Found ${repoInvitations.length} pending repo invitations`);
73
179
 
74
- // Accept each repo invitation
180
+ // Fetch organization invitations
181
+ const { stdout: orgMemJson } = await exec('gh api /user/memberships/orgs 2>/dev/null || echo "[]"');
182
+ const orgMemberships = JSON.parse(orgMemJson.trim() || '[]');
183
+ const pendingOrgs = orgMemberships.filter(m => m.state === 'pending');
184
+ state.totalOrgs = pendingOrgs.length;
185
+ VERBOSE && console.log(`[VERBOSE] Found ${pendingOrgs.length} pending org invitations`);
186
+
187
+ // Check if there are any invitations
188
+ if (state.totalRepos === 0 && state.totalOrgs === 0) {
189
+ state.isComplete = true;
190
+ await updateMessage();
191
+ return;
192
+ }
193
+
194
+ // Update to show we found invitations
195
+ await updateMessage();
196
+
197
+ // Accept each repo invitation with progress updates
75
198
  for (const inv of repoInvitations) {
76
199
  const repoName = inv.repository?.full_name || 'unknown';
77
200
  try {
78
201
  await exec(`gh api -X PATCH /user/repository_invitations/${inv.id}`);
79
- accepted.push(`šŸ“¦ Repository: ${repoName}`);
202
+ state.acceptedRepos.push(repoName);
80
203
  VERBOSE && console.log(`[VERBOSE] Accepted repo invitation: ${repoName}`);
81
204
  } catch (e) {
82
- errors.push(`šŸ“¦ ${repoName}: ${e.message}`);
205
+ state.errors.push(`šŸ“¦ ${repoName}: ${e.message}`);
83
206
  VERBOSE && console.log(`[VERBOSE] Failed to accept repo invitation ${repoName}: ${e.message}`);
84
207
  }
208
+ state.processedRepos++;
209
+ await updateMessage();
85
210
  }
86
211
 
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
212
+ // Accept each org invitation with progress updates
94
213
  for (const membership of pendingOrgs) {
95
214
  const orgName = membership.organization?.login || 'unknown';
96
215
  try {
97
216
  await exec(`gh api -X PATCH /user/memberships/orgs/${orgName} -f state=active`);
98
- accepted.push(`šŸ¢ Organization: ${orgName}`);
217
+ state.acceptedOrgs.push(orgName);
99
218
  VERBOSE && console.log(`[VERBOSE] Accepted org invitation: ${orgName}`);
100
219
  } catch (e) {
101
- errors.push(`šŸ¢ ${orgName}: ${e.message}`);
220
+ state.errors.push(`šŸ¢ ${orgName}: ${e.message}`);
102
221
  VERBOSE && console.log(`[VERBOSE] Failed to accept org invitation ${orgName}: ${e.message}`);
103
222
  }
223
+ state.processedOrgs++;
224
+ await updateMessage();
104
225
  }
105
226
 
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' });
227
+ // Final update
228
+ state.isComplete = true;
229
+ await updateMessage();
123
230
  } catch (error) {
124
231
  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' });
232
+ const escapedError = escapeMarkdown(error.message);
233
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `āŒ Error fetching invitations: ${escapedError}\n\nMake sure \`gh\` CLI is installed and authenticated\\.`, { parse_mode: 'MarkdownV2' });
126
234
  }
127
235
  });
128
236
  }