@link-assistant/hive-mind 1.3.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,30 @@
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
+
9
+ ## 1.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 4a476ae: Add separate log comment for each auto-restart session with cost estimation
14
+ - Each auto-restart iteration now uploads its own session log with cost estimation to the PR
15
+ - Log comments use "Auto-restart X/Y Log" format instead of generic "Solution Draft Log"
16
+ - Issue #1107
17
+
18
+ ### Patch Changes
19
+
20
+ - 3239fa1: Add git identity validation to prevent commit failures
21
+ - Added `checkGitIdentity()` and `validateGitIdentity()` functions to validate git user configuration
22
+ - Added git identity check to `performSystemChecks()` that runs before any work begins
23
+ - Added `--auto-gh-configuration-repair` option that uses external `gh-setup-git-identity` command for automatic repair
24
+ - Added unit tests for identity validation
25
+
26
+ This fix prevents the "fatal: empty ident name" error that occurs when git user.name and user.email are not configured. When git identity is missing, users now see a clear error message with instructions for fixing it. The auto-repair feature requires the external [gh-setup-git-identity](https://github.com/link-foundation/gh-setup-git-identity) package to be installed.
27
+
3
28
  ## 1.3.0
4
29
 
5
30
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.3.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",
package/src/git.lib.mjs CHANGED
@@ -134,6 +134,201 @@ export const getGitVersionAsync = async ($, currentVersion) => {
134
134
  return currentVersion;
135
135
  };
136
136
 
137
+ /**
138
+ * Validates git user identity configuration
139
+ * Returns an object with validation status and identity info
140
+ *
141
+ * Git commits require both user.name and user.email to be set.
142
+ * This function checks both global (~/.gitconfig) and local (.git/config) configurations.
143
+ *
144
+ * See: https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup
145
+ * Related error: "fatal: empty ident name (for <>) not allowed"
146
+ *
147
+ * @param {function} execFunc - The exec function to use (for testing)
148
+ * @returns {Promise<{isValid: boolean, name: string|null, email: string|null, scope: string|null, error: string|null}>}
149
+ */
150
+ export const checkGitIdentity = async (execFunc = execAsync) => {
151
+ const result = {
152
+ isValid: false,
153
+ name: null,
154
+ email: null,
155
+ scope: null, // 'global', 'local', or 'none'
156
+ error: null,
157
+ };
158
+
159
+ try {
160
+ // Check for user.name
161
+ try {
162
+ const { stdout: nameStdout } = await execFunc('git config user.name', {
163
+ encoding: 'utf8',
164
+ env: process.env,
165
+ });
166
+ result.name = nameStdout.trim() || null;
167
+ } catch {
168
+ // user.name not set
169
+ result.name = null;
170
+ }
171
+
172
+ // Check for user.email
173
+ try {
174
+ const { stdout: emailStdout } = await execFunc('git config user.email', {
175
+ encoding: 'utf8',
176
+ env: process.env,
177
+ });
178
+ result.email = emailStdout.trim() || null;
179
+ } catch {
180
+ // user.email not set
181
+ result.email = null;
182
+ }
183
+
184
+ // Determine scope (check if local config exists)
185
+ if (result.name || result.email) {
186
+ try {
187
+ const { stdout: scopeStdout } = await execFunc('git config --show-origin user.name', {
188
+ encoding: 'utf8',
189
+ env: process.env,
190
+ });
191
+ // Output format: "file:/path/to/config\tvalue"
192
+ if (scopeStdout.includes('.git/config')) {
193
+ result.scope = 'local';
194
+ } else if (scopeStdout.includes('.gitconfig') || scopeStdout.includes('/etc/gitconfig')) {
195
+ result.scope = 'global';
196
+ } else {
197
+ result.scope = 'global';
198
+ }
199
+ } catch {
200
+ result.scope = 'none';
201
+ }
202
+ } else {
203
+ result.scope = 'none';
204
+ }
205
+
206
+ // Both name and email must be non-empty for valid git identity
207
+ // Empty string is also invalid (git rejects it)
208
+ result.isValid = !!(result.name && result.name.length > 0 && result.email && result.email.length > 0);
209
+
210
+ if (!result.isValid) {
211
+ const missing = [];
212
+ if (!result.name || result.name.length === 0) missing.push('user.name');
213
+ if (!result.email || result.email.length === 0) missing.push('user.email');
214
+ result.error = `Git identity incomplete: missing ${missing.join(' and ')}`;
215
+ }
216
+ } catch (error) {
217
+ result.error = `Failed to check git identity: ${error.message}`;
218
+ }
219
+
220
+ return result;
221
+ };
222
+
223
+ /**
224
+ * Validates git user identity and returns detailed error message if invalid
225
+ * Uses zx's $ for async execution
226
+ *
227
+ * @param {function} $ - The zx $ function
228
+ * @param {object} options - Options object
229
+ * @param {function} options.log - Log function for output
230
+ * @returns {Promise<boolean>} - True if identity is valid, false otherwise
231
+ */
232
+ export const validateGitIdentity = async ($, options = {}) => {
233
+ const { log = console.log } = options;
234
+
235
+ // Check user.name
236
+ let userName = null;
237
+ try {
238
+ const nameResult = await $`git config user.name 2>/dev/null || true`;
239
+ userName = nameResult.stdout.toString().trim() || null;
240
+ } catch {
241
+ userName = null;
242
+ }
243
+
244
+ // Check user.email
245
+ let userEmail = null;
246
+ try {
247
+ const emailResult = await $`git config user.email 2>/dev/null || true`;
248
+ userEmail = emailResult.stdout.toString().trim() || null;
249
+ } catch {
250
+ userEmail = null;
251
+ }
252
+
253
+ // Both must be set and non-empty
254
+ const isValid = !!(userName && userName.length > 0 && userEmail && userEmail.length > 0);
255
+
256
+ if (!isValid) {
257
+ const missing = [];
258
+ if (!userName || userName.length === 0) missing.push('user.name');
259
+ if (!userEmail || userEmail.length === 0) missing.push('user.email');
260
+
261
+ await log('');
262
+ await log('❌ Git identity not configured', { level: 'error' });
263
+ await log('');
264
+ await log(' Git commits require both user.name and user.email to be set.');
265
+ await log(` Missing: ${missing.join(' and ')}`);
266
+ await log('');
267
+ await log(' Current configuration:');
268
+ await log(` user.name: ${userName || '(not set)'}`);
269
+ await log(` user.email: ${userEmail || '(not set)'}`);
270
+ await log('');
271
+ await log(' 🔧 How to fix:');
272
+ await log('');
273
+ await log(' Option 1: Use GitHub CLI to set identity from your account');
274
+ await log(' gh-setup-git-identity');
275
+ await log('');
276
+ await log(' Option 2: Set identity manually');
277
+ await log(' git config --global user.name "Your Name"');
278
+ await log(' git config --global user.email "you@example.com"');
279
+ await log('');
280
+ await log(' Related error: "fatal: empty ident name (for <>) not allowed"');
281
+ await log('');
282
+ return false;
283
+ }
284
+
285
+ return true;
286
+ };
287
+
288
+ /**
289
+ * Attempts to repair git identity using gh-setup-git-identity --repair
290
+ * This function requires gh-setup-git-identity to be installed.
291
+ *
292
+ * @param {function} execFunc - The exec function to use (for testing)
293
+ * @returns {Promise<{success: boolean, error: string|null}>}
294
+ */
295
+ export const repairGitIdentity = async (execFunc = execAsync) => {
296
+ const result = {
297
+ success: false,
298
+ error: null,
299
+ };
300
+
301
+ try {
302
+ // First check if gh-setup-git-identity is installed
303
+ try {
304
+ await execFunc('which gh-setup-git-identity', {
305
+ encoding: 'utf8',
306
+ });
307
+ } catch {
308
+ result.error = 'gh-setup-git-identity is not installed. Please install it first or fix git identity manually.';
309
+ return result;
310
+ }
311
+
312
+ // Run gh-setup-git-identity --repair
313
+ await execFunc('gh-setup-git-identity --repair', {
314
+ encoding: 'utf8',
315
+ env: process.env,
316
+ });
317
+
318
+ // Check if the repair was successful by validating git identity
319
+ const identityCheck = await checkGitIdentity(execFunc);
320
+ if (identityCheck.isValid) {
321
+ result.success = true;
322
+ } else {
323
+ result.error = `Repair command completed but identity is still invalid: ${identityCheck.error}`;
324
+ }
325
+ } catch (error) {
326
+ result.error = `Failed to repair git identity: ${error.message}`;
327
+ }
328
+
329
+ return result;
330
+ };
331
+
137
332
  // Export all functions as default as well
138
333
  export default {
139
334
  isGitRepository,
@@ -142,4 +337,7 @@ export default {
142
337
  getCommitSha,
143
338
  getGitVersion,
144
339
  getGitVersionAsync,
340
+ checkGitIdentity,
341
+ validateGitIdentity,
342
+ repairGitIdentity,
145
343
  };
@@ -321,6 +321,11 @@ export const createYargsConfig = yargsInstance => {
321
321
  description: 'Automatically remove .playwright-mcp/ folder before checking for uncommitted changes. This prevents browser automation artifacts from triggering auto-restart. Use --no-playwright-mcp-auto-cleanup to keep the folder for debugging.',
322
322
  default: true,
323
323
  })
324
+ .option('auto-gh-configuration-repair', {
325
+ type: 'boolean',
326
+ description: 'Automatically repair git configuration using gh-setup-git-identity --repair when git identity is not configured. Requires gh-setup-git-identity to be installed.',
327
+ default: false,
328
+ })
324
329
  .parserConfiguration({
325
330
  'boolean-negation': true,
326
331
  })
@@ -33,6 +33,10 @@ const {
33
33
  // isGitHubUrlType - not currently used
34
34
  } = githubLib;
35
35
 
36
+ // Import git-related functions for identity validation and repair
37
+ const gitLib = await import('./git.lib.mjs');
38
+ const { checkGitIdentity, repairGitIdentity } = gitLib;
39
+
36
40
  // Import Claude-related functions
37
41
  const claudeLib = await import('./claude.lib.mjs');
38
42
  // Import Sentry integration
@@ -217,6 +221,77 @@ export const performSystemChecks = async (minDiskSpace = 2048, skipToolConnectio
217
221
  return false;
218
222
  }
219
223
 
224
+ // Check git identity configuration before proceeding
225
+ // This prevents the "fatal: empty ident name" error during commits
226
+ // See: https://github.com/link-assistant/hive-mind/issues/1131
227
+ let gitIdentity = await checkGitIdentity();
228
+ if (!gitIdentity.isValid) {
229
+ // Check if auto-repair is enabled
230
+ if (argv.autoGhConfigurationRepair) {
231
+ await log('');
232
+ await log('⚠️ Git identity not configured, attempting auto-repair...', { level: 'warning' });
233
+ await log(` ${gitIdentity.error || 'Configuration is incomplete'}`);
234
+ await log('');
235
+
236
+ const repairResult = await repairGitIdentity();
237
+ if (repairResult.success) {
238
+ await log('✅ Git identity successfully repaired using gh-setup-git-identity --repair');
239
+ // Re-check identity to display the configured values
240
+ gitIdentity = await checkGitIdentity();
241
+ await log(` user.name: ${gitIdentity.name}`);
242
+ await log(` user.email: ${gitIdentity.email}`);
243
+ await log('');
244
+ } else {
245
+ await log('');
246
+ await log('❌ Auto-repair failed', { level: 'error' });
247
+ await log(` ${repairResult.error}`);
248
+ await log('');
249
+ await log(' Current configuration:');
250
+ await log(` user.name: ${gitIdentity.name || '(not set)'}`);
251
+ await log(` user.email: ${gitIdentity.email || '(not set)'}`);
252
+ await log('');
253
+ await log(' 🔧 How to fix manually:');
254
+ await log('');
255
+ await log(' Option 1: Install gh-setup-git-identity and use --auto-gh-configuration-repair');
256
+ await log(' npm install -g @link-foundation/gh-setup-git-identity');
257
+ await log('');
258
+ await log(' Option 2: Set identity manually');
259
+ await log(' git config --global user.name "Your Name"');
260
+ await log(' git config --global user.email "you@example.com"');
261
+ await log('');
262
+ await log(' Related error: "fatal: empty ident name (for <>) not allowed"');
263
+ await log('');
264
+ return false;
265
+ }
266
+ } else {
267
+ await log('');
268
+ await log('❌ Git identity not configured', { level: 'error' });
269
+ await log('');
270
+ await log(' Git commits require both user.name and user.email to be set.');
271
+ await log(` ${gitIdentity.error || 'Configuration is incomplete'}`);
272
+ await log('');
273
+ await log(' Current configuration:');
274
+ await log(` user.name: ${gitIdentity.name || '(not set)'}`);
275
+ await log(` user.email: ${gitIdentity.email || '(not set)'}`);
276
+ await log('');
277
+ await log(' 🔧 How to fix:');
278
+ await log('');
279
+ await log(' Option 1: Use GitHub CLI to set identity from your account');
280
+ await log(' gh-setup-git-identity');
281
+ await log('');
282
+ await log(' Option 2: Set identity manually');
283
+ await log(' git config --global user.name "Your Name"');
284
+ await log(' git config --global user.email "you@example.com"');
285
+ await log('');
286
+ await log(' Option 3: Enable auto-repair (requires gh-setup-git-identity)');
287
+ await log(' solve <issue-url> --auto-gh-configuration-repair');
288
+ await log('');
289
+ await log(' Related error: "fatal: empty ident name (for <>) not allowed"');
290
+ await log('');
291
+ return false;
292
+ }
293
+ }
294
+
220
295
  // Skip tool connection validation if in dry-run mode or explicitly requested
221
296
  if (!skipToolConnection) {
222
297
  let isToolConnected = false;
@@ -21,7 +21,7 @@ const fs = (await use('fs')).promises;
21
21
 
22
22
  // Import shared library functions
23
23
  const lib = await import('./lib.mjs');
24
- const { log, cleanErrorMessage, formatAligned } = lib;
24
+ const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
25
25
 
26
26
  // Import feedback detection functions
27
27
  const feedbackLib = await import('./solve.feedback.lib.mjs');
@@ -29,6 +29,10 @@ const feedbackLib = await import('./solve.feedback.lib.mjs');
29
29
  const sentryLib = await import('./sentry.lib.mjs');
30
30
  const { reportError } = sentryLib;
31
31
 
32
+ // Import GitHub functions for log attachment
33
+ const githubLib = await import('./github.lib.mjs');
34
+ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
35
+
32
36
  const { detectAndCountFeedback } = feedbackLib;
33
37
 
34
38
  /**
@@ -517,6 +521,55 @@ export const watchForFeedback = async params => {
517
521
  }
518
522
  }
519
523
 
524
+ // Issue #1107: Attach log after each auto-restart session with its own cost estimation
525
+ // This ensures each restart has its own log comment instead of one combined log at the end
526
+ const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
527
+ if (isTemporaryWatch && prNumber && shouldAttachLogs) {
528
+ await log('');
529
+ await log(formatAligned('📎', 'Uploading auto-restart session log...', ''));
530
+ try {
531
+ const logFile = getLogFile();
532
+ if (logFile) {
533
+ // Use "Auto-restart X/Y Log" format as requested in issue #1107
534
+ const customTitle = `🔄 Auto-restart ${autoRestartCount}/${maxAutoRestartIterations} Log`;
535
+ const logUploadSuccess = await attachLogToGitHub({
536
+ logFile,
537
+ targetType: 'pr',
538
+ targetNumber: prNumber,
539
+ owner,
540
+ repo,
541
+ $,
542
+ log,
543
+ sanitizeLogContent,
544
+ verbose: argv.verbose,
545
+ customTitle,
546
+ sessionId: latestSessionId,
547
+ tempDir,
548
+ anthropicTotalCostUSD: latestAnthropicCost,
549
+ // Pass agent tool pricing data when available
550
+ publicPricingEstimate: toolResult.publicPricingEstimate,
551
+ pricingInfo: toolResult.pricingInfo,
552
+ });
553
+
554
+ if (logUploadSuccess) {
555
+ await log(formatAligned('', '✅ Auto-restart session log uploaded to PR', '', 2));
556
+ } else {
557
+ await log(formatAligned('', '⚠️ Could not upload auto-restart session log', '', 2));
558
+ }
559
+ }
560
+ } catch (logUploadError) {
561
+ reportError(logUploadError, {
562
+ context: 'attach_auto_restart_log',
563
+ prNumber,
564
+ owner,
565
+ repo,
566
+ autoRestartCount,
567
+ operation: 'upload_session_log',
568
+ });
569
+ await log(formatAligned('', `⚠️ Log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
570
+ }
571
+ }
572
+
520
573
  await log('');
521
574
  if (isTemporaryWatch) {
522
575
  await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking for remaining changes...'));
@@ -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');