@link-assistant/hive-mind 1.62.0 ā 1.63.0
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 +12 -0
- package/README.hi.md +4 -2
- package/README.md +18 -15
- package/README.ru.md +4 -2
- package/README.zh.md +17 -15
- package/package.json +1 -1
- package/src/bidirectional-interactive.lib.mjs +1 -1
- package/src/claude.budget-stats.lib.mjs +49 -30
- package/src/claude.lib.mjs +11 -15
- package/src/config.lib.mjs +1 -0
- package/src/gemini.lib.mjs +611 -0
- package/src/gemini.prompts.lib.mjs +236 -0
- package/src/hive.config.lib.mjs +1 -1
- package/src/interactive-mode.lib.mjs +1 -1
- package/src/models/index.mjs +39 -8
- package/src/solve.config.lib.mjs +4 -4
- package/src/solve.mjs +33 -0
- package/src/solve.restart-shared.lib.mjs +47 -1
- package/src/solve.results.lib.mjs +1 -1
- package/src/solve.validation.lib.mjs +8 -0
- package/src/task.config.lib.mjs +1 -1
- package/src/task.mjs +1 -1
- package/src/telegram-bot.mjs +4 -4
- package/src/telegram-solve-command.lib.mjs +1 -0
- package/src/telegram-solve-queue-command.lib.mjs +1 -1
- package/src/telegram-solve-queue.helpers.lib.mjs +12 -1
- package/src/telegram-solve-queue.lib.mjs +37 -20
- package/src/usage-limit.lib.mjs +1 -1
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Google Gemini CLI-related utility functions
|
|
3
|
+
|
|
4
|
+
// Check if use is already defined (when imported from solve.mjs)
|
|
5
|
+
// If not, fetch it (when running standalone)
|
|
6
|
+
if (typeof globalThis.use === 'undefined') {
|
|
7
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { $ } = await use('command-stream');
|
|
11
|
+
const fs = (await use('fs')).promises;
|
|
12
|
+
const path = (await use('path')).default;
|
|
13
|
+
const os = (await use('os')).default;
|
|
14
|
+
|
|
15
|
+
import { log } from './lib.mjs';
|
|
16
|
+
import { reportError } from './sentry.lib.mjs';
|
|
17
|
+
import { timeouts, retryLimits } from './config.lib.mjs';
|
|
18
|
+
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
19
|
+
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
20
|
+
import { defaultModels, geminiModels } from './models/index.mjs';
|
|
21
|
+
import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
|
|
22
|
+
import { classifyRetryableError, getRetryDelayMs, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
23
|
+
|
|
24
|
+
const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
25
|
+
|
|
26
|
+
// Model mapping to translate aliases to full model IDs for Gemini.
|
|
27
|
+
// Issue #1473: Uses centralized geminiModels from models/index.mjs.
|
|
28
|
+
export const mapModelToId = model => {
|
|
29
|
+
return geminiModels[model] || model;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const extractGeminiTextContent = value => {
|
|
33
|
+
if (!value) return '';
|
|
34
|
+
if (typeof value === 'string') return value;
|
|
35
|
+
if (Array.isArray(value)) return value.map(extractGeminiTextContent).filter(Boolean).join('\n');
|
|
36
|
+
if (typeof value !== 'object') return '';
|
|
37
|
+
|
|
38
|
+
if (typeof value.text === 'string') return value.text;
|
|
39
|
+
if (typeof value.response === 'string') return value.response;
|
|
40
|
+
if (typeof value.result === 'string') return value.result;
|
|
41
|
+
if (typeof value.content === 'string') return value.content;
|
|
42
|
+
if (value.content) return extractGeminiTextContent(value.content);
|
|
43
|
+
if (value.message) return extractGeminiTextContent(value.message);
|
|
44
|
+
if (value.parts) return extractGeminiTextContent(value.parts);
|
|
45
|
+
if (value.delta) return extractGeminiTextContent(value.delta);
|
|
46
|
+
return '';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildGeminiResultModelUsage = (modelId, stats = null) => {
|
|
50
|
+
const modelStats = stats?.models && typeof stats.models === 'object' ? stats.models : null;
|
|
51
|
+
if (modelStats) {
|
|
52
|
+
const usage = {};
|
|
53
|
+
for (const [id, data] of Object.entries(modelStats)) {
|
|
54
|
+
const tokens = data?.tokens || {};
|
|
55
|
+
usage[id] = {
|
|
56
|
+
inputTokens: tokens.input || tokens.prompt || 0,
|
|
57
|
+
cacheCreationTokens: tokens.cacheWrite || 0,
|
|
58
|
+
cacheReadTokens: tokens.cacheRead || 0,
|
|
59
|
+
outputTokens: tokens.output || tokens.completion || 0,
|
|
60
|
+
modelName: data?.name || id,
|
|
61
|
+
modelInfo: null,
|
|
62
|
+
peakContextUsage: tokens.total || 0,
|
|
63
|
+
costUSD: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(usage).length > 0) return usage;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!modelId || modelId === 'auto') return null;
|
|
70
|
+
return {
|
|
71
|
+
[modelId]: {
|
|
72
|
+
inputTokens: 0,
|
|
73
|
+
cacheCreationTokens: 0,
|
|
74
|
+
cacheReadTokens: 0,
|
|
75
|
+
outputTokens: 0,
|
|
76
|
+
modelName: modelId,
|
|
77
|
+
modelInfo: null,
|
|
78
|
+
peakContextUsage: 0,
|
|
79
|
+
costUSD: null,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const applyGeminiJsonEvent = (event, nextState, modelId = null) => {
|
|
85
|
+
const data = sanitizeObjectStrings(event);
|
|
86
|
+
if (!data || typeof data !== 'object') return;
|
|
87
|
+
|
|
88
|
+
const type = String(data.type || data.event || data.kind || 'json');
|
|
89
|
+
nextState.eventCounts[type] = (nextState.eventCounts[type] || 0) + 1;
|
|
90
|
+
|
|
91
|
+
const emittedSessionId = data.sessionId || data.session_id || data.session?.id || data.chat?.id || null;
|
|
92
|
+
if (emittedSessionId) {
|
|
93
|
+
nextState.sessionId = emittedSessionId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (type.includes('message') || data.message || data.response || data.content || data.text) {
|
|
97
|
+
nextState.messageCount++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (type.includes('tool') || data.toolCall || data.tool_call || data.functionCall || data.function_call) {
|
|
101
|
+
nextState.toolUseCount++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const text = extractGeminiTextContent(data);
|
|
105
|
+
if (text) {
|
|
106
|
+
nextState.resultSummary = text;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (data.error) {
|
|
110
|
+
nextState.errorMessages.push(extractGeminiTextContent(data.error) || JSON.stringify(data.error));
|
|
111
|
+
} else if (type.toLowerCase().includes('error')) {
|
|
112
|
+
nextState.errorMessages.push(text || JSON.stringify(data));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const usage = buildGeminiResultModelUsage(modelId, data.stats || data.usage || null);
|
|
116
|
+
if (usage) {
|
|
117
|
+
nextState.resultModelUsage = usage;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const parseGeminiJsonOutput = (output, state = {}, modelId = null) => {
|
|
122
|
+
const nextState = {
|
|
123
|
+
messageCount: state.messageCount || 0,
|
|
124
|
+
toolUseCount: state.toolUseCount || 0,
|
|
125
|
+
resultSummary: state.resultSummary || '',
|
|
126
|
+
sessionId: state.sessionId || null,
|
|
127
|
+
errorMessages: [...(state.errorMessages || [])],
|
|
128
|
+
eventCounts: { ...(state.eventCounts || {}) },
|
|
129
|
+
resultModelUsage: state.resultModelUsage || null,
|
|
130
|
+
partialLine: state.partialLine || '',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const trimmedOutput = output.trim();
|
|
134
|
+
if (trimmedOutput && !nextState.partialLine) {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(trimmedOutput);
|
|
137
|
+
for (const event of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
138
|
+
applyGeminiJsonEvent(event, nextState, modelId);
|
|
139
|
+
}
|
|
140
|
+
return nextState;
|
|
141
|
+
} catch {
|
|
142
|
+
// stream-json emits one JSON object per line; fall through to JSONL parsing.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const bufferedOutput = `${nextState.partialLine}${output}`;
|
|
147
|
+
nextState.partialLine = '';
|
|
148
|
+
const lines = bufferedOutput.split(/\r?\n/);
|
|
149
|
+
const hasTrailingLineBreak = /\r?\n$/.test(bufferedOutput);
|
|
150
|
+
const completeLines = hasTrailingLineBreak ? lines : lines.slice(0, -1);
|
|
151
|
+
const possiblePartialLine = hasTrailingLineBreak ? '' : lines.at(-1) || '';
|
|
152
|
+
|
|
153
|
+
for (const line of completeLines) {
|
|
154
|
+
if (!line.trim()) continue;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
applyGeminiJsonEvent(JSON.parse(line), nextState, modelId);
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (possiblePartialLine.trim()) {
|
|
164
|
+
try {
|
|
165
|
+
applyGeminiJsonEvent(JSON.parse(possiblePartialLine), nextState, modelId);
|
|
166
|
+
} catch {
|
|
167
|
+
nextState.partialLine = possiblePartialLine;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return nextState;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Function to validate Gemini CLI connection
|
|
175
|
+
export const validateGeminiConnection = async (model = defaultModels.gemini) => {
|
|
176
|
+
const mappedModel = mapModelToId(model);
|
|
177
|
+
const geminiPath = process.env.GEMINI_PATH || 'gemini';
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await log('š Validating Gemini CLI connection...');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const versionResult = await $`timeout ${Math.floor(timeouts.geminiCli / 1000)} ${geminiPath} --version`;
|
|
184
|
+
if (versionResult.code === 0) {
|
|
185
|
+
const version = versionResult.stdout?.toString().trim();
|
|
186
|
+
await log(`š¦ Gemini CLI version: ${version}`);
|
|
187
|
+
}
|
|
188
|
+
} catch (versionError) {
|
|
189
|
+
await log(`ā ļø Gemini CLI version check failed (${versionError.code}), proceeding with connection test...`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const testResult = await $`printf "hi" | timeout ${Math.floor(timeouts.geminiCli / 1000)} ${geminiPath} --prompt "say hi" --output-format json --model ${mappedModel}`;
|
|
193
|
+
const stdout = testResult.stdout?.toString() || '';
|
|
194
|
+
const stderr = testResult.stderr?.toString() || '';
|
|
195
|
+
const combinedOutput = `${stdout}\n${stderr}`;
|
|
196
|
+
|
|
197
|
+
if (testResult.code !== 0) {
|
|
198
|
+
await log(`ā Gemini CLI validation failed with exit code ${testResult.code}`, { level: 'error' });
|
|
199
|
+
if (stderr) await log(` Error: ${stderr.trim()}`, { level: 'error' });
|
|
200
|
+
|
|
201
|
+
if (/auth|login|credential/i.test(combinedOutput)) {
|
|
202
|
+
await log(' š” Please authenticate or configure Gemini CLI credentials.', { level: 'error' });
|
|
203
|
+
}
|
|
204
|
+
if (/project/i.test(combinedOutput)) {
|
|
205
|
+
await log(' š” Please set GOOGLE_CLOUD_PROJECT if your Gemini CLI setup requires it.', { level: 'error' });
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const parsed = parseGeminiJsonOutput(stdout, {}, mappedModel);
|
|
211
|
+
if (parsed.errorMessages.length > 0) {
|
|
212
|
+
await log(`ā Gemini CLI validation returned an error: ${parsed.errorMessages.join('; ')}`, { level: 'error' });
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await log('ā
Gemini CLI connection validated successfully');
|
|
217
|
+
return true;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
await log(`ā Failed to validate Gemini CLI connection: ${error.message}`, { level: 'error' });
|
|
220
|
+
await log(' š” Make sure Gemini CLI is installed, authenticated, and accessible', { level: 'error' });
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Function to handle Gemini runtime switching (if applicable)
|
|
226
|
+
export const handleGeminiRuntimeSwitch = async () => {
|
|
227
|
+
await log('ā¹ļø Gemini runtime handling not required for this operation');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** Check if Playwright MCP is available for Gemini prompt hints @returns {Promise<boolean>} */
|
|
231
|
+
export const checkPlaywrightMcpAvailability = checkPlaywrightMcpPackageAvailability;
|
|
232
|
+
|
|
233
|
+
// Main function to execute Gemini with prompts and settings
|
|
234
|
+
export const executeGemini = async params => {
|
|
235
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, geminiPath = 'gemini', $ } = params;
|
|
236
|
+
|
|
237
|
+
const { buildUserPrompt, buildSystemPrompt } = await import('./gemini.prompts.lib.mjs');
|
|
238
|
+
|
|
239
|
+
const prompt = buildUserPrompt({
|
|
240
|
+
issueUrl,
|
|
241
|
+
issueNumber,
|
|
242
|
+
prNumber,
|
|
243
|
+
prUrl,
|
|
244
|
+
branchName,
|
|
245
|
+
tempDir,
|
|
246
|
+
workspaceTmpDir,
|
|
247
|
+
isContinueMode,
|
|
248
|
+
mergeStateStatus,
|
|
249
|
+
forkedRepo,
|
|
250
|
+
feedbackLines,
|
|
251
|
+
forkActionsUrl,
|
|
252
|
+
owner,
|
|
253
|
+
repo,
|
|
254
|
+
argv,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const systemPrompt = buildSystemPrompt({
|
|
258
|
+
owner,
|
|
259
|
+
repo,
|
|
260
|
+
issueNumber,
|
|
261
|
+
prNumber,
|
|
262
|
+
branchName,
|
|
263
|
+
tempDir,
|
|
264
|
+
workspaceTmpDir,
|
|
265
|
+
isContinueMode,
|
|
266
|
+
forkedRepo,
|
|
267
|
+
argv,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (argv.verbose) {
|
|
271
|
+
await log('\nš Final prompt structure:', { verbose: true });
|
|
272
|
+
await log(` Characters: ${prompt.length}`, { verbose: true });
|
|
273
|
+
await log(` System prompt characters: ${systemPrompt.length}`, { verbose: true });
|
|
274
|
+
if (feedbackLines && feedbackLines.length > 0) {
|
|
275
|
+
await log(' Feedback info: Included', { verbose: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (argv.dryRun) {
|
|
279
|
+
await log('\nš User prompt content:', { verbose: true });
|
|
280
|
+
await log('---BEGIN USER PROMPT---', { verbose: true });
|
|
281
|
+
await log(prompt, { verbose: true });
|
|
282
|
+
await log('---END USER PROMPT---', { verbose: true });
|
|
283
|
+
await log('\nš System prompt content:', { verbose: true });
|
|
284
|
+
await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
|
|
285
|
+
await log(systemPrompt, { verbose: true });
|
|
286
|
+
await log('---END SYSTEM PROMPT---', { verbose: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return await executeGeminiCommand({
|
|
291
|
+
tempDir,
|
|
292
|
+
branchName,
|
|
293
|
+
prompt,
|
|
294
|
+
systemPrompt,
|
|
295
|
+
argv,
|
|
296
|
+
log,
|
|
297
|
+
setLogFile,
|
|
298
|
+
getLogFile,
|
|
299
|
+
formatAligned,
|
|
300
|
+
getResourceSnapshot,
|
|
301
|
+
forkedRepo,
|
|
302
|
+
feedbackLines,
|
|
303
|
+
geminiPath,
|
|
304
|
+
$,
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const executeGeminiCommand = async params => {
|
|
309
|
+
const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, geminiPath, $, waitForRetryDelay = waitWithCountdown } = params;
|
|
310
|
+
|
|
311
|
+
let retryCount = 0;
|
|
312
|
+
|
|
313
|
+
const executeWithRetry = async () => {
|
|
314
|
+
if (retryCount === 0) {
|
|
315
|
+
await log(`\n${formatAligned('š¤', 'Executing Gemini:', argv.model.toUpperCase())}`);
|
|
316
|
+
} else {
|
|
317
|
+
await log(`\n${formatAligned('š', 'Retry attempt:', `${retryCount}/${retryLimits.maxTransientErrorRetries}`)}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (argv.verbose) {
|
|
321
|
+
await log(` Model: ${argv.model}`, { verbose: true });
|
|
322
|
+
await log(` Working directory: ${tempDir}`, { verbose: true });
|
|
323
|
+
await log(` Branch: ${branchName}`, { verbose: true });
|
|
324
|
+
await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
|
|
325
|
+
await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true });
|
|
326
|
+
await log(` Feedback info included: ${feedbackLines && feedbackLines.length > 0 ? `Yes (${feedbackLines.length} lines)` : 'No'}`, { verbose: true });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const resourcesBefore = await getResourceSnapshot();
|
|
330
|
+
await log('š System resources before execution:', { verbose: true });
|
|
331
|
+
await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
|
|
332
|
+
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
333
|
+
|
|
334
|
+
const mappedModel = mapModelToId(argv.model || defaultModels.gemini);
|
|
335
|
+
const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
|
336
|
+
const promptFile = path.join(os.tmpdir(), `gemini_prompt_${Date.now()}_${process.pid}.txt`);
|
|
337
|
+
await fs.writeFile(promptFile, combinedPrompt);
|
|
338
|
+
|
|
339
|
+
let geminiArgs = `--output-format stream-json --model ${shellQuote(mappedModel)} --approval-mode yolo --skip-trust`;
|
|
340
|
+
if (argv.resume) {
|
|
341
|
+
await log(`š Resuming from Gemini session: ${argv.resume}`);
|
|
342
|
+
geminiArgs = `--resume ${shellQuote(argv.resume)} ${geminiArgs}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${geminiPath} ${geminiArgs})`;
|
|
346
|
+
|
|
347
|
+
await log(`\n${formatAligned('š', 'Raw command:', '')}`);
|
|
348
|
+
await log(fullCommand);
|
|
349
|
+
await log('');
|
|
350
|
+
|
|
351
|
+
let geminiJsonState = {};
|
|
352
|
+
let allOutput = '';
|
|
353
|
+
let exitCode = 0;
|
|
354
|
+
let sessionId = argv.resume || null;
|
|
355
|
+
let limitReached = false;
|
|
356
|
+
let limitResetTime = null;
|
|
357
|
+
let lastMessage = '';
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const execCommand = argv.resume
|
|
361
|
+
? $({
|
|
362
|
+
cwd: tempDir,
|
|
363
|
+
mirror: false,
|
|
364
|
+
})`cat ${promptFile} | ${geminiPath} --resume ${argv.resume} --output-format stream-json --model ${mappedModel} --approval-mode yolo --skip-trust`
|
|
365
|
+
: $({
|
|
366
|
+
cwd: tempDir,
|
|
367
|
+
mirror: false,
|
|
368
|
+
})`cat ${promptFile} | ${geminiPath} --output-format stream-json --model ${mappedModel} --approval-mode yolo --skip-trust`;
|
|
369
|
+
|
|
370
|
+
await log(`${formatAligned('š', 'Command details:', '')}`);
|
|
371
|
+
await log(formatAligned('š', 'Working directory:', tempDir, 2));
|
|
372
|
+
await log(formatAligned('šæ', 'Branch:', branchName, 2));
|
|
373
|
+
await log(formatAligned('š¤', 'Model:', `Gemini ${argv.model.toUpperCase()}`, 2));
|
|
374
|
+
if (argv.fork && forkedRepo) {
|
|
375
|
+
await log(formatAligned('š“', 'Fork:', forkedRepo, 2));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await log(`\n${formatAligned('ā¶ļø', 'Streaming output:', '')}\n`);
|
|
379
|
+
|
|
380
|
+
for await (const chunk of execCommand.stream()) {
|
|
381
|
+
if (chunk.type === 'stdout') {
|
|
382
|
+
const output = chunk.data.toString();
|
|
383
|
+
await log(output);
|
|
384
|
+
allOutput += output;
|
|
385
|
+
geminiJsonState = parseGeminiJsonOutput(output, geminiJsonState, mappedModel);
|
|
386
|
+
if (geminiJsonState.sessionId) {
|
|
387
|
+
sessionId = geminiJsonState.sessionId;
|
|
388
|
+
}
|
|
389
|
+
if (geminiJsonState.resultSummary) {
|
|
390
|
+
lastMessage = geminiJsonState.resultSummary;
|
|
391
|
+
} else {
|
|
392
|
+
lastMessage = output;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (chunk.type === 'stderr') {
|
|
397
|
+
const errorOutput = chunk.data.toString();
|
|
398
|
+
if (errorOutput) {
|
|
399
|
+
await log(errorOutput, { stream: 'stderr' });
|
|
400
|
+
allOutput += errorOutput;
|
|
401
|
+
lastMessage = errorOutput;
|
|
402
|
+
}
|
|
403
|
+
} else if (chunk.type === 'exit') {
|
|
404
|
+
exitCode = chunk.code;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (exitCode !== 0 || geminiJsonState.errorMessages?.length > 0) {
|
|
409
|
+
const errorText = geminiJsonState.errorMessages?.length > 0 ? geminiJsonState.errorMessages.join('\n') : allOutput || lastMessage;
|
|
410
|
+
const retryableError = classifyRetryableError(errorText);
|
|
411
|
+
if (retryableError.isRetryable) {
|
|
412
|
+
const isRequestTimeoutRetry = retryableError.label === 'Request timeout';
|
|
413
|
+
const maxRetries = isRequestTimeoutRetry ? retryLimits.maxRequestTimeoutRetries : retryLimits.maxTransientErrorRetries;
|
|
414
|
+
if (retryCount < maxRetries) {
|
|
415
|
+
const delay = getRetryDelayMs({
|
|
416
|
+
retryCount,
|
|
417
|
+
initialDelayMs: isRequestTimeoutRetry ? retryLimits.initialRequestTimeoutDelayMs : retryLimits.initialTransientErrorDelayMs,
|
|
418
|
+
maxDelayMs: isRequestTimeoutRetry ? retryLimits.maxRequestTimeoutDelayMs : retryLimits.maxTransientErrorDelayMs,
|
|
419
|
+
});
|
|
420
|
+
const delayLabel = delay >= 60000 ? `${Math.round(delay / 60000)} min` : `${Math.round(delay / 1000)}s`;
|
|
421
|
+
await log(`\nā ļø ${retryableError.label} detected. Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}${sessionId ? ' (session preserved)' : ''}...`, { level: 'warning' });
|
|
422
|
+
await maybeSwitchToFallbackModel({ tool: 'gemini', argv, log, errorMessage: retryableError.message });
|
|
423
|
+
await waitForRetryDelay(delay, log);
|
|
424
|
+
await log('\nš Retrying now...');
|
|
425
|
+
retryCount++;
|
|
426
|
+
return await executeWithRetry();
|
|
427
|
+
}
|
|
428
|
+
await log(`\n\nā ${retryableError.label} persisted after ${maxRetries} retries`, { level: 'error' });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const limitInfo = detectUsageLimit(errorText);
|
|
432
|
+
if (limitInfo.isUsageLimit) {
|
|
433
|
+
limitReached = true;
|
|
434
|
+
limitResetTime = limitInfo.resetTime;
|
|
435
|
+
|
|
436
|
+
const messageLines = formatUsageLimitMessage({
|
|
437
|
+
tool: 'Gemini CLI',
|
|
438
|
+
resetTime: limitInfo.resetTime,
|
|
439
|
+
sessionId,
|
|
440
|
+
resumeCommand: sessionId ? `${process.argv[0]} ${process.argv[1]} ${argv.url} --resume ${sessionId} --tool gemini` : null,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
for (const line of messageLines) {
|
|
444
|
+
await log(line, { level: 'warning' });
|
|
445
|
+
}
|
|
446
|
+
} else if (exitCode === 130) {
|
|
447
|
+
await log('\n\nā ļø Gemini command interrupted (CTRL+C)');
|
|
448
|
+
} else {
|
|
449
|
+
await log(`\n\nā Gemini command failed with exit code ${exitCode}`, { level: 'error' });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const resourcesAfter = await getResourceSnapshot();
|
|
453
|
+
await log('\nš System resources after execution:', { verbose: true });
|
|
454
|
+
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
455
|
+
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
sessionId,
|
|
460
|
+
limitReached,
|
|
461
|
+
limitResetTime,
|
|
462
|
+
messageCount: geminiJsonState.messageCount || 0,
|
|
463
|
+
toolUseCount: geminiJsonState.toolUseCount || 0,
|
|
464
|
+
resultModelUsage: geminiJsonState.resultModelUsage || buildGeminiResultModelUsage(mappedModel),
|
|
465
|
+
pricingInfo: { modelId: mappedModel, modelName: mappedModel, provider: 'Google', totalCostUSD: null },
|
|
466
|
+
publicPricingEstimate: null,
|
|
467
|
+
resultSummary: geminiJsonState.resultSummary || null,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
await log('\n\nā
Gemini command completed');
|
|
472
|
+
await log(`š Total messages: ${geminiJsonState.messageCount || 0}, Tool uses: ${geminiJsonState.toolUseCount || 0}`);
|
|
473
|
+
if (geminiJsonState.resultSummary) {
|
|
474
|
+
await log('š Captured result summary from Gemini output', { verbose: true });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
success: true,
|
|
479
|
+
sessionId,
|
|
480
|
+
limitReached,
|
|
481
|
+
limitResetTime,
|
|
482
|
+
messageCount: geminiJsonState.messageCount || 0,
|
|
483
|
+
toolUseCount: geminiJsonState.toolUseCount || 0,
|
|
484
|
+
resultModelUsage: geminiJsonState.resultModelUsage || buildGeminiResultModelUsage(mappedModel),
|
|
485
|
+
pricingInfo: { modelId: mappedModel, modelName: mappedModel, provider: 'Google', totalCostUSD: null },
|
|
486
|
+
publicPricingEstimate: null,
|
|
487
|
+
resultSummary: geminiJsonState.resultSummary || null,
|
|
488
|
+
};
|
|
489
|
+
} catch (error) {
|
|
490
|
+
reportError(error, {
|
|
491
|
+
context: 'execute_gemini',
|
|
492
|
+
command: params.command,
|
|
493
|
+
geminiPath: params.geminiPath,
|
|
494
|
+
operation: 'run_gemini_command',
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await log(`\n\nā Error executing Gemini command: ${error.message}`, { level: 'error' });
|
|
498
|
+
return {
|
|
499
|
+
success: false,
|
|
500
|
+
sessionId: null,
|
|
501
|
+
limitReached: false,
|
|
502
|
+
limitResetTime: null,
|
|
503
|
+
messageCount: 0,
|
|
504
|
+
toolUseCount: 0,
|
|
505
|
+
resultModelUsage: null,
|
|
506
|
+
pricingInfo: null,
|
|
507
|
+
publicPricingEstimate: null,
|
|
508
|
+
resultSummary: null,
|
|
509
|
+
};
|
|
510
|
+
} finally {
|
|
511
|
+
await fs.unlink(promptFile).catch(() => {});
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
return await executeWithRetry();
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
export const checkForUncommittedChanges = async (tempDir, owner, repo, branchName, $, log, autoCommit = false, autoRestartEnabled = true) => {
|
|
519
|
+
await log('\nš Checking for uncommitted changes...');
|
|
520
|
+
try {
|
|
521
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
522
|
+
|
|
523
|
+
if (gitStatusResult.code === 0) {
|
|
524
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
525
|
+
|
|
526
|
+
if (statusOutput) {
|
|
527
|
+
await log('š Found uncommitted changes');
|
|
528
|
+
await log('Changes:');
|
|
529
|
+
for (const line of statusOutput.split('\n')) {
|
|
530
|
+
await log(` ${line}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (autoCommit) {
|
|
534
|
+
await log('š¾ Auto-committing changes (--auto-commit-uncommitted-changes is enabled)...');
|
|
535
|
+
|
|
536
|
+
const addResult = await $({ cwd: tempDir })`git add -A`;
|
|
537
|
+
if (addResult.code === 0) {
|
|
538
|
+
const commitMessage = 'Auto-commit: Changes made by Gemini during problem-solving session';
|
|
539
|
+
const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`;
|
|
540
|
+
|
|
541
|
+
if (commitResult.code === 0) {
|
|
542
|
+
await log('ā
Changes committed successfully');
|
|
543
|
+
|
|
544
|
+
const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
|
|
545
|
+
|
|
546
|
+
if (pushResult.code === 0) {
|
|
547
|
+
await log('ā
Changes pushed successfully');
|
|
548
|
+
} else {
|
|
549
|
+
await log(`ā ļø Warning: Could not push changes: ${pushResult.stderr?.toString().trim() || pushResult.stdout?.toString().trim()}`, {
|
|
550
|
+
level: 'warning',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
await log(`ā ļø Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, {
|
|
555
|
+
level: 'warning',
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
await log(`ā ļø Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, {
|
|
560
|
+
level: 'warning',
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (autoRestartEnabled) {
|
|
567
|
+
await log('');
|
|
568
|
+
await log('ā ļø IMPORTANT: Uncommitted changes detected!');
|
|
569
|
+
await log(' Gemini made changes that were not committed.');
|
|
570
|
+
await log('');
|
|
571
|
+
await log('š AUTO-RESTART: Restarting Gemini to handle uncommitted changes...');
|
|
572
|
+
await log(' Gemini will review the changes and decide what to commit.');
|
|
573
|
+
await log('');
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
await log('');
|
|
578
|
+
await log('ā ļø Uncommitted changes detected but auto-restart is disabled.');
|
|
579
|
+
await log(' Use --auto-restart-on-uncommitted-changes to enable or commit manually.');
|
|
580
|
+
await log('');
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await log('ā
No uncommitted changes found');
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
await log(`ā ļø Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, {
|
|
589
|
+
level: 'warning',
|
|
590
|
+
});
|
|
591
|
+
return false;
|
|
592
|
+
} catch (gitError) {
|
|
593
|
+
reportError(gitError, {
|
|
594
|
+
context: 'check_uncommitted_changes_gemini',
|
|
595
|
+
tempDir,
|
|
596
|
+
operation: 'git_status_check',
|
|
597
|
+
});
|
|
598
|
+
await log(`ā ļø Warning: Error checking for uncommitted changes: ${gitError.message}`, { level: 'warning' });
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
export default {
|
|
604
|
+
validateGeminiConnection,
|
|
605
|
+
handleGeminiRuntimeSwitch,
|
|
606
|
+
checkPlaywrightMcpAvailability,
|
|
607
|
+
parseGeminiJsonOutput,
|
|
608
|
+
executeGemini,
|
|
609
|
+
executeGeminiCommand,
|
|
610
|
+
checkForUncommittedChanges,
|
|
611
|
+
};
|