@link-assistant/hive-mind 1.21.0 → 1.21.2

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,25 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.21.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 586b84d: Add retry mechanism for GitHub 500 errors during repository clone
8
+
9
+ This change adds intelligent retry logic with exponential backoff to handle transient GitHub server errors during repository cloning operations.
10
+
11
+ ## 1.21.1
12
+
13
+ ### Patch Changes
14
+
15
+ - fbfc0c3: Fix `--tool agent` pricing display for free models (Issue #1250)
16
+ - Add base model pricing lookup for free model variants (e.g., `kimi-k2.5-free` → `kimi-k2.5`)
17
+ - Show actual market price as "Public pricing estimate" based on the underlying paid model
18
+ - Display base model reference in cost output: "(based on Moonshot AI kimi-k2.5 prices)"
19
+ - Distinguish between truly free models and free access to paid models
20
+ - Fix token usage showing "0 input, 0 output" by accumulating tokens during streaming
21
+ - Token accumulation now happens in real-time as step_finish events arrive, avoiding NDJSON concatenation issues
22
+
3
23
  ## 1.21.0
4
24
 
5
25
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.21.0",
3
+ "version": "1.21.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/agent.lib.mjs CHANGED
@@ -100,11 +100,59 @@ const getOriginalProviderName = providerId => {
100
100
  return providerMap[providerId] || providerId.charAt(0).toUpperCase() + providerId.slice(1);
101
101
  };
102
102
 
103
+ /**
104
+ * Issue #1250: Normalize model name and find base model for pricing lookup
105
+ * Free models like "kimi-k2.5-free" should use pricing from base model "kimi-k2.5"
106
+ *
107
+ * @param {string} modelName - The model name (e.g., 'kimi-k2.5-free')
108
+ * @returns {Object} Object with:
109
+ * - baseModelName: The base model name for pricing lookup
110
+ * - isFreeVariant: Whether this is a free variant
111
+ */
112
+ const getBaseModelForPricing = modelName => {
113
+ // Known mappings for free models to their base paid versions
114
+ const freeToBaseMap = {
115
+ 'kimi-k2.5-free': 'kimi-k2.5',
116
+ 'glm-4.7-free': 'glm-4.7',
117
+ 'minimax-m2.1-free': 'minimax-m2.1',
118
+ 'trinity-large-preview-free': 'trinity-large-preview',
119
+ // Grok models don't have a paid equivalent with same name
120
+ // These are kept as-is since they're truly free
121
+ };
122
+
123
+ // Check if there's a direct mapping
124
+ if (freeToBaseMap[modelName]) {
125
+ return {
126
+ baseModelName: freeToBaseMap[modelName],
127
+ isFreeVariant: true,
128
+ };
129
+ }
130
+
131
+ // Try removing "-free" suffix
132
+ if (modelName.endsWith('-free')) {
133
+ return {
134
+ baseModelName: modelName.replace(/-free$/, ''),
135
+ isFreeVariant: true,
136
+ };
137
+ }
138
+
139
+ // Not a free variant
140
+ return {
141
+ baseModelName: modelName,
142
+ isFreeVariant: false,
143
+ };
144
+ };
145
+
103
146
  /**
104
147
  * Calculate pricing for agent tool usage using models.dev API
105
148
  * Issue #1250: Shows actual provider (OpenCode Zen) and calculates public pricing estimate
106
149
  * based on original provider prices (Moonshot AI, OpenAI, Anthropic, etc.)
107
150
  *
151
+ * For free models like "kimi-k2.5-free", this function:
152
+ * 1. First fetches the free model info to get the model name
153
+ * 2. Then fetches the base model (e.g., "kimi-k2.5") for actual pricing
154
+ * 3. Calculates public pricing estimate based on the base model's cost
155
+ *
108
156
  * @param {string} modelId - The model ID used (e.g., 'opencode/grok-code')
109
157
  * @param {Object} tokenUsage - Token usage data from parseAgentTokenUsage
110
158
  * @returns {Object} Pricing information with:
@@ -122,40 +170,66 @@ export const calculateAgentPricing = async (modelId, tokenUsage) => {
122
170
  const providerFromModel = modelId.includes('/') ? modelId.split('/')[0] : null;
123
171
 
124
172
  // Get original provider name for pricing reference
125
- const originalProvider = getOriginalProviderName(providerFromModel);
173
+ let originalProvider = getOriginalProviderName(providerFromModel);
126
174
 
127
175
  try {
128
176
  // Fetch model info from models.dev API
129
- const modelInfo = await fetchModelInfo(modelName);
177
+ let modelInfo = await fetchModelInfo(modelName);
178
+
179
+ // Issue #1250: Check if model has zero pricing (free model from OpenCode Zen)
180
+ // If so, look up the base model for actual public pricing estimate
181
+ const { baseModelName, isFreeVariant } = getBaseModelForPricing(modelName);
182
+ let baseModelInfo = null;
183
+ let pricingCost = modelInfo?.cost;
184
+
185
+ if (modelInfo && modelInfo.cost && modelInfo.cost.input === 0 && modelInfo.cost.output === 0 && baseModelName !== modelName) {
186
+ // This is a free model with zero pricing - look up base model for public pricing
187
+ baseModelInfo = await fetchModelInfo(baseModelName);
188
+ if (baseModelInfo && baseModelInfo.cost) {
189
+ // Use base model pricing for public estimate
190
+ pricingCost = baseModelInfo.cost;
191
+ // Update original provider from base model if available
192
+ if (baseModelInfo.provider && !originalProvider) {
193
+ originalProvider = baseModelInfo.provider;
194
+ }
195
+ }
196
+ }
130
197
 
131
- if (modelInfo && modelInfo.cost) {
132
- const cost = modelInfo.cost;
198
+ if (modelInfo || baseModelInfo) {
199
+ const effectiveModelInfo = modelInfo || baseModelInfo;
200
+ const cost = pricingCost || { input: 0, output: 0, cache_read: 0, cache_write: 0, reasoning: 0 };
133
201
 
134
202
  // Calculate public pricing estimate based on original provider prices
135
203
  // Prices are per 1M tokens, so divide by 1,000,000
204
+ // All priced components from models.dev: input, output, cache_read, cache_write, reasoning
136
205
  const inputCost = (tokenUsage.inputTokens * (cost.input || 0)) / 1_000_000;
137
206
  const outputCost = (tokenUsage.outputTokens * (cost.output || 0)) / 1_000_000;
138
207
  const cacheReadCost = (tokenUsage.cacheReadTokens * (cost.cache_read || 0)) / 1_000_000;
139
208
  const cacheWriteCost = (tokenUsage.cacheWriteTokens * (cost.cache_write || 0)) / 1_000_000;
209
+ const reasoningCost = (tokenUsage.reasoningTokens * (cost.reasoning || 0)) / 1_000_000;
140
210
 
141
- const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
211
+ const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost + reasoningCost;
142
212
 
143
213
  // Determine if this is a free model from OpenCode Zen
144
214
  // Models accessed via OpenCode Zen are free, regardless of original provider pricing
145
- const isOpencodeFreeModel = providerFromModel === 'opencode' || modelName.toLowerCase().includes('free') || modelName.toLowerCase().includes('grok') || providerFromModel === 'moonshot' || providerFromModel === 'openai' || providerFromModel === 'anthropic';
215
+ const isOpencodeFreeModel = providerFromModel === 'opencode' || isFreeVariant || modelName.toLowerCase().includes('free') || modelName.toLowerCase().includes('grok') || providerFromModel === 'moonshot' || providerFromModel === 'openai' || providerFromModel === 'anthropic';
216
+
217
+ // Use base model's provider for original provider reference if available
218
+ const effectiveOriginalProvider = baseModelInfo?.provider || originalProvider || effectiveModelInfo?.provider || null;
146
219
 
147
220
  return {
148
221
  modelId,
149
- modelName: modelInfo.name || modelName,
222
+ modelName: effectiveModelInfo?.name || modelName,
150
223
  // Issue #1250: Always show OpenCode Zen as actual provider
151
224
  provider: 'OpenCode Zen',
152
225
  // Store original provider for reference in pricing display
153
- originalProvider: originalProvider || modelInfo.provider || null,
226
+ originalProvider: effectiveOriginalProvider,
154
227
  pricing: {
155
228
  inputPerMillion: cost.input || 0,
156
229
  outputPerMillion: cost.output || 0,
157
230
  cacheReadPerMillion: cost.cache_read || 0,
158
231
  cacheWritePerMillion: cost.cache_write || 0,
232
+ reasoningPerMillion: cost.reasoning || 0,
159
233
  },
160
234
  tokenUsage,
161
235
  breakdown: {
@@ -163,15 +237,18 @@ export const calculateAgentPricing = async (modelId, tokenUsage) => {
163
237
  output: outputCost,
164
238
  cacheRead: cacheReadCost,
165
239
  cacheWrite: cacheWriteCost,
240
+ reasoning: reasoningCost,
166
241
  },
167
- // Public pricing estimate based on original provider prices
242
+ // Public pricing estimate based on original/base model prices
168
243
  totalCostUSD: totalCost,
169
244
  // Actual cost from OpenCode Zen (free for supported models)
170
245
  opencodeCost: isOpencodeFreeModel ? 0 : totalCost,
171
- // Keep for backward compatibility - indicates if model has zero pricing
172
- isFreeModel: cost.input === 0 && cost.output === 0,
246
+ // Keep for backward compatibility - indicates if the accessed model has zero pricing
247
+ isFreeModel: modelInfo?.cost?.input === 0 && modelInfo?.cost?.output === 0,
173
248
  // New flag to indicate if OpenCode Zen provides this model for free
174
249
  isOpencodeFreeModel,
250
+ // Issue #1250: Include base model info for transparency
251
+ baseModelName: baseModelName !== modelName ? baseModelName : null,
175
252
  };
176
253
  }
177
254
  // Model not found in API, return what we have
@@ -476,11 +553,39 @@ export const executeAgentCommand = async params => {
476
553
  let limitReached = false;
477
554
  let limitResetTime = null;
478
555
  let lastMessage = '';
479
- let fullOutput = ''; // Collect all output for pricing calculation and error detection
556
+ let fullOutput = ''; // Collect all output for error detection (kept for backward compatibility)
480
557
  // Issue #1201: Track error events detected during streaming for reliable error detection
481
558
  // Post-hoc detection on fullOutput can miss errors if NDJSON lines get concatenated without newlines
482
559
  let streamingErrorDetected = false;
483
560
  let streamingErrorMessage = null;
561
+ // Issue #1250: Accumulate token usage during streaming instead of parsing fullOutput later
562
+ // This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
563
+ const streamingTokenUsage = {
564
+ inputTokens: 0,
565
+ outputTokens: 0,
566
+ reasoningTokens: 0,
567
+ cacheReadTokens: 0,
568
+ cacheWriteTokens: 0,
569
+ totalCost: 0,
570
+ stepCount: 0,
571
+ };
572
+ // Helper to accumulate tokens from step_finish events during streaming
573
+ const accumulateTokenUsage = data => {
574
+ if (data.type === 'step_finish' && data.part?.tokens) {
575
+ const tokens = data.part.tokens;
576
+ streamingTokenUsage.stepCount++;
577
+ if (tokens.input) streamingTokenUsage.inputTokens += tokens.input;
578
+ if (tokens.output) streamingTokenUsage.outputTokens += tokens.output;
579
+ if (tokens.reasoning) streamingTokenUsage.reasoningTokens += tokens.reasoning;
580
+ if (tokens.cache) {
581
+ if (tokens.cache.read) streamingTokenUsage.cacheReadTokens += tokens.cache.read;
582
+ if (tokens.cache.write) streamingTokenUsage.cacheWriteTokens += tokens.cache.write;
583
+ }
584
+ if (data.part.cost !== undefined) {
585
+ streamingTokenUsage.totalCost += data.part.cost;
586
+ }
587
+ }
588
+ };
484
589
 
485
590
  for await (const chunk of execCommand.stream()) {
486
591
  if (chunk.type === 'stdout') {
@@ -500,6 +605,8 @@ export const executeAgentCommand = async params => {
500
605
  sessionId = data.sessionID;
501
606
  await log(`📌 Session ID: ${sessionId}`);
502
607
  }
608
+ // Issue #1250: Accumulate token usage during streaming
609
+ accumulateTokenUsage(data);
503
610
  // Issue #1201: Detect error events during streaming for reliable detection
504
611
  if (data.type === 'error' || data.type === 'step_error') {
505
612
  streamingErrorDetected = true;
@@ -532,6 +639,8 @@ export const executeAgentCommand = async params => {
532
639
  sessionId = stderrData.sessionID;
533
640
  await log(`📌 Session ID: ${sessionId}`);
534
641
  }
642
+ // Issue #1250: Accumulate token usage during streaming (stderr)
643
+ accumulateTokenUsage(stderrData);
535
644
  // Issue #1201: Detect error events during streaming (stderr) for reliable detection
536
645
  if (stderrData.type === 'error' || stderrData.type === 'step_error') {
537
646
  streamingErrorDetected = true;
@@ -646,8 +755,9 @@ export const executeAgentCommand = async params => {
646
755
  await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
647
756
  await log(` Load: ${resourcesAfter.load}`, { verbose: true });
648
757
 
649
- // Parse token usage even on failure (partial work may have been done)
650
- const tokenUsage = parseAgentTokenUsage(fullOutput);
758
+ // Issue #1250: Use streaming-accumulated token usage instead of re-parsing fullOutput
759
+ // This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
760
+ const tokenUsage = streamingTokenUsage;
651
761
  const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
652
762
 
653
763
  return {
@@ -664,29 +774,47 @@ export const executeAgentCommand = async params => {
664
774
 
665
775
  await log('\n\n✅ Agent command completed');
666
776
 
667
- // Parse token usage from collected output
668
- const tokenUsage = parseAgentTokenUsage(fullOutput);
777
+ // Issue #1250: Use streaming-accumulated token usage instead of re-parsing fullOutput
778
+ // This fixes the issue where NDJSON lines get concatenated without newlines, breaking JSON.parse
779
+ const tokenUsage = streamingTokenUsage;
669
780
  const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
670
781
 
671
- // Log pricing information
782
+ // Log pricing information (similar to --tool claude breakdown)
672
783
  if (tokenUsage.stepCount > 0) {
673
784
  await log('\n💰 Token Usage Summary:');
674
- await log(` 📊 ${pricingInfo.modelName || mappedModel}:`);
675
- await log(` Input tokens: ${tokenUsage.inputTokens.toLocaleString()}`);
676
- await log(` Output tokens: ${tokenUsage.outputTokens.toLocaleString()}`);
785
+ await log(` 📊 ${pricingInfo.modelName || mappedModel} (${tokenUsage.stepCount} steps):`);
786
+ await log(` Input tokens: ${tokenUsage.inputTokens.toLocaleString()}`);
787
+ await log(` Output tokens: ${tokenUsage.outputTokens.toLocaleString()}`);
677
788
  if (tokenUsage.reasoningTokens > 0) {
678
789
  await log(` Reasoning tokens: ${tokenUsage.reasoningTokens.toLocaleString()}`);
679
790
  }
680
791
  if (tokenUsage.cacheReadTokens > 0 || tokenUsage.cacheWriteTokens > 0) {
681
- await log(` Cache read: ${tokenUsage.cacheReadTokens.toLocaleString()}`);
682
- await log(` Cache write: ${tokenUsage.cacheWriteTokens.toLocaleString()}`);
792
+ await log(` Cache read: ${tokenUsage.cacheReadTokens.toLocaleString()}`);
793
+ await log(` Cache write: ${tokenUsage.cacheWriteTokens.toLocaleString()}`);
683
794
  }
684
795
 
685
- if (pricingInfo.totalCostUSD !== null) {
686
- if (pricingInfo.isFreeModel) {
687
- await log(' Cost: $0.00 (Free model)');
688
- } else {
689
- await log(` Cost: $${pricingInfo.totalCostUSD.toFixed(6)}`);
796
+ if (pricingInfo.totalCostUSD !== null && pricingInfo.breakdown) {
797
+ // Show per-component cost breakdown (similar to --tool claude)
798
+ await log(' Cost breakdown:');
799
+ await log(` Input: $${pricingInfo.breakdown.input.toFixed(6)} (${(pricingInfo.pricing?.inputPerMillion || 0).toFixed(2)}/M tokens)`);
800
+ await log(` Output: $${pricingInfo.breakdown.output.toFixed(6)} (${(pricingInfo.pricing?.outputPerMillion || 0).toFixed(2)}/M tokens)`);
801
+ if (tokenUsage.cacheReadTokens > 0) {
802
+ await log(` Cache read: $${pricingInfo.breakdown.cacheRead.toFixed(6)} (${(pricingInfo.pricing?.cacheReadPerMillion || 0).toFixed(2)}/M tokens)`);
803
+ }
804
+ if (tokenUsage.cacheWriteTokens > 0) {
805
+ await log(` Cache write: $${pricingInfo.breakdown.cacheWrite.toFixed(6)} (${(pricingInfo.pricing?.cacheWritePerMillion || 0).toFixed(2)}/M tokens)`);
806
+ }
807
+ if (tokenUsage.reasoningTokens > 0 && pricingInfo.breakdown.reasoning > 0) {
808
+ await log(` Reasoning: $${pricingInfo.breakdown.reasoning.toFixed(6)} (${(pricingInfo.pricing?.reasoningPerMillion || 0).toFixed(2)}/M tokens)`);
809
+ }
810
+ // Show public pricing estimate
811
+ const pricingRef = pricingInfo.baseModelName && pricingInfo.originalProvider ? ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)` : pricingInfo.originalProvider ? ` (based on ${pricingInfo.originalProvider} prices)` : '';
812
+ await log(` Public pricing estimate: $${pricingInfo.totalCostUSD.toFixed(6)}${pricingRef}`);
813
+ // Show actual OpenCode Zen cost
814
+ if (pricingInfo.isOpencodeFreeModel) {
815
+ await log(' Calculated by OpenCode Zen: $0.00 (Free model)');
816
+ } else if (pricingInfo.opencodeCost !== undefined) {
817
+ await log(` Calculated by OpenCode Zen: $${pricingInfo.opencodeCost.toFixed(6)}`);
690
818
  }
691
819
  await log(` Provider: ${pricingInfo.provider || 'OpenCode Zen'}`);
692
820
  } else {
@@ -30,6 +30,7 @@ import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
30
30
  * - opencodeCost: Actual billed cost from OpenCode Zen (for agent tool)
31
31
  * - isOpencodeFreeModel: Whether OpenCode Zen provides this model for free
32
32
  * - originalProvider: Original provider for pricing reference
33
+ * - baseModelName: Base model name if pricing was derived from base model (Issue #1250)
33
34
  * @returns {string} Formatted cost info string for markdown (empty if no data available)
34
35
  */
35
36
  const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
@@ -47,13 +48,20 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
47
48
  }
48
49
  // Issue #1250: Show public pricing estimate based on original provider prices
49
50
  if (hasPublic) {
50
- // For models with zero pricing from original provider, show as free
51
- if (pricingInfo?.isFreeModel && totalCostUSD === 0) {
51
+ // Issue #1250: For free models accessed via OpenCode Zen, show pricing based on base model
52
+ // Only show as completely free if the base model also has no pricing
53
+ if (pricingInfo?.isFreeModel && totalCostUSD === 0 && !pricingInfo?.baseModelName) {
52
54
  costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
53
55
  } else {
54
56
  // Show actual public pricing estimate with original provider reference
55
- const originalProviderRef = pricingInfo?.originalProvider ? ` (based on ${pricingInfo.originalProvider} prices)` : '';
56
- costInfo += `\n- Public pricing estimate: $${totalCostUSD.toFixed(6)}${originalProviderRef}`;
57
+ // Issue #1250: Include base model reference when pricing comes from base model
58
+ let pricingRef = '';
59
+ if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
60
+ pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
61
+ } else if (pricingInfo?.originalProvider) {
62
+ pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
63
+ }
64
+ costInfo += `\n- Public pricing estimate: $${totalCostUSD.toFixed(6)}${pricingRef}`;
57
65
  }
58
66
  } else if (hasPricing) {
59
67
  costInfo += '\n- Public pricing estimate: unknown';
@@ -884,56 +884,140 @@ Thank you!`;
884
884
  return { repoToClone, forkedRepo, upstreamRemote, prForkOwner: forkOwner };
885
885
  };
886
886
 
887
- // Clone repository and set up remotes
887
+ // Classify git clone errors to determine if they are retryable
888
+ export const classifyCloneError = errorOutput => {
889
+ const output = errorOutput.toLowerCase();
890
+
891
+ // Transient server errors (5xx) - typically retryable
892
+ if (output.includes('error: 500') || output.includes('internal server error') || output.includes('error: 502') || output.includes('error: 503') || output.includes('error: 504')) {
893
+ return { type: 'TRANSIENT', retryable: true, description: 'GitHub server error' };
894
+ }
895
+
896
+ // Network-related errors - typically retryable
897
+ if (output.includes('connection refused') || output.includes('connection timed out') || output.includes('connection reset') || output.includes('unable to connect') || output.includes('network is unreachable') || output.includes('ssl error')) {
898
+ return { type: 'NETWORK', retryable: true, description: 'Network connectivity issue' };
899
+ }
900
+
901
+ // Authentication/permission errors - not retryable
902
+ if (output.includes('error: 401') || output.includes('error: 403') || output.includes('authentication failed') || output.includes('permission denied')) {
903
+ return { type: 'PERMISSION', retryable: false, description: 'Authentication or permission error' };
904
+ }
905
+
906
+ // Repository not found - not retryable
907
+ if (output.includes('error: 404') || output.includes('not found') || output.includes('repository not found')) {
908
+ return { type: 'NOT_FOUND', retryable: false, description: 'Repository not found' };
909
+ }
910
+
911
+ // Rate limiting - retryable with backoff
912
+ if (output.includes('rate limit') || output.includes('too many requests') || output.includes('api rate limit exceeded')) {
913
+ return { type: 'RATE_LIMIT', retryable: true, description: 'Rate limit exceeded' };
914
+ }
915
+
916
+ // Default to retryable for unknown errors
917
+ return { type: 'UNKNOWN', retryable: true, description: 'Unknown error' };
918
+ };
919
+
920
+ // Clone repository and set up remotes with retry mechanism
888
921
  export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) => {
889
- // Clone the repository (or fork) using gh tool with authentication
890
- await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
922
+ const maxRetries = 3;
923
+ const baseDelay = 2000; // Start with 2 seconds
891
924
 
892
- // Use 2>&1 to capture all output and filter "Cloning into" message
893
- const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
925
+ await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
894
926
 
895
- // Verify clone was successful
896
- if (cloneResult.code !== 0) {
897
- const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
898
- await log('');
899
- await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
900
- await log('');
901
- await log(' 🔍 What happened:');
902
- await log(` Failed to clone repository ${repoToClone}`);
903
- await log('');
904
- await log(' 📦 Error details:');
905
- for (const line of errorOutput.split('\n')) {
906
- if (line.trim()) await log(` ${line}`);
927
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
928
+ if (attempt > 1) {
929
+ await log(`${formatAligned('⏳', 'Clone attempt:', `${attempt}/${maxRetries} (with retry logic)`)}`);
907
930
  }
908
- await log('');
909
- await log(' 💡 Common causes:');
910
- await log(" • Repository doesn't exist or is private");
911
- await log(' • No GitHub authentication');
912
- await log(' • Network connectivity issues');
913
- if (argv.fork) {
914
- await log(' Fork not ready yet (try again in a moment)');
931
+
932
+ // Use 2>&1 to capture all output and filter "Cloning into" message
933
+ const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
934
+
935
+ // Verify clone was successful
936
+ if (cloneResult.code === 0) {
937
+ await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
938
+
939
+ // Verify and fix remote configuration
940
+ const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
941
+ if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
942
+ await log(' Setting up git remote...', { verbose: true });
943
+ // Add origin remote manually
944
+ await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
945
+ }
946
+ return; // Success - exit function
915
947
  }
916
- await log('');
917
- await log(' 🔧 How to fix:');
918
- await log(' 1. Check authentication: gh auth status');
919
- await log(' 2. Login if needed: gh auth login');
920
- await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
921
- if (argv.fork) {
922
- await log(` 4. Check fork: gh repo view ${repoToClone}`);
948
+
949
+ // Clone failed - analyze error and determine if retry is appropriate
950
+ const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
951
+
952
+ const errorClassification = classifyCloneError(errorOutput);
953
+
954
+ if (!errorClassification.retryable || attempt === maxRetries) {
955
+ // Non-retryable error or max retries reached - fail with detailed error
956
+ await log('');
957
+ await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
958
+ await log('');
959
+ await log(' 🔍 What happened:');
960
+ await log(` Failed to clone repository ${repoToClone}`);
961
+
962
+ if (!errorClassification.retryable) {
963
+ await log(` Error type: ${errorClassification.description} (not retryable)`);
964
+ } else {
965
+ await log(` Error type: ${errorClassification.description} (max retries exceeded)`);
966
+ }
967
+ await log('');
968
+ await log(' 📦 Error details:');
969
+ for (const line of errorOutput.split('\n')) {
970
+ if (line.trim()) await log(` ${line}`);
971
+ }
972
+ await log('');
973
+ await log(' 💡 Common causes:');
974
+ await log(" • Repository doesn't exist or is private");
975
+ await log(' • No GitHub authentication');
976
+ await log(' • Network connectivity issues');
977
+ if (errorClassification.type === 'TRANSIENT') {
978
+ await log(' • GitHub server issues (temporary)');
979
+ }
980
+ if (errorClassification.type === 'RATE_LIMIT') {
981
+ await log(' • API rate limiting exceeded');
982
+ }
983
+ if (argv.fork) {
984
+ await log(' • Fork not ready yet (try again in a moment)');
985
+ }
986
+ await log('');
987
+ await log(' 🔧 How to fix:');
988
+ await log(' 1. Check authentication: gh auth status');
989
+ await log(' 2. Login if needed: gh auth login');
990
+ await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
991
+ if (argv.fork) {
992
+ await log(` 4. Check fork: gh repo view ${repoToClone}`);
993
+ }
994
+ if (errorClassification.type === 'TRANSIENT') {
995
+ await log(' 5. Wait a few minutes and retry (GitHub server issue)');
996
+ await log(' 6. Check GitHub status: https://www.githubstatus.com');
997
+ }
998
+ if (errorClassification.type === 'RATE_LIMIT') {
999
+ await log(' 5. Wait for rate limit to reset (check your quota)');
1000
+ await log(' 6. Use --token flag with different token if available');
1001
+ }
1002
+ await log('');
1003
+ await safeExit(1, 'Repository setup failed');
923
1004
  }
924
- await log('');
925
- await safeExit(1, 'Repository setup failed');
926
- }
927
1005
 
928
- await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
1006
+ // Retryable error and we have attempts left
1007
+ const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
1008
+ await log(`${formatAligned('⚠️', 'Clone failed:', errorClassification.description)}`);
1009
+ await log(`${formatAligned('⏳', 'Retrying:', `Waiting ${delay / 1000}s before attempt ${attempt + 1}/${maxRetries}...`)}`);
1010
+
1011
+ if (errorClassification.type === 'RATE_LIMIT') {
1012
+ await log(' 💡 Tip: Rate limiting detected - using longer delay');
1013
+ }
929
1014
 
930
- // Verify and fix remote configuration
931
- const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
932
- if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
933
- await log(' Setting up git remote...', { verbose: true });
934
- // Add origin remote manually
935
- await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
1015
+ await new Promise(resolve => setTimeout(resolve, delay));
936
1016
  }
1017
+
1018
+ // This should never be reached due to the loop logic above
1019
+ await log(`${formatAligned('❌', 'UNEXPECTED ERROR:', 'Clone logic failed')}`);
1020
+ await safeExit(1, 'Repository setup failed');
937
1021
  };
938
1022
 
939
1023
  // Set up upstream remote and sync fork