@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.51.6",
3
+ "version": "0.51.9",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 free percentage with progress bar (inverted - showing free space)
401
- const freeBar = getProgressBar(diskSpace.freePercentage);
402
- message += `${freeBar} ${diskSpace.freePercentage}% free\n`;
403
- message += `${diskSpace.availableFormatted} free of ${diskSpace.totalFormatted}\n\n`;
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
- const allBranchPrsResult = await $`gh pr list --repo ${owner}/${repo} --head ${branchName} --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
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 body has proper issue linking keywords
442
- const prBodyResult = await $`gh pr view ${pr.number} --repo ${owner}/${repo} --json body --jq .body`;
443
- if (prBodyResult.code === 0) {
444
- const prBody = prBodyResult.stdout.toString();
445
- const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
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
- if (updateResult.code === 0) {
473
- await log(` ✅ Updated PR body to include "Fixes ${issueRef}"`);
474
- } else {
475
- await log(` âš ī¸ Could not update PR body: ${updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error'}`);
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
- } catch (updateError) {
478
- // Clean up temp file on error
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
- // Check if PR is ready for review (convert from draft if necessary)
488
- if (pr.isDraft) {
489
- await log(' 🔄 Converting PR from draft to ready for review...');
490
- const readyResult = await $`gh pr ready ${pr.number} --repo ${owner}/${repo}`;
491
- if (readyResult.code === 0) {
492
- await log(' ✅ PR converted to ready for review');
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(` âš ī¸ Could not convert PR to ready (${readyResult.stderr ? readyResult.stderr.toString().trim() : 'unknown error'})`);
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
@@ -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 and disk space info in parallel
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
- // Pass disk space info if available (non-critical if it fails)
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
  });