@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 +25 -0
- package/package.json +1 -1
- package/src/git.lib.mjs +198 -0
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.validation.lib.mjs +75 -0
- package/src/solve.watch.lib.mjs +54 -1
- package/src/telegram-accept-invitations.lib.mjs +128 -0
- package/src/telegram-bot.mjs +15 -1
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
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
|
};
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -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;
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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 /
|
|
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');
|