@link-assistant/hive-mind 0.51.6 â 0.51.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.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/claude-limits.lib.mjs +99 -5
- package/src/solve.results.lib.mjs +62 -51
- package/src/telegram-bot.mjs +5 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 0.51.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- de72c12: fix: ensure log attachment works when PR is merged during session
|
|
8
|
+
|
|
9
|
+
Fixes issue where log files would not be attached to pull requests when the PR was merged during the AI solving session. The `gh pr list` command only returns OPEN PRs by default, causing merged PRs to not be found. Added `--state all` flag to find PRs regardless of their state (OPEN, MERGED, or CLOSED), and added handling to skip operations that don't work on merged PRs (like `gh pr edit` and `gh pr ready`) while still allowing log attachment.
|
|
10
|
+
|
|
11
|
+
## 0.51.7
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- b7c7a2c: feat: add GitHub API rate limits to /limits command
|
|
16
|
+
|
|
17
|
+
Adds GitHub API core rate limit information to the Telegram bot's /limits command output, allowing users to monitor GitHub API usage alongside Claude usage limits and disk space. This helps plan issue execution when GitHub API limits are approaching.
|
|
18
|
+
|
|
3
19
|
## 0.51.6
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -147,6 +147,83 @@ function formatBytes(bytes) {
|
|
|
147
147
|
return `${value.toFixed(decimals)} ${sizes[i]}`;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Get GitHub API rate limits by calling gh api rate_limit
|
|
152
|
+
* Returns rate limit info for core, search, graphql, and other resources
|
|
153
|
+
*
|
|
154
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
155
|
+
* @returns {Object} Object with success boolean, and either rate limit data or error message
|
|
156
|
+
*/
|
|
157
|
+
export async function getGitHubRateLimits(verbose = false) {
|
|
158
|
+
try {
|
|
159
|
+
const { stdout } = await execAsync('gh api rate_limit 2>/dev/null');
|
|
160
|
+
const data = JSON.parse(stdout);
|
|
161
|
+
|
|
162
|
+
if (verbose) {
|
|
163
|
+
console.log('[VERBOSE] /limits GitHub rate limit response:', JSON.stringify(data, null, 2));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract the core rate limit (most important for general API usage)
|
|
167
|
+
const core = data.resources?.core;
|
|
168
|
+
if (!core) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: 'Could not parse GitHub rate limit response',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Calculate remaining percentage
|
|
176
|
+
const usedPercentage = core.limit > 0 ? Math.round((core.used / core.limit) * 100) : 0;
|
|
177
|
+
const remainingPercentage = 100 - usedPercentage;
|
|
178
|
+
|
|
179
|
+
// Format reset time from Unix timestamp
|
|
180
|
+
const resetDate = new Date(core.reset * 1000);
|
|
181
|
+
const resetTimeFormatted = formatResetTime(resetDate.toISOString());
|
|
182
|
+
|
|
183
|
+
// Calculate relative time until reset
|
|
184
|
+
const now = new Date();
|
|
185
|
+
const diffMs = resetDate - now;
|
|
186
|
+
let relativeReset = null;
|
|
187
|
+
if (diffMs > 0) {
|
|
188
|
+
const totalMinutes = Math.floor(diffMs / (1000 * 60));
|
|
189
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
190
|
+
const minutes = totalMinutes % 60;
|
|
191
|
+
if (hours > 0) {
|
|
192
|
+
relativeReset = `${hours}h ${minutes}m`;
|
|
193
|
+
} else {
|
|
194
|
+
relativeReset = `${minutes}m`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (verbose) {
|
|
199
|
+
console.log(`[VERBOSE] /limits GitHub API: ${core.remaining}/${core.limit} remaining (${remainingPercentage}% available)`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
success: true,
|
|
204
|
+
githubRateLimit: {
|
|
205
|
+
limit: core.limit,
|
|
206
|
+
used: core.used,
|
|
207
|
+
remaining: core.remaining,
|
|
208
|
+
usedPercentage,
|
|
209
|
+
remainingPercentage,
|
|
210
|
+
resetTimestamp: core.reset,
|
|
211
|
+
resetTime: resetTimeFormatted,
|
|
212
|
+
relativeReset,
|
|
213
|
+
resetsAt: resetDate.toISOString(),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (verbose) {
|
|
218
|
+
console.error('[VERBOSE] /limits GitHub rate limit error:', error);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
error: `Failed to get GitHub rate limits: ${error.message}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
150
227
|
/**
|
|
151
228
|
* Get disk space information for the current filesystem
|
|
152
229
|
* Returns total, used, available space and usage percentage
|
|
@@ -385,9 +462,10 @@ export function calculateTimePassedPercentage(resetsAt, periodHours) {
|
|
|
385
462
|
* Format Claude usage data into a Telegram-friendly message
|
|
386
463
|
* @param {Object} usage - The usage object from getClaudeUsageLimits
|
|
387
464
|
* @param {Object} diskSpace - Optional disk space info from getDiskSpaceInfo
|
|
465
|
+
* @param {Object} githubRateLimit - Optional GitHub rate limit info from getGitHubRateLimits
|
|
388
466
|
* @returns {string} Formatted message
|
|
389
467
|
*/
|
|
390
|
-
export function formatUsageMessage(usage, diskSpace = null) {
|
|
468
|
+
export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = null) {
|
|
391
469
|
// Use code block for monospace font to align progress bars properly
|
|
392
470
|
let message = '```\n';
|
|
393
471
|
|
|
@@ -397,10 +475,25 @@ export function formatUsageMessage(usage, diskSpace = null) {
|
|
|
397
475
|
// Disk space section (if provided)
|
|
398
476
|
if (diskSpace) {
|
|
399
477
|
message += 'Disk space\n';
|
|
400
|
-
// Show
|
|
401
|
-
const
|
|
402
|
-
message += `${
|
|
403
|
-
message += `${diskSpace.
|
|
478
|
+
// Show used percentage with progress bar
|
|
479
|
+
const usedBar = getProgressBar(diskSpace.usedPercentage);
|
|
480
|
+
message += `${usedBar} ${diskSpace.usedPercentage}% used\n`;
|
|
481
|
+
message += `${diskSpace.usedFormatted} used of ${diskSpace.totalFormatted}\n\n`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// GitHub API rate limits section (if provided)
|
|
485
|
+
if (githubRateLimit) {
|
|
486
|
+
message += 'GitHub API\n';
|
|
487
|
+
// Show used percentage with progress bar
|
|
488
|
+
const usedBar = getProgressBar(githubRateLimit.usedPercentage);
|
|
489
|
+
message += `${usedBar} ${githubRateLimit.usedPercentage}% used\n`;
|
|
490
|
+
message += `${githubRateLimit.used}/${githubRateLimit.limit} requests used\n`;
|
|
491
|
+
if (githubRateLimit.relativeReset) {
|
|
492
|
+
message += `Resets in ${githubRateLimit.relativeReset} (${githubRateLimit.resetTime})\n`;
|
|
493
|
+
} else if (githubRateLimit.resetTime) {
|
|
494
|
+
message += `Resets ${githubRateLimit.resetTime}\n`;
|
|
495
|
+
}
|
|
496
|
+
message += '\n';
|
|
404
497
|
}
|
|
405
498
|
|
|
406
499
|
// Current session (five_hour)
|
|
@@ -493,6 +586,7 @@ export function formatUsageMessage(usage, diskSpace = null) {
|
|
|
493
586
|
export default {
|
|
494
587
|
getClaudeUsageLimits,
|
|
495
588
|
getDiskSpaceInfo,
|
|
589
|
+
getGitHubRateLimits,
|
|
496
590
|
getProgressBar,
|
|
497
591
|
calculateTimePassedPercentage,
|
|
498
592
|
formatUsageMessage,
|
|
@@ -417,7 +417,9 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
417
417
|
await log('\nđ Checking for pull requests from branch ' + branchName + '...');
|
|
418
418
|
|
|
419
419
|
// First, get all PRs from our branch
|
|
420
|
-
|
|
420
|
+
// IMPORTANT: Use --state all to find PRs that may have been merged during the session (Issue #1008)
|
|
421
|
+
// Without --state all, gh pr list only returns OPEN PRs, missing merged ones
|
|
422
|
+
const allBranchPrsResult = await $`gh pr list --repo ${owner}/${repo} --head ${branchName} --state all --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
|
|
421
423
|
|
|
422
424
|
if (allBranchPrsResult.code !== 0) {
|
|
423
425
|
await log(' â ī¸ Failed to check pull requests');
|
|
@@ -438,63 +440,72 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
438
440
|
if (isPrFromSession) {
|
|
439
441
|
await log(` â
Found pull request #${pr.number}: "${pr.title}"`);
|
|
440
442
|
|
|
441
|
-
// Check if PR
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// Use the new GitHub linking detection library to check for valid keywords
|
|
448
|
-
// This ensures we only detect actual GitHub-recognized linking keywords
|
|
449
|
-
// (fixes, closes, resolves and their variants) in proper format
|
|
450
|
-
// See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
|
|
451
|
-
const hasLinkingKeyword = hasGitHubLinkingKeyword(prBody, issueNumber, argv.fork ? owner : null, argv.fork ? repo : null);
|
|
452
|
-
|
|
453
|
-
if (!hasLinkingKeyword) {
|
|
454
|
-
await log(` đ Updating PR body to link issue #${issueNumber}...`);
|
|
455
|
-
|
|
456
|
-
// Add proper issue reference to the PR body
|
|
457
|
-
const linkingText = `\n\nFixes ${issueRef}`;
|
|
458
|
-
const updatedBody = prBody + linkingText;
|
|
459
|
-
|
|
460
|
-
// Use --body-file instead of --body to avoid command-line length limits
|
|
461
|
-
// and special character escaping issues that can cause hangs/timeouts
|
|
462
|
-
const fs = (await use('fs')).promises;
|
|
463
|
-
const tempBodyFile = `/tmp/pr-body-update-${pr.number}-${Date.now()}.md`;
|
|
464
|
-
await fs.writeFile(tempBodyFile, updatedBody);
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
const updateResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
|
|
468
|
-
|
|
469
|
-
// Clean up temp file
|
|
470
|
-
await fs.unlink(tempBodyFile).catch(() => {});
|
|
443
|
+
// Check if PR was merged during the session (Issue #1008)
|
|
444
|
+
const isPrMerged = pr.state === 'MERGED';
|
|
445
|
+
if (isPrMerged) {
|
|
446
|
+
await log(` âšī¸ PR #${pr.number} was merged during the session`);
|
|
447
|
+
}
|
|
471
448
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
449
|
+
// Skip PR body update and ready conversion for merged PRs (they can't be edited)
|
|
450
|
+
if (!isPrMerged) {
|
|
451
|
+
// Check if PR body has proper issue linking keywords
|
|
452
|
+
const prBodyResult = await $`gh pr view ${pr.number} --repo ${owner}/${repo} --json body --jq .body`;
|
|
453
|
+
if (prBodyResult.code === 0) {
|
|
454
|
+
const prBody = prBodyResult.stdout.toString();
|
|
455
|
+
const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
|
|
456
|
+
|
|
457
|
+
// Use the new GitHub linking detection library to check for valid keywords
|
|
458
|
+
// This ensures we only detect actual GitHub-recognized linking keywords
|
|
459
|
+
// (fixes, closes, resolves and their variants) in proper format
|
|
460
|
+
// See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
|
|
461
|
+
const hasLinkingKeyword = hasGitHubLinkingKeyword(prBody, issueNumber, argv.fork ? owner : null, argv.fork ? repo : null);
|
|
462
|
+
|
|
463
|
+
if (!hasLinkingKeyword) {
|
|
464
|
+
await log(` đ Updating PR body to link issue #${issueNumber}...`);
|
|
465
|
+
|
|
466
|
+
// Add proper issue reference to the PR body
|
|
467
|
+
const linkingText = `\n\nFixes ${issueRef}`;
|
|
468
|
+
const updatedBody = prBody + linkingText;
|
|
469
|
+
|
|
470
|
+
// Use --body-file instead of --body to avoid command-line length limits
|
|
471
|
+
// and special character escaping issues that can cause hangs/timeouts
|
|
472
|
+
const fs = (await use('fs')).promises;
|
|
473
|
+
const tempBodyFile = `/tmp/pr-body-update-${pr.number}-${Date.now()}.md`;
|
|
474
|
+
await fs.writeFile(tempBodyFile, updatedBody);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const updateResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
|
|
478
|
+
|
|
479
|
+
// Clean up temp file
|
|
480
|
+
await fs.unlink(tempBodyFile).catch(() => {});
|
|
481
|
+
|
|
482
|
+
if (updateResult.code === 0) {
|
|
483
|
+
await log(` â
Updated PR body to include "Fixes ${issueRef}"`);
|
|
484
|
+
} else {
|
|
485
|
+
await log(` â ī¸ Could not update PR body: ${updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error'}`);
|
|
486
|
+
}
|
|
487
|
+
} catch (updateError) {
|
|
488
|
+
// Clean up temp file on error
|
|
489
|
+
await fs.unlink(tempBodyFile).catch(() => {});
|
|
490
|
+
throw updateError;
|
|
476
491
|
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
await fs.unlink(tempBodyFile).catch(() => {});
|
|
480
|
-
throw updateError;
|
|
492
|
+
} else {
|
|
493
|
+
await log(' â
PR body already contains issue reference');
|
|
481
494
|
}
|
|
482
|
-
} else {
|
|
483
|
-
await log(' â
PR body already contains issue reference');
|
|
484
495
|
}
|
|
485
|
-
}
|
|
486
496
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
497
|
+
// Check if PR is ready for review (convert from draft if necessary)
|
|
498
|
+
if (pr.isDraft) {
|
|
499
|
+
await log(' đ Converting PR from draft to ready for review...');
|
|
500
|
+
const readyResult = await $`gh pr ready ${pr.number} --repo ${owner}/${repo}`;
|
|
501
|
+
if (readyResult.code === 0) {
|
|
502
|
+
await log(' â
PR converted to ready for review');
|
|
503
|
+
} else {
|
|
504
|
+
await log(` â ī¸ Could not convert PR to ready (${readyResult.stderr ? readyResult.stderr.toString().trim() : 'unknown error'})`);
|
|
505
|
+
}
|
|
493
506
|
} else {
|
|
494
|
-
await log(
|
|
507
|
+
await log(' â
PR is already ready for review', { verbose: true });
|
|
495
508
|
}
|
|
496
|
-
} else {
|
|
497
|
-
await log(' â
PR is already ready for review', { verbose: true });
|
|
498
509
|
}
|
|
499
510
|
|
|
500
511
|
// Upload log file to PR if requested
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -45,7 +45,7 @@ const { parseGitHubUrl } = await import('./github.lib.mjs');
|
|
|
45
45
|
const { validateModelName } = await import('./model-validation.lib.mjs');
|
|
46
46
|
|
|
47
47
|
// Import Claude limits library for /limits command
|
|
48
|
-
const { getClaudeUsageLimits, getDiskSpaceInfo, formatUsageMessage } = await import('./claude-limits.lib.mjs');
|
|
48
|
+
const { getClaudeUsageLimits, getDiskSpaceInfo, getGitHubRateLimits, formatUsageMessage } = await import('./claude-limits.lib.mjs');
|
|
49
49
|
|
|
50
50
|
// Import version info library for /version command
|
|
51
51
|
const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
|
|
@@ -848,8 +848,8 @@ bot.command('limits', async ctx => {
|
|
|
848
848
|
reply_to_message_id: ctx.message.message_id,
|
|
849
849
|
});
|
|
850
850
|
|
|
851
|
-
// Get the usage limits
|
|
852
|
-
const [result, diskSpaceResult] = await Promise.all([getClaudeUsageLimits(VERBOSE), getDiskSpaceInfo(VERBOSE)]);
|
|
851
|
+
// Get the usage limits, disk space info, and GitHub rate limits in parallel
|
|
852
|
+
const [result, diskSpaceResult, githubLimitsResult] = await Promise.all([getClaudeUsageLimits(VERBOSE), getDiskSpaceInfo(VERBOSE), getGitHubRateLimits(VERBOSE)]);
|
|
853
853
|
|
|
854
854
|
if (!result.success) {
|
|
855
855
|
// Edit the fetching message to show the error
|
|
@@ -859,10 +859,8 @@ bot.command('limits', async ctx => {
|
|
|
859
859
|
return;
|
|
860
860
|
}
|
|
861
861
|
|
|
862
|
-
// Format and edit the fetching message with the results
|
|
863
|
-
|
|
864
|
-
const diskSpace = diskSpaceResult.success ? diskSpaceResult.diskSpace : null;
|
|
865
|
-
const message = 'đ *Usage Limits*\n\n' + formatUsageMessage(result.usage, diskSpace);
|
|
862
|
+
// Format and edit the fetching message with the results (pass disk space and GitHub limits if available)
|
|
863
|
+
const message = 'đ *Usage Limits*\n\n' + formatUsageMessage(result.usage, diskSpaceResult.success ? diskSpaceResult.diskSpace : null, githubLimitsResult.success ? githubLimitsResult.githubRateLimit : null);
|
|
866
864
|
await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, {
|
|
867
865
|
parse_mode: 'Markdown',
|
|
868
866
|
});
|