@link-assistant/hive-mind 0.51.4 → 0.51.6

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,34 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.51.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 9ee79c8: fix(ci): Add timeout, verbose diagnostics, and pre-fetch caching for Docker ARM64 builds
8
+
9
+ Addresses issue #998 where Docker Publish (linux/arm64) was stuck for >1.5 hours due to slow Homebrew bottle downloads on GitHub's ARM64 runners.
10
+
11
+ Changes:
12
+ - Added 90-minute timeout to docker-publish jobs to prevent indefinite hangs
13
+ - Switched from ubuntu-24.04-arm to ubuntu-22.04-arm for better network performance
14
+ - Added documentation comments about known ARM64 runner issues
15
+ - Added Homebrew verbose mode (`HOMEBREW_VERBOSE=1`) for detailed diagnostics
16
+ - Added `brew fetch --deps --retry` to pre-download bottles before installation
17
+ - Added timing measurements for fetch and install steps
18
+ - Updated case study with diagnostic approach
19
+
20
+ Root cause: GitHub's ubuntu-24.04-arm runners have known network performance issues (actions/runner-images#11790, actions/partner-runner-images#101). The ARM64 build was stuck downloading Homebrew bottles for PHP dependencies at extremely slow speeds.
21
+
22
+ See docs/case-studies/issue-998/README.md for detailed analysis.
23
+
24
+ ## 0.51.5
25
+
26
+ ### Patch Changes
27
+
28
+ - 1a17f74: feat: add disk space information to /limits command
29
+
30
+ Adds free disk space percentage and size information to the Telegram bot's /limits command output, allowing users to monitor disk usage alongside Claude API limits and plan issue execution accordingly.
31
+
3
32
  ## 0.51.4
4
33
 
5
34
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.51.4",
3
+ "version": "0.51.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -4,9 +4,13 @@
4
4
  * Provides functions to fetch and parse Claude usage limits via OAuth API
5
5
  */
6
6
 
7
+ import { exec } from 'node:child_process';
7
8
  import { readFile } from 'node:fs/promises';
8
9
  import { homedir } from 'node:os';
9
10
  import { join } from 'node:path';
11
+ import { promisify } from 'node:util';
12
+
13
+ const execAsync = promisify(exec);
10
14
 
11
15
  /**
12
16
  * Default path to Claude credentials file
@@ -127,6 +131,101 @@ function formatCurrentTime() {
127
131
  return `${month} ${day}, ${hour12}:${minutes.toString().padStart(2, '0')}${ampm} UTC`;
128
132
  }
129
133
 
134
+ /**
135
+ * Format bytes into human-readable size
136
+ * @param {number} bytes - Size in bytes
137
+ * @returns {string} Formatted size (e.g., "19.3 GB")
138
+ */
139
+ function formatBytes(bytes) {
140
+ if (bytes === 0) return '0 B';
141
+ const k = 1024;
142
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
143
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
144
+ const value = bytes / Math.pow(k, i);
145
+ // Use 1 decimal place for GB and above, none for smaller units
146
+ const decimals = i >= 3 ? 1 : 0;
147
+ return `${value.toFixed(decimals)} ${sizes[i]}`;
148
+ }
149
+
150
+ /**
151
+ * Get disk space information for the current filesystem
152
+ * Returns total, used, available space and usage percentage
153
+ *
154
+ * @param {boolean} verbose - Whether to log verbose output
155
+ * @returns {Object} Object with success boolean, and either disk space data or error message
156
+ */
157
+ export async function getDiskSpaceInfo(verbose = false) {
158
+ try {
159
+ let totalMB, usedMB, availableMB, usedPercentage;
160
+
161
+ if (process.platform === 'darwin') {
162
+ // macOS: use df with 1024-byte blocks and parse
163
+ const { stdout } = await execAsync("df -k . 2>/dev/null | tail -1 | awk '{print $2, $3, $4}'");
164
+ const [totalKB, usedKB, availableKB] = stdout.trim().split(/\s+/).map(Number);
165
+ totalMB = Math.round(totalKB / 1024);
166
+ usedMB = Math.round(usedKB / 1024);
167
+ availableMB = Math.round(availableKB / 1024);
168
+ } else if (process.platform === 'win32') {
169
+ // Windows: use PowerShell to get drive info
170
+ const { stdout } = await execAsync('powershell -Command "$drive = (Get-Location).Drive; $info = Get-PSDrive -Name $drive.Name; Write-Output \\"$($info.Used) $($info.Free)\\""');
171
+ const [usedBytes, freeBytes] = stdout.trim().split(/\s+/).map(Number);
172
+ const totalBytes = usedBytes + freeBytes;
173
+ totalMB = Math.round(totalBytes / (1024 * 1024));
174
+ usedMB = Math.round(usedBytes / (1024 * 1024));
175
+ availableMB = Math.round(freeBytes / (1024 * 1024));
176
+ } else {
177
+ // Linux: use df with megabyte blocks
178
+ const { stdout } = await execAsync("df -BM . 2>/dev/null | tail -1 | awk '{print $2, $3, $4}'");
179
+ const parts = stdout
180
+ .trim()
181
+ .split(/\s+/)
182
+ .map(s => parseInt(s.replace('M', '')));
183
+ [totalMB, usedMB, availableMB] = parts;
184
+ }
185
+
186
+ if (isNaN(totalMB) || isNaN(usedMB) || isNaN(availableMB)) {
187
+ return {
188
+ success: false,
189
+ error: 'Failed to parse disk space information',
190
+ };
191
+ }
192
+
193
+ // Calculate used percentage (rounded to nearest integer)
194
+ usedPercentage = Math.round((usedMB / totalMB) * 100);
195
+ // Free percentage is the inverse
196
+ const freePercentage = 100 - usedPercentage;
197
+
198
+ if (verbose) {
199
+ console.log(`[VERBOSE] /limits disk space: ${availableMB}MB free of ${totalMB}MB total (${freePercentage}% free)`);
200
+ }
201
+
202
+ return {
203
+ success: true,
204
+ diskSpace: {
205
+ totalMB,
206
+ usedMB,
207
+ availableMB,
208
+ totalBytes: totalMB * 1024 * 1024,
209
+ usedBytes: usedMB * 1024 * 1024,
210
+ availableBytes: availableMB * 1024 * 1024,
211
+ usedPercentage,
212
+ freePercentage,
213
+ totalFormatted: formatBytes(totalMB * 1024 * 1024),
214
+ usedFormatted: formatBytes(usedMB * 1024 * 1024),
215
+ availableFormatted: formatBytes(availableMB * 1024 * 1024),
216
+ },
217
+ };
218
+ } catch (error) {
219
+ if (verbose) {
220
+ console.error('[VERBOSE] /limits disk space error:', error);
221
+ }
222
+ return {
223
+ success: false,
224
+ error: `Failed to get disk space info: ${error.message}`,
225
+ };
226
+ }
227
+ }
228
+
130
229
  /**
131
230
  * Get Claude usage limits by calling the Anthropic OAuth usage API
132
231
  * This approach is more reliable than trying to parse CLI output
@@ -285,15 +384,25 @@ export function calculateTimePassedPercentage(resetsAt, periodHours) {
285
384
  /**
286
385
  * Format Claude usage data into a Telegram-friendly message
287
386
  * @param {Object} usage - The usage object from getClaudeUsageLimits
387
+ * @param {Object} diskSpace - Optional disk space info from getDiskSpaceInfo
288
388
  * @returns {string} Formatted message
289
389
  */
290
- export function formatUsageMessage(usage) {
390
+ export function formatUsageMessage(usage, diskSpace = null) {
291
391
  // Use code block for monospace font to align progress bars properly
292
392
  let message = '```\n';
293
393
 
294
394
  // Show current time
295
395
  message += `Current time: ${formatCurrentTime()}\n\n`;
296
396
 
397
+ // Disk space section (if provided)
398
+ if (diskSpace) {
399
+ 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`;
404
+ }
405
+
297
406
  // Current session (five_hour)
298
407
  message += 'Current session\n';
299
408
  if (usage.currentSession.percentage !== null) {
@@ -383,6 +492,7 @@ export function formatUsageMessage(usage) {
383
492
 
384
493
  export default {
385
494
  getClaudeUsageLimits,
495
+ getDiskSpaceInfo,
386
496
  getProgressBar,
387
497
  calculateTimePassedPercentage,
388
498
  formatUsageMessage,
@@ -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, formatUsageMessage } = await import('./claude-limits.lib.mjs');
48
+ const { getClaudeUsageLimits, getDiskSpaceInfo, 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');
@@ -844,12 +844,12 @@ bot.command('limits', async ctx => {
844
844
  }
845
845
 
846
846
  // Send "fetching" message to indicate work is in progress
847
- const fetchingMessage = await ctx.reply('🔄 Fetching Claude usage limits...', {
847
+ const fetchingMessage = await ctx.reply('🔄 Fetching usage limits...', {
848
848
  reply_to_message_id: ctx.message.message_id,
849
849
  });
850
850
 
851
- // Get the usage limits using the library function
852
- const result = await getClaudeUsageLimits(VERBOSE);
851
+ // Get the usage limits and disk space info in parallel
852
+ const [result, diskSpaceResult] = await Promise.all([getClaudeUsageLimits(VERBOSE), getDiskSpaceInfo(VERBOSE)]);
853
853
 
854
854
  if (!result.success) {
855
855
  // Edit the fetching message to show the error
@@ -860,7 +860,9 @@ bot.command('limits', async ctx => {
860
860
  }
861
861
 
862
862
  // Format and edit the fetching message with the results
863
- const message = '📊 *Claude Usage Limits*\n\n' + formatUsageMessage(result.usage);
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);
864
866
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, {
865
867
  parse_mode: 'Markdown',
866
868
  });