@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 +20 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +155 -27
- package/src/github.lib.mjs +12 -4
- package/src/solve.repository.lib.mjs +125 -41
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
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
|
-
|
|
173
|
+
let originalProvider = getOriginalProviderName(providerFromModel);
|
|
126
174
|
|
|
127
175
|
try {
|
|
128
176
|
// Fetch model info from models.dev API
|
|
129
|
-
|
|
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
|
|
132
|
-
const
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
650
|
-
|
|
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
|
-
//
|
|
668
|
-
|
|
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:
|
|
676
|
-
await log(` Output tokens:
|
|
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:
|
|
682
|
-
await log(` Cache write:
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
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 {
|
package/src/github.lib.mjs
CHANGED
|
@@ -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
|
|
51
|
-
if
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
890
|
-
|
|
922
|
+
const maxRetries = 3;
|
|
923
|
+
const baseDelay = 2000; // Start with 2 seconds
|
|
891
924
|
|
|
892
|
-
|
|
893
|
-
const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
|
|
925
|
+
await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
|
|
894
926
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
await
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
if (
|
|
914
|
-
await log('
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|