@link-assistant/hive-mind 1.0.2 → 1.0.4

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,28 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 4e5e1ab: Use gh-upload-log for log file uploads (issue #587)
8
+ - Replace custom gist creation with gh-upload-log command
9
+ - Implement smart linking: 1 chunk = direct raw link, >1 chunks = repo link
10
+ - Update case study documentation with gh-upload-log v0.5.0 fixes
11
+ - Remove custom log compression in favor of gh-upload-log auto mode
12
+
13
+ ## 1.0.3
14
+
15
+ ### Patch Changes
16
+
17
+ - 26b69f2: Fix Claude Code output token limit by setting CLAUDE_CODE_MAX_OUTPUT_TOKENS to 64000
18
+ - Claude Code CLI defaults to 32K output token limit, but Claude Sonnet/Opus/Haiku 4.5 models support 64K
19
+ - Added `claudeCode.maxOutputTokens` configuration in `config.lib.mjs` (default: 64000)
20
+ - Pass `CLAUDE_CODE_MAX_OUTPUT_TOKENS` environment variable when executing Claude CLI
21
+ - Configuration can be overridden via `CLAUDE_CODE_MAX_OUTPUT_TOKENS` or `HIVE_MIND_CLAUDE_CODE_MAX_OUTPUT_TOKENS` environment variables
22
+ - Added comprehensive case study analysis in `docs/case-studies/issue-1076/`
23
+
24
+ See: https://github.com/link-assistant/hive-mind/issues/1076
25
+
3
26
  ## 1.0.2
4
27
 
5
28
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -10,7 +10,7 @@ const path = (await use('path')).default;
10
10
  // Import log from general lib
11
11
  import { log, cleanErrorMessage } from './lib.mjs';
12
12
  import { reportError } from './sentry.lib.mjs';
13
- import { timeouts, retryLimits } from './config.lib.mjs';
13
+ import { timeouts, retryLimits, claudeCode, getClaudeEnv } from './config.lib.mjs';
14
14
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
15
15
  import { createInteractiveHandler } from './interactive-mode.lib.mjs';
16
16
  import { displayBudgetStats } from './claude.budget-stats.lib.mjs';
@@ -931,24 +931,17 @@ export const executeClaudeCommand = async params => {
931
931
  await log('', { verbose: true });
932
932
  }
933
933
  try {
934
+ const claudeEnv = getClaudeEnv(); // Set CLAUDE_CODE_MAX_OUTPUT_TOKENS (see issue #1076)
935
+ if (argv.verbose) await log(`📊 CLAUDE_CODE_MAX_OUTPUT_TOKENS: ${claudeCode.maxOutputTokens}`, { verbose: true });
934
936
  if (argv.resume) {
935
- // When resuming, pass prompt directly with -p flag
936
- // Use simpler escaping - just escape double quotes
937
+ // When resuming, pass prompt directly with -p flag. Escape double quotes for shell.
937
938
  const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
938
939
  const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
939
- execCommand = $({
940
- cwd: tempDir,
941
- mirror: false,
942
- })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
940
+ execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
943
941
  } else {
944
- // When not resuming, pass prompt via stdin
945
- // For system prompt, escape it properly for shell - just escape double quotes
942
+ // When not resuming, pass prompt via stdin. Escape double quotes for shell.
946
943
  const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
947
- execCommand = $({
948
- cwd: tempDir,
949
- stdin: prompt,
950
- mirror: false,
951
- })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} --append-system-prompt "${simpleEscapedSystem}"`;
944
+ execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} --append-system-prompt "${simpleEscapedSystem}"`;
952
945
  }
953
946
  await log(`${formatAligned('📋', 'Command details:', '')}`);
954
947
  await log(formatAligned('📂', 'Working directory:', tempDir, 2));
@@ -78,6 +78,20 @@ export const retryLimits = {
78
78
  initial503RetryDelayMs: parseIntWithDefault('HIVE_MIND_INITIAL_503_RETRY_DELAY_MS', 5 * 60 * 1000), // 5 minutes
79
79
  };
80
80
 
81
+ // Claude Code CLI configurations
82
+ // See: https://github.com/link-assistant/hive-mind/issues/1076
83
+ // Claude models support up to 64K output tokens, but Claude Code CLI defaults to 32K
84
+ // Setting a higher limit allows Claude to generate longer responses without hitting the limit
85
+ export const claudeCode = {
86
+ // Maximum output tokens for Claude Code CLI responses
87
+ // Default: 64000 (matches Claude Sonnet/Opus/Haiku 4.5 model capabilities)
88
+ // Set via CLAUDE_CODE_MAX_OUTPUT_TOKENS or HIVE_MIND_CLAUDE_CODE_MAX_OUTPUT_TOKENS
89
+ maxOutputTokens: parseIntWithDefault('CLAUDE_CODE_MAX_OUTPUT_TOKENS', parseIntWithDefault('HIVE_MIND_CLAUDE_CODE_MAX_OUTPUT_TOKENS', 64000)),
90
+ };
91
+
92
+ // Helper function to get Claude CLI environment with CLAUDE_CODE_MAX_OUTPUT_TOKENS set
93
+ export const getClaudeEnv = () => ({ ...process.env, CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(claudeCode.maxOutputTokens) });
94
+
81
95
  // Cache TTL configurations (in milliseconds)
82
96
  // The Usage API (Claude limits) has stricter rate limiting than regular APIs
83
97
  // See: https://github.com/link-assistant/hive-mind/issues/1074
@@ -190,6 +204,7 @@ export function getAllConfigurations() {
190
204
  githubLimits,
191
205
  systemLimits,
192
206
  retryLimits,
207
+ claudeCode,
193
208
  cacheTtl,
194
209
  filePaths,
195
210
  textProcessing,
@@ -16,6 +16,8 @@ import { reportError } from './sentry.lib.mjs';
16
16
  import { githubLimits, timeouts } from './config.lib.mjs';
17
17
  // Import batch operations from separate module
18
18
  import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRepositories as batchCheckArchived } from './github.batch.lib.mjs';
19
+ // Import log upload function from separate module
20
+ import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
19
21
 
20
22
  /**
21
23
  * Build cost estimation string for log comments
@@ -55,6 +57,7 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
55
57
  }
56
58
  return costInfo;
57
59
  };
60
+
58
61
  // Helper function to mask GitHub tokens (alias for backward compatibility)
59
62
  export const maskGitHubToken = maskToken;
60
63
  // Helper function to get GitHub tokens from local config files
@@ -649,7 +652,7 @@ ${logContent}
649
652
  let commentResult;
650
653
  if (logComment.length > githubLimits.commentMaxSize) {
651
654
  await log(` ⚠️ Log comment too long (${logComment.length} chars), GitHub limit is ${githubLimits.commentMaxSize} chars`);
652
- await log(' 📎 Uploading log as GitHub Gist instead...');
655
+ await log(' 📎 Uploading log using gh-upload-log...');
653
656
  try {
654
657
  // Check if repository is public or private
655
658
  let isPublicRepo = true;
@@ -670,49 +673,36 @@ ${logContent}
670
673
  repo,
671
674
  });
672
675
  // Default to public if we can't determine visibility
673
- await log(' ⚠️ Could not determine repository visibility, defaulting to public gist', { verbose: true });
676
+ await log(' ⚠️ Could not determine repository visibility, defaulting to public', { verbose: true });
674
677
  }
675
- // Create gist with appropriate visibility
676
- // Note: Gists don't need escaping because they are uploaded as plain text files
678
+ // Create temp log file with sanitized content (no compression, just gh-upload-log)
677
679
  const tempLogFile = `/tmp/solution-draft-log-${targetType}-${Date.now()}.txt`;
678
- // Use the original sanitized content (before escaping) for gist since it's a text file
680
+ // Use the original sanitized content for upload since it's a plain text file
679
681
  await fs.writeFile(tempLogFile, await sanitizeLogContent(rawLogContent));
680
- const gistCommand = isPublicRepo ? `gh gist create "${tempLogFile}" --public --desc "Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}" --filename "solution-draft-log.txt"` : `gh gist create "${tempLogFile}" --desc "Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}" --filename "solution-draft-log.txt"`;
681
- if (verbose) {
682
- await log(` 🔐 Creating ${isPublicRepo ? 'public' : 'private'} gist...`, { verbose: true });
683
- }
684
- const gistResult = await $(gistCommand);
682
+
683
+ // Use gh-upload-log to upload the log file
684
+ const uploadDescription = `Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}`;
685
+ const uploadResult = await uploadLogWithGhUploadLog({
686
+ logFile: tempLogFile,
687
+ isPublic: isPublicRepo,
688
+ description: uploadDescription,
689
+ verbose,
690
+ });
685
691
  await fs.unlink(tempLogFile).catch(() => {});
686
- if (gistResult.code === 0) {
687
- const gistPageUrl = gistResult.stdout.toString().trim();
688
- // Extract gist ID from URL
689
- const gistId = gistPageUrl.split('/').pop();
690
- // Construct raw file URL
691
- // Format: https://gist.githubusercontent.com/{owner}/{gist_id}/raw/{commit_sha}/{filename}
692
- // We use gh api to get the gist details for owner and commit SHA
693
- let gistUrl = gistPageUrl; // fallback to page URL if we can't get raw URL
694
-
695
- const gistDetailsResult = await $`gh api gists/${gistId} --jq '{owner: .owner.login, files: .files, history: .history}'`;
696
- if (gistDetailsResult.code === 0) {
697
- const gistDetails = JSON.parse(gistDetailsResult.stdout.toString());
698
- const commitSha = gistDetails.history && gistDetails.history[0] ? gistDetails.history[0].version : null;
699
- // Get the actual filename from the gist API response (--filename flag only works with stdin)
700
- const fileNames = gistDetails.files ? Object.keys(gistDetails.files) : [];
701
- const fileName = fileNames.length > 0 ? fileNames[0] : 'solution-draft-log.txt';
702
-
703
- if (commitSha) {
704
- gistUrl = `https://gist.githubusercontent.com/${gistDetails.owner}/${gistId}/raw/${commitSha}/${fileName}`;
705
- } else {
706
- // Fallback: use simpler format without commit SHA (GitHub will redirect to latest)
707
- gistUrl = `https://gist.githubusercontent.com/${gistDetails.owner}/${gistId}/raw/${fileName}`;
708
- }
709
- }
710
- // Create comment with gist link
711
- let gistComment;
692
+
693
+ if (uploadResult.success) {
694
+ // Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks)
695
+ // Requirements: 1 chunk = direct raw link, >1 chunks = repo link
696
+ const logUrl = uploadResult.chunks === 1 ? uploadResult.rawUrl : uploadResult.url;
697
+ const uploadTypeLabel = uploadResult.type === 'gist' ? 'Gist' : 'Repository';
698
+ const chunkInfo = uploadResult.chunks > 1 ? ` (${uploadResult.chunks} chunks)` : '';
699
+
700
+ // Create comment with log link
701
+ let logUploadComment;
712
702
  // For usage limit cases, always use the dedicated format regardless of errorMessage
713
703
  if (isUsageLimit) {
714
- // Usage limit error gist format
715
- gistComment = `## ⏳ Usage Limit Reached
704
+ // Usage limit error format
705
+ logUploadComment = `## ⏳ Usage Limit Reached
716
706
 
717
707
  The automated solution draft was interrupted because the ${toolName} usage limit was reached.
718
708
 
@@ -721,86 +711,86 @@ The automated solution draft was interrupted because the ${toolName} usage limit
721
711
  - **Limit Type**: Usage limit exceeded`;
722
712
 
723
713
  if (limitResetTime) {
724
- gistComment += `\n- **Reset Time**: ${limitResetTime}`;
714
+ logUploadComment += `\n- **Reset Time**: ${limitResetTime}`;
725
715
  }
726
716
 
727
717
  if (sessionId) {
728
- gistComment += `\n- **Session ID**: ${sessionId}`;
718
+ logUploadComment += `\n- **Session ID**: ${sessionId}`;
729
719
  }
730
720
 
731
- gistComment += '\n\n### 🔄 How to Continue\n';
721
+ logUploadComment += '\n\n### 🔄 How to Continue\n';
732
722
 
733
723
  if (limitResetTime) {
734
- gistComment += `Once the limit resets at **${limitResetTime}**, `;
724
+ logUploadComment += `Once the limit resets at **${limitResetTime}**, `;
735
725
  } else {
736
- gistComment += 'Once the limit resets, ';
726
+ logUploadComment += 'Once the limit resets, ';
737
727
  }
738
728
 
739
729
  if (resumeCommand) {
740
- gistComment += `you can resume this session by running:
730
+ logUploadComment += `you can resume this session by running:
741
731
  \`\`\`bash
742
732
  ${resumeCommand}
743
733
  \`\`\``;
744
734
  } else if (sessionId) {
745
- gistComment += `you can resume this session using session ID: \`${sessionId}\``;
735
+ logUploadComment += `you can resume this session using session ID: \`${sessionId}\``;
746
736
  } else {
747
- gistComment += 'you can retry the operation.';
737
+ logUploadComment += 'you can retry the operation.';
748
738
  }
749
739
 
750
- gistComment += `
740
+ logUploadComment += `
751
741
 
752
- 📎 **Execution log uploaded as GitHub Gist** (${Math.round(logStats.size / 1024)}KB)
753
- 🔗 [View complete execution log](${gistUrl})
742
+ 📎 **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
743
+ 🔗 [View complete execution log](${logUrl})
754
744
 
755
745
  ---
756
746
  *This session was interrupted due to usage limits. You can resume once the limit resets.*`;
757
747
  } else if (errorMessage) {
758
- // Failure log gist format (non-usage-limit errors)
759
- gistComment = `## 🚨 Solution Draft Failed
748
+ // Failure log format (non-usage-limit errors)
749
+ logUploadComment = `## 🚨 Solution Draft Failed
760
750
  The automated solution draft encountered an error:
761
751
  \`\`\`
762
752
  ${errorMessage}
763
753
  \`\`\`
764
- 📎 **Failure log uploaded as GitHub Gist** (${Math.round(logStats.size / 1024)}KB)
765
- 🔗 [View complete failure log](${gistUrl})
754
+ 📎 **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
755
+ 🔗 [View complete failure log](${logUrl})
766
756
  ---
767
757
  *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
768
758
  } else {
769
- // Success log gist format - use helper function for cost info
759
+ // Success log format - use helper function for cost info
770
760
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
771
- gistComment = `## ${customTitle}
761
+ logUploadComment = `## ${customTitle}
772
762
  This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
773
- 📎 **Log file uploaded as GitHub Gist** (${Math.round(logStats.size / 1024)}KB)
774
- 🔗 [View complete solution draft log](${gistUrl})
763
+ 📎 **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
764
+ 🔗 [View complete solution draft log](${logUrl})
775
765
  ---
776
766
  *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
777
767
  }
778
- const tempGistCommentFile = `/tmp/log-gist-comment-${targetType}-${Date.now()}.md`;
779
- await fs.writeFile(tempGistCommentFile, gistComment);
780
- commentResult = await $`gh ${ghCommand} comment ${targetNumber} --repo ${owner}/${repo} --body-file "${tempGistCommentFile}"`;
781
- await fs.unlink(tempGistCommentFile).catch(() => {});
768
+ const tempCommentFile = `/tmp/log-upload-comment-${targetType}-${Date.now()}.md`;
769
+ await fs.writeFile(tempCommentFile, logUploadComment);
770
+ commentResult = await $`gh ${ghCommand} comment ${targetNumber} --repo ${owner}/${repo} --body-file "${tempCommentFile}"`;
771
+ await fs.unlink(tempCommentFile).catch(() => {});
782
772
  if (commentResult.code === 0) {
783
- await log(` ✅ Solution draft log uploaded to ${targetName} as ${isPublicRepo ? 'public' : 'private'} Gist`);
784
- await log(` 🔗 Gist URL: ${gistUrl}`);
773
+ await log(` ✅ Solution draft log uploaded to ${targetName} as ${isPublicRepo ? 'public' : 'private'} ${uploadTypeLabel}${chunkInfo}`);
774
+ await log(` 🔗 Log URL: ${logUrl}`);
785
775
  await log(` 📊 Log size: ${Math.round(logStats.size / 1024)}KB`);
786
776
  return true;
787
777
  } else {
788
- await log(` ❌ Failed to upload comment with gist link: ${commentResult.stderr ? commentResult.stderr.toString().trim() : 'unknown error'}`);
778
+ await log(` ❌ Failed to post comment with log link: ${commentResult.stderr ? commentResult.stderr.toString().trim() : 'unknown error'}`);
789
779
  return false;
790
780
  }
791
781
  } else {
792
- await log(`Failed to create gist: ${gistResult.stderr ? gistResult.stderr.toString().trim() : 'unknown error'}`);
782
+ await log('gh-upload-log failed');
793
783
 
794
784
  // Fallback to truncated comment
795
785
  await log(' 🔄 Falling back to truncated comment...');
796
786
  return await attachTruncatedLog(options);
797
787
  }
798
- } catch (gistError) {
799
- reportError(gistError, {
800
- context: 'create_gist',
788
+ } catch (uploadError) {
789
+ reportError(uploadError, {
790
+ context: 'upload_log_gh_upload_log',
801
791
  level: 'error',
802
792
  });
803
- await log(` ❌ Error creating gist: ${gistError.message}`);
793
+ await log(` ❌ Error uploading log: ${uploadError.message}`);
804
794
  // Try regular comment as last resort
805
795
  return await attachRegularComment(options, logComment);
806
796
  }
@@ -1471,6 +1461,8 @@ export async function detectRepositoryVisibility(owner, repo) {
1471
1461
  }
1472
1462
  // Re-export batch archived check from separate module
1473
1463
  export const batchCheckArchivedRepositories = batchCheckArchived;
1464
+ // Re-export log upload function from separate module
1465
+ export { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
1474
1466
  // Export all functions as default object too
1475
1467
  export default {
1476
1468
  maskGitHubToken,
@@ -1482,6 +1474,7 @@ export default {
1482
1474
  checkGitHubPermissions,
1483
1475
  checkRepositoryWritePermission,
1484
1476
  attachLogToGitHub,
1477
+ uploadLogWithGhUploadLog,
1485
1478
  fetchAllIssuesWithPagination,
1486
1479
  fetchProjectIssues,
1487
1480
  isRateLimitError,
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Log upload module for hive-mind
4
+ // Uses gh-upload-log for uploading log files to GitHub
5
+
6
+ // Use use-m to dynamically import modules for cross-runtime compatibility
7
+ if (typeof globalThis.use === 'undefined') {
8
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
9
+ }
10
+ const use = globalThis.use;
11
+
12
+ // Use command-stream for consistent $ behavior across runtimes
13
+ const { $ } = await use('command-stream');
14
+
15
+ // Import shared library functions
16
+ const lib = await import('./lib.mjs');
17
+ const { log } = lib;
18
+
19
+ // Import Sentry integration
20
+ const sentryLib = await import('./sentry.lib.mjs');
21
+ const { reportError } = sentryLib;
22
+
23
+ /**
24
+ * Upload a log file using gh-upload-log command
25
+ * @param {Object} options - Upload options
26
+ * @param {string} options.logFile - Path to the log file to upload
27
+ * @param {boolean} options.isPublic - Whether to make the upload public
28
+ * @param {string} options.description - Description for the upload
29
+ * @param {boolean} [options.verbose=false] - Enable verbose logging
30
+ * @returns {Promise<{success: boolean, url: string|null, rawUrl: string|null, type: 'gist'|'repository'|null, chunks: number}>}
31
+ */
32
+ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description, verbose = false }) => {
33
+ const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
34
+
35
+ try {
36
+ // Build command with appropriate flags
37
+ const publicFlag = isPublic ? '--public' : '--private';
38
+ const descFlag = description ? `--description "${description}"` : '';
39
+ const verboseFlag = verbose ? '--verbose' : '';
40
+
41
+ const command = `gh-upload-log "${logFile}" ${publicFlag} ${descFlag} ${verboseFlag}`.trim().replace(/\s+/g, ' ');
42
+
43
+ if (verbose) {
44
+ await log(` 📤 Running: ${command}`, { verbose: true });
45
+ }
46
+
47
+ const uploadResult = await $`gh-upload-log "${logFile}" ${publicFlag} ${verbose ? '--verbose' : ''}`;
48
+ const output = (uploadResult.stdout?.toString() || '') + (uploadResult.stderr?.toString() || '');
49
+
50
+ if (uploadResult.code !== 0) {
51
+ await log(` ❌ gh-upload-log failed: ${output}`);
52
+ return result;
53
+ }
54
+
55
+ // Parse output to extract URL and type
56
+ // Look for the URL line: 🔗 https://...
57
+ const urlMatch = output.match(/🔗\s+(https:\/\/[^\s\n]+)/);
58
+ if (urlMatch) {
59
+ result.url = urlMatch[1].trim();
60
+ }
61
+
62
+ // Determine type from output
63
+ if (output.includes('Type: 📝 Gist') || result.url?.includes('gist.github.com')) {
64
+ result.type = 'gist';
65
+ } else if (output.includes('Type: 📦 Repository') || (result.url?.includes('github.com') && !result.url?.includes('gist'))) {
66
+ result.type = 'repository';
67
+ }
68
+
69
+ // Extract chunk count if mentioned
70
+ const chunkMatch = output.match(/split into (\d+) chunks/i);
71
+ if (chunkMatch) {
72
+ result.chunks = parseInt(chunkMatch[1], 10);
73
+ }
74
+
75
+ // Construct raw URL based on type and chunks
76
+ if (result.url) {
77
+ result.success = true;
78
+
79
+ if (result.type === 'gist') {
80
+ // For gist: get raw URL from gist API
81
+ const gistId = result.url.split('/').pop();
82
+ try {
83
+ const gistDetailsResult = await $`gh api gists/${gistId} --jq '{owner: .owner.login, files: .files, history: .history}'`;
84
+ if (gistDetailsResult.code === 0) {
85
+ const gistDetails = JSON.parse(gistDetailsResult.stdout.toString());
86
+ const gistOwner = gistDetails.owner;
87
+ const commitSha = gistDetails.history?.[0]?.version;
88
+ const fileNames = gistDetails.files ? Object.keys(gistDetails.files) : [];
89
+ const fileName = fileNames.length > 0 ? fileNames[0] : 'log.txt';
90
+
91
+ if (commitSha) {
92
+ result.rawUrl = `https://gist.githubusercontent.com/${gistOwner}/${gistId}/raw/${commitSha}/${fileName}`;
93
+ } else {
94
+ result.rawUrl = `https://gist.githubusercontent.com/${gistOwner}/${gistId}/raw/${fileName}`;
95
+ }
96
+ }
97
+ } catch (apiError) {
98
+ if (verbose) {
99
+ await log(` ⚠️ Could not get gist raw URL: ${apiError.message}`, { verbose: true });
100
+ }
101
+ // Use page URL as fallback
102
+ result.rawUrl = result.url;
103
+ }
104
+ } else if (result.type === 'repository') {
105
+ if (result.chunks === 1) {
106
+ // For single chunk repository: construct raw URL to the file
107
+ // Repository URL format: https://github.com/owner/repo
108
+ // We need to find the actual file name in the repo
109
+ try {
110
+ const repoUrl = result.url;
111
+ const repoPath = repoUrl.replace('https://github.com/', '');
112
+ const contentsResult = await $`gh api repos/${repoPath}/contents --jq '.[].name'`;
113
+ if (contentsResult.code === 0) {
114
+ const files = contentsResult.stdout
115
+ .toString()
116
+ .trim()
117
+ .split('\n')
118
+ .filter(f => f && !f.startsWith('.'));
119
+ if (files.length > 0) {
120
+ const fileName = files[0];
121
+ result.rawUrl = `${repoUrl}/raw/main/${fileName}`;
122
+ }
123
+ }
124
+ } catch (apiError) {
125
+ if (verbose) {
126
+ await log(` ⚠️ Could not get repo file raw URL: ${apiError.message}`, { verbose: true });
127
+ }
128
+ // For single chunk, try common pattern
129
+ result.rawUrl = result.url;
130
+ }
131
+ } else {
132
+ // For multiple chunks: link to repository itself (not raw)
133
+ result.rawUrl = result.url;
134
+ }
135
+ }
136
+ }
137
+
138
+ if (verbose) {
139
+ await log(` ✅ Upload successful: ${result.url}`, { verbose: true });
140
+ await log(` 📊 Type: ${result.type}, Chunks: ${result.chunks}`, { verbose: true });
141
+ if (result.rawUrl !== result.url) {
142
+ await log(` 🔗 Raw URL: ${result.rawUrl}`, { verbose: true });
143
+ }
144
+ }
145
+
146
+ return result;
147
+ } catch (error) {
148
+ reportError(error, {
149
+ context: 'upload_log_with_gh_upload_log',
150
+ logFile,
151
+ operation: 'gh_upload_log_command',
152
+ });
153
+ await log(` ❌ Error running gh-upload-log: ${error.message}`);
154
+ return result;
155
+ }
156
+ };
157
+
158
+ // Export all functions as default object too
159
+ export default {
160
+ uploadLogWithGhUploadLog,
161
+ };