vibecodingmachine-core 1.0.2 → 2025.11.2-7.1239
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/.babelrc +13 -13
- package/README.md +28 -28
- package/__tests__/applescript-manager-claude-fix.test.js +286 -286
- package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
- package/__tests__/requirement-7-status-tracking.test.js +332 -332
- package/jest.config.js +18 -18
- package/jest.setup.js +12 -12
- package/package.json +48 -48
- package/src/auth/access-denied.html +119 -119
- package/src/auth/shared-auth-storage.js +230 -230
- package/src/autonomous-mode/feature-implementer.cjs +70 -70
- package/src/autonomous-mode/feature-implementer.js +425 -425
- package/src/chat-management/chat-manager.cjs +71 -71
- package/src/chat-management/chat-manager.js +342 -342
- package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
- package/src/ide-integration/aider-cli-manager.cjs +850 -850
- package/src/ide-integration/applescript-manager.cjs +1088 -1088
- package/src/ide-integration/applescript-manager.js +2802 -2802
- package/src/ide-integration/applescript-utils.js +306 -306
- package/src/ide-integration/cdp-manager.cjs +221 -221
- package/src/ide-integration/cdp-manager.js +321 -321
- package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
- package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
- package/src/ide-integration/continue-cli-manager.js +431 -431
- package/src/ide-integration/provider-manager.cjs +354 -354
- package/src/ide-integration/quota-detector.cjs +34 -34
- package/src/ide-integration/quota-detector.js +349 -349
- package/src/ide-integration/windows-automation-manager.js +262 -262
- package/src/index.cjs +47 -43
- package/src/index.js +17 -17
- package/src/llm/direct-llm-manager.cjs +609 -609
- package/src/ui/ButtonComponents.js +247 -247
- package/src/ui/ChatInterface.js +499 -499
- package/src/ui/StateManager.js +259 -259
- package/src/utils/audit-logger.cjs +116 -116
- package/src/utils/config-helpers.cjs +94 -94
- package/src/utils/config-helpers.js +94 -94
- package/src/utils/electron-update-checker.js +113 -85
- package/src/utils/gcloud-auth.cjs +394 -394
- package/src/utils/logger.cjs +193 -193
- package/src/utils/logger.js +191 -191
- package/src/utils/repo-helpers.cjs +120 -120
- package/src/utils/repo-helpers.js +120 -120
- package/src/utils/requirement-helpers.js +432 -432
- package/src/utils/update-checker.js +227 -167
- package/src/utils/version-checker.js +169 -0
|
@@ -1,850 +1,850 @@
|
|
|
1
|
-
// Aider CLI Manager - handles Aider CLI installation and execution
|
|
2
|
-
const { execSync, spawn } = require('child_process');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const ProviderManager = require('./provider-manager.cjs');
|
|
7
|
-
|
|
8
|
-
// Helper function to get formatted timestamp
|
|
9
|
-
function getTimestamp() {
|
|
10
|
-
const now = new Date();
|
|
11
|
-
let hours = now.getHours();
|
|
12
|
-
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
13
|
-
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
14
|
-
hours = hours % 12;
|
|
15
|
-
hours = hours ? hours : 12;
|
|
16
|
-
const timeZoneString = now.toLocaleTimeString('en-US', { timeZoneName: 'short' });
|
|
17
|
-
const timezone = timeZoneString.split(' ').pop();
|
|
18
|
-
return `${hours}:${minutes} ${ampm} ${timezone}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
class AiderCLIManager {
|
|
22
|
-
constructor() {
|
|
23
|
-
this.logger = console;
|
|
24
|
-
this.runningProcesses = []; // Track all running Aider subprocesses for cleanup
|
|
25
|
-
this.providerManager = new ProviderManager(); // Track provider rate limits
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Kill all running Aider processes immediately (force kill all aider processes)
|
|
30
|
-
*/
|
|
31
|
-
killAllProcesses() {
|
|
32
|
-
const { execSync } = require('child_process');
|
|
33
|
-
|
|
34
|
-
// First, try to kill tracked processes
|
|
35
|
-
for (const proc of this.runningProcesses) {
|
|
36
|
-
if (proc && proc.pid) {
|
|
37
|
-
try {
|
|
38
|
-
proc.kill('SIGKILL');
|
|
39
|
-
} catch (err) {
|
|
40
|
-
// Process already dead
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
this.runningProcesses = [];
|
|
45
|
-
|
|
46
|
-
// Then, force kill ALL aider processes (fallback to catch any orphans)
|
|
47
|
-
try {
|
|
48
|
-
execSync('pkill -9 -f "\\-m aider"', { stdio: 'ignore' });
|
|
49
|
-
} catch (err) {
|
|
50
|
-
// No processes to kill or pkill not available
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Kill the current Aider process if it's running (legacy - use killAllProcesses)
|
|
56
|
-
*/
|
|
57
|
-
killCurrentProcess() {
|
|
58
|
-
this.killAllProcesses();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if Aider CLI is installed
|
|
63
|
-
*/
|
|
64
|
-
isInstalled() {
|
|
65
|
-
try {
|
|
66
|
-
// Fast check: try which aider first (instant)
|
|
67
|
-
try {
|
|
68
|
-
execSync('which aider', { stdio: 'pipe' });
|
|
69
|
-
return true;
|
|
70
|
-
} catch {
|
|
71
|
-
// Fast check: try common installation paths (file system check, no exec)
|
|
72
|
-
const os = require('os');
|
|
73
|
-
const fs = require('fs');
|
|
74
|
-
const homeDir = os.homedir();
|
|
75
|
-
const possiblePaths = [
|
|
76
|
-
`${homeDir}/.local/bin/aider`,
|
|
77
|
-
'/usr/local/bin/aider',
|
|
78
|
-
'/opt/homebrew/bin/aider'
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
for (const checkPath of possiblePaths) {
|
|
82
|
-
try {
|
|
83
|
-
if (fs.existsSync(checkPath)) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
// Continue checking other paths
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Last resort: try python3 -m aider (can be slow, but only if other checks fail)
|
|
92
|
-
// This is the slowest check, so we do it last
|
|
93
|
-
try {
|
|
94
|
-
execSync('python3 -m aider --version', { stdio: 'pipe' });
|
|
95
|
-
return true;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get Aider CLI version
|
|
107
|
-
*/
|
|
108
|
-
getVersion() {
|
|
109
|
-
try {
|
|
110
|
-
const version = execSync('aider --version', { encoding: 'utf8', stdio: 'pipe' });
|
|
111
|
-
return version.trim();
|
|
112
|
-
} catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Install Aider CLI
|
|
119
|
-
*/
|
|
120
|
-
async install() {
|
|
121
|
-
try {
|
|
122
|
-
this.logger.log('Installing Aider CLI...');
|
|
123
|
-
|
|
124
|
-
// Try different pip commands in order of preference
|
|
125
|
-
let pipCommand = null;
|
|
126
|
-
|
|
127
|
-
// First try pip3 (common on macOS)
|
|
128
|
-
try {
|
|
129
|
-
execSync('which pip3', { stdio: 'pipe' });
|
|
130
|
-
pipCommand = 'pip3';
|
|
131
|
-
} catch {
|
|
132
|
-
// Try python3 -m pip (also common on macOS)
|
|
133
|
-
try {
|
|
134
|
-
execSync('which python3', { stdio: 'pipe' });
|
|
135
|
-
pipCommand = 'python3 -m pip';
|
|
136
|
-
} catch {
|
|
137
|
-
// Try regular pip
|
|
138
|
-
try {
|
|
139
|
-
execSync('which pip', { stdio: 'pipe' });
|
|
140
|
-
pipCommand = 'pip';
|
|
141
|
-
} catch {
|
|
142
|
-
throw new Error('No pip command found. Please install Python and pip.');
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.logger.log(`Using ${pipCommand} to install Aider CLI...`);
|
|
148
|
-
execSync(`${pipCommand} install aider-chat`, { stdio: 'inherit', timeout: 120000 });
|
|
149
|
-
return { success: true };
|
|
150
|
-
} catch (error) {
|
|
151
|
-
return {
|
|
152
|
-
success: false,
|
|
153
|
-
error: error.message,
|
|
154
|
-
needsManualInstall: true,
|
|
155
|
-
suggestions: [
|
|
156
|
-
'Install Python: brew install python (if using Homebrew)',
|
|
157
|
-
'Or download from: https://www.python.org/downloads/',
|
|
158
|
-
'Then try: pip3 install aider-chat'
|
|
159
|
-
]
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if Ollama is installed and running
|
|
166
|
-
*/
|
|
167
|
-
isOllamaInstalled() {
|
|
168
|
-
try {
|
|
169
|
-
execSync('which ollama', { stdio: 'pipe' });
|
|
170
|
-
return true;
|
|
171
|
-
} catch {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Verify Ollama API is accessible
|
|
178
|
-
*/
|
|
179
|
-
async verifyOllamaAPI() {
|
|
180
|
-
try {
|
|
181
|
-
const http = require('http');
|
|
182
|
-
return new Promise((resolve) => {
|
|
183
|
-
const req = http.request({
|
|
184
|
-
hostname: 'localhost',
|
|
185
|
-
port: 11434,
|
|
186
|
-
path: '/api/tags',
|
|
187
|
-
method: 'GET',
|
|
188
|
-
timeout: 2000
|
|
189
|
-
}, (res) => {
|
|
190
|
-
if (res.statusCode === 200) {
|
|
191
|
-
resolve({ success: true });
|
|
192
|
-
} else {
|
|
193
|
-
resolve({ success: false, error: `HTTP ${res.statusCode}` });
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
req.on('error', () => {
|
|
198
|
-
resolve({ success: false, error: 'Connection refused' });
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
req.on('timeout', () => {
|
|
202
|
-
req.destroy();
|
|
203
|
-
resolve({ success: false, error: 'Timeout' });
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
req.end();
|
|
207
|
-
});
|
|
208
|
-
} catch (error) {
|
|
209
|
-
return { success: false, error: error.message };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Start Ollama service if not running
|
|
215
|
-
* @returns {Promise<boolean>} True if service is running (or was started), false otherwise
|
|
216
|
-
*/
|
|
217
|
-
async startOllamaService() {
|
|
218
|
-
try {
|
|
219
|
-
// First check if it's already running
|
|
220
|
-
const apiCheck = await this.verifyOllamaAPI();
|
|
221
|
-
if (apiCheck.success) {
|
|
222
|
-
return true; // Already running
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
this.logger.log('Starting Ollama service...');
|
|
226
|
-
|
|
227
|
-
// Try to start Ollama service in background
|
|
228
|
-
const platform = os.platform();
|
|
229
|
-
|
|
230
|
-
if (platform === 'darwin') {
|
|
231
|
-
// On macOS, try to launch Ollama.app first (doesn't require CLI to be in PATH)
|
|
232
|
-
try {
|
|
233
|
-
execSync('open -a Ollama', { stdio: 'pipe' });
|
|
234
|
-
this.logger.log('Launched Ollama.app');
|
|
235
|
-
} catch (appErr) {
|
|
236
|
-
// If app doesn't exist, try ollama serve (requires CLI in PATH)
|
|
237
|
-
if (!this.isOllamaInstalled()) {
|
|
238
|
-
this.logger.error('Ollama is not installed (neither Ollama.app nor ollama CLI found)');
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
spawn('ollama', ['serve'], {
|
|
243
|
-
detached: true,
|
|
244
|
-
stdio: 'ignore'
|
|
245
|
-
}).unref();
|
|
246
|
-
this.logger.log('Started ollama serve in background');
|
|
247
|
-
} catch (err) {
|
|
248
|
-
this.logger.error('Failed to start Ollama:', err.message);
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} else {
|
|
253
|
-
// On Linux/Windows, use ollama serve (requires CLI in PATH)
|
|
254
|
-
if (!this.isOllamaInstalled()) {
|
|
255
|
-
this.logger.error('Ollama CLI is not installed');
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
try {
|
|
259
|
-
spawn('ollama', ['serve'], {
|
|
260
|
-
detached: true,
|
|
261
|
-
stdio: 'ignore'
|
|
262
|
-
}).unref();
|
|
263
|
-
this.logger.log('Started ollama serve in background');
|
|
264
|
-
} catch (err) {
|
|
265
|
-
this.logger.error('Failed to start Ollama:', err.message);
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Wait for service to be ready (max 15 seconds for initial startup)
|
|
271
|
-
for (let i = 0; i < 30; i++) {
|
|
272
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
273
|
-
const check = await this.verifyOllamaAPI();
|
|
274
|
-
if (check.success) {
|
|
275
|
-
this.logger.log('Ollama service is ready');
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Even if API isn't responding yet, Ollama may still be starting up
|
|
281
|
-
// Return true so Aider can try (it will handle connection errors)
|
|
282
|
-
this.logger.warn('Ollama service started but API not responding yet (may still be initializing)');
|
|
283
|
-
return true;
|
|
284
|
-
} catch (error) {
|
|
285
|
-
this.logger.error('Error starting Ollama service:', error.message);
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Get Ollama models
|
|
292
|
-
*/
|
|
293
|
-
async getOllamaModels() {
|
|
294
|
-
try {
|
|
295
|
-
const output = execSync('ollama list', { encoding: 'utf8', stdio: 'pipe' });
|
|
296
|
-
const lines = output.split('\n').slice(1); // Skip header
|
|
297
|
-
return lines
|
|
298
|
-
.filter(line => line.trim())
|
|
299
|
-
.map(line => {
|
|
300
|
-
const parts = line.trim().split(/\s+/);
|
|
301
|
-
return parts[0];
|
|
302
|
-
});
|
|
303
|
-
} catch {
|
|
304
|
-
return [];
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Configure Aider CLI for Ollama
|
|
310
|
-
* @param {string} modelName - Model name (e.g., 'llama3.1:8b')
|
|
311
|
-
*/
|
|
312
|
-
configureForOllama(modelName = 'llama3.1:8b') {
|
|
313
|
-
// Aider uses environment variables for configuration
|
|
314
|
-
// No config file needed - just set env vars
|
|
315
|
-
return {
|
|
316
|
-
success: true,
|
|
317
|
-
model: modelName,
|
|
318
|
-
env: {
|
|
319
|
-
OPENAI_API_BASE: 'http://localhost:11434/v1',
|
|
320
|
-
OPENAI_API_KEY: 'ollama'
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Configure Aider CLI for AWS Bedrock
|
|
327
|
-
* @param {string} bedrockEndpoint - Bedrock endpoint URL (OpenAI-compatible proxy)
|
|
328
|
-
* @param {string} modelName - Model name
|
|
329
|
-
*/
|
|
330
|
-
configureForBedrock(bedrockEndpoint, modelName) {
|
|
331
|
-
return {
|
|
332
|
-
success: true,
|
|
333
|
-
model: modelName,
|
|
334
|
-
env: {
|
|
335
|
-
OPENAI_API_BASE: bedrockEndpoint,
|
|
336
|
-
OPENAI_API_KEY: 'bedrock' // Bedrock doesn't need a real key if using a proxy
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Run Aider CLI in background and return process
|
|
343
|
-
* @param {string} text - The instruction text
|
|
344
|
-
* @param {string} cwd - Working directory
|
|
345
|
-
* @param {string} provider - 'ollama' or 'bedrock'
|
|
346
|
-
* @param {string} modelName - Model name
|
|
347
|
-
* @param {string} bedrockEndpoint - Bedrock endpoint (if provider is bedrock)
|
|
348
|
-
* @param {Function} onOutput - Callback for stdout chunks
|
|
349
|
-
* @param {Function} onError - Callback for stderr chunks
|
|
350
|
-
* @returns {ChildProcess} The spawned process
|
|
351
|
-
*/
|
|
352
|
-
runInBackground(text, cwd = process.cwd(), provider = 'ollama', modelName = 'llama3.1:8b', bedrockEndpoint = null, onOutput, onError) {
|
|
353
|
-
// Build environment variables
|
|
354
|
-
const env = { ...process.env };
|
|
355
|
-
|
|
356
|
-
// CRITICAL: Disable browser opens from litellm/aider (prevents docs from opening in loop)
|
|
357
|
-
// Use /usr/bin/true as browser command (does nothing, returns success)
|
|
358
|
-
env.BROWSER = '/usr/bin/true';
|
|
359
|
-
env.AIDER_NO_BROWSER = '1';
|
|
360
|
-
env.AIDER_NO_AUTO_COMMITS = '1'; // Prevent auto-commits that might trigger browsers
|
|
361
|
-
|
|
362
|
-
// Disable Python webbrowser module completely
|
|
363
|
-
// This prevents litellm from opening docs when it encounters errors
|
|
364
|
-
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
365
|
-
env.PYTHONUNBUFFERED = '1';
|
|
366
|
-
|
|
367
|
-
if (provider === 'ollama') {
|
|
368
|
-
// Aider uses OLLAMA_API_BASE for Ollama models, not OPENAI_API_BASE
|
|
369
|
-
env.OLLAMA_API_BASE = 'http://localhost:11434';
|
|
370
|
-
// Also set OPENAI_API_BASE as fallback (some versions might use it)
|
|
371
|
-
env.OPENAI_API_BASE = 'http://localhost:11434/v1';
|
|
372
|
-
env.OPENAI_API_KEY = 'ollama';
|
|
373
|
-
} else if (provider === 'bedrock' && bedrockEndpoint) {
|
|
374
|
-
env.OPENAI_API_BASE = bedrockEndpoint;
|
|
375
|
-
env.OPENAI_API_KEY = 'bedrock';
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Aider CLI arguments
|
|
379
|
-
// --yes-always: always auto-apply changes without asking (for autonomous mode)
|
|
380
|
-
// --auto-commits: enable git commits (allows rollback if changes are bad)
|
|
381
|
-
// --model: specify the model (must be prefixed with ollama/ for Ollama models)
|
|
382
|
-
// --edit-format: use diff format to prevent lazy coding / file destruction
|
|
383
|
-
// --message (-m): send a single message and exit (non-interactive mode)
|
|
384
|
-
// --no-show-model-warnings: suppress warnings for cleaner output
|
|
385
|
-
const fullModelName = provider === 'ollama' ? `ollama/${modelName}` : modelName;
|
|
386
|
-
const args = [
|
|
387
|
-
'--yes-always',
|
|
388
|
-
'--auto-commits', // Changed from --no-git to enable safety commits
|
|
389
|
-
'--edit-format', 'diff', // Use diff format to prevent file destruction
|
|
390
|
-
'--model', fullModelName,
|
|
391
|
-
'--no-show-model-warnings',
|
|
392
|
-
'--message', text
|
|
393
|
-
];
|
|
394
|
-
|
|
395
|
-
// Find aider command (try multiple locations)
|
|
396
|
-
let aiderCommand = 'aider';
|
|
397
|
-
let finalArgs = args;
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
// First try direct command
|
|
401
|
-
execSync('which aider', { stdio: 'pipe' });
|
|
402
|
-
aiderCommand = 'aider';
|
|
403
|
-
finalArgs = args;
|
|
404
|
-
} catch {
|
|
405
|
-
// Try python3 -m aider (most common on macOS)
|
|
406
|
-
try {
|
|
407
|
-
execSync('python3 -m aider --version', { stdio: 'pipe' });
|
|
408
|
-
aiderCommand = 'python3';
|
|
409
|
-
finalArgs = ['-m', 'aider', ...args];
|
|
410
|
-
} catch {
|
|
411
|
-
// Try common installation paths
|
|
412
|
-
const os = require('os');
|
|
413
|
-
const homeDir = os.homedir();
|
|
414
|
-
const possiblePaths = [
|
|
415
|
-
`${homeDir}/.local/bin/aider`,
|
|
416
|
-
'/usr/local/bin/aider',
|
|
417
|
-
'/opt/homebrew/bin/aider'
|
|
418
|
-
];
|
|
419
|
-
|
|
420
|
-
let found = false;
|
|
421
|
-
for (const path of possiblePaths) {
|
|
422
|
-
const fs = require('fs');
|
|
423
|
-
if (fs.existsSync(path)) {
|
|
424
|
-
aiderCommand = path;
|
|
425
|
-
finalArgs = args;
|
|
426
|
-
found = true;
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (!found) {
|
|
432
|
-
// Last resort: try python3 -m aider anyway (might work even if version check fails)
|
|
433
|
-
aiderCommand = 'python3';
|
|
434
|
-
finalArgs = ['-m', 'aider', ...args];
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Add error handler for spawn failures
|
|
440
|
-
let proc;
|
|
441
|
-
try {
|
|
442
|
-
// Suppress verbose logging for cleaner output
|
|
443
|
-
// this.logger.log(`Spawning: ${aiderCommand} ${finalArgs.join(' ')}`);
|
|
444
|
-
proc = spawn(aiderCommand, finalArgs, {
|
|
445
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
446
|
-
env: env,
|
|
447
|
-
cwd: cwd
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
// Handle spawn errors (e.g., aider not found)
|
|
451
|
-
proc.on('error', (spawnError) => {
|
|
452
|
-
if (onError) {
|
|
453
|
-
onError(`Failed to spawn Aider CLI: ${spawnError.message}\n`);
|
|
454
|
-
if (spawnError.code === 'ENOENT') {
|
|
455
|
-
onError('Aider CLI is not installed or not in PATH. Install with: pip install aider-chat\n');
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
if (onOutput) {
|
|
461
|
-
proc.stdout.on('data', (data) => {
|
|
462
|
-
onOutput(data.toString());
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (onError) {
|
|
467
|
-
proc.stderr.on('data', (data) => {
|
|
468
|
-
onError(data.toString());
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
} catch (spawnError) {
|
|
472
|
-
// If spawn itself fails (shouldn't happen, but handle it)
|
|
473
|
-
if (onError) {
|
|
474
|
-
onError(`Failed to start Aider CLI: ${spawnError.message}\n`);
|
|
475
|
-
}
|
|
476
|
-
// Return a mock process that will fail immediately
|
|
477
|
-
const { EventEmitter } = require('events');
|
|
478
|
-
const mockProc = new EventEmitter();
|
|
479
|
-
mockProc.pid = null;
|
|
480
|
-
mockProc.kill = () => {};
|
|
481
|
-
mockProc.on = () => {};
|
|
482
|
-
mockProc.stdout = { on: () => {} };
|
|
483
|
-
mockProc.stderr = { on: () => {} };
|
|
484
|
-
setTimeout(() => {
|
|
485
|
-
mockProc.emit('close', 1);
|
|
486
|
-
}, 0);
|
|
487
|
-
return mockProc;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return proc;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Send text to Aider CLI and execute (non-blocking, returns immediately)
|
|
495
|
-
* @param {string} text - The instruction text to send
|
|
496
|
-
* @param {string} cwd - Working directory (defaults to current)
|
|
497
|
-
* @param {string} provider - 'ollama' or 'bedrock'
|
|
498
|
-
* @param {string} modelName - Model name
|
|
499
|
-
* @param {string} bedrockEndpoint - Bedrock endpoint (if provider is bedrock)
|
|
500
|
-
* @param {Array<string>} filesToAdd - Optional array of file paths to add to Aider's context
|
|
501
|
-
* @param {Function} onOutput - Optional callback for output (filtered)
|
|
502
|
-
* @param {Function} onError - Optional callback for errors
|
|
503
|
-
* @returns {Promise<Object>} Result with success, output, and error
|
|
504
|
-
*/
|
|
505
|
-
async sendText(text, cwd = process.cwd(), provider = 'ollama', modelName = 'llama3.1:8b', bedrockEndpoint = null, filesToAdd = [], onOutput = null, onError = null, timeoutMs = 300000) {
|
|
506
|
-
if (!this.isInstalled()) {
|
|
507
|
-
return {
|
|
508
|
-
success: false,
|
|
509
|
-
error: 'Aider CLI is not installed. Run install() first.',
|
|
510
|
-
needsInstall: true
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Start Ollama service if using Ollama provider
|
|
515
|
-
if (provider === 'ollama') {
|
|
516
|
-
const ollamaStarted = await this.startOllamaService();
|
|
517
|
-
if (!ollamaStarted) {
|
|
518
|
-
return {
|
|
519
|
-
success: false,
|
|
520
|
-
error: 'Failed to start Ollama service. Please start it manually with: ollama serve',
|
|
521
|
-
needsOllama: true
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
const env = { ...process.env };
|
|
528
|
-
|
|
529
|
-
// CRITICAL: Disable browser opens from litellm/aider (prevents docs from opening in loop)
|
|
530
|
-
// Use /usr/bin/true as browser command (does nothing, returns success)
|
|
531
|
-
env.BROWSER = '/usr/bin/true';
|
|
532
|
-
env.AIDER_NO_BROWSER = '1';
|
|
533
|
-
env.AIDER_NO_AUTO_COMMITS = '1'; // Prevent auto-commits that might trigger browsers
|
|
534
|
-
|
|
535
|
-
// Disable Python webbrowser module completely
|
|
536
|
-
// This prevents litellm from opening docs when it encounters errors
|
|
537
|
-
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
538
|
-
env.PYTHONUNBUFFERED = '1';
|
|
539
|
-
|
|
540
|
-
if (provider === 'ollama') {
|
|
541
|
-
// Aider uses OLLAMA_API_BASE for Ollama models, not OPENAI_API_BASE
|
|
542
|
-
env.OLLAMA_API_BASE = 'http://localhost:11434';
|
|
543
|
-
// Also set OPENAI_API_BASE as fallback (some versions might use it)
|
|
544
|
-
env.OPENAI_API_BASE = 'http://localhost:11434/v1';
|
|
545
|
-
env.OPENAI_API_KEY = 'ollama';
|
|
546
|
-
} else if (provider === 'bedrock' && bedrockEndpoint) {
|
|
547
|
-
env.OPENAI_API_BASE = bedrockEndpoint;
|
|
548
|
-
env.OPENAI_API_KEY = 'bedrock';
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const fullModelName = provider === 'ollama' ? `ollama/${modelName}` : modelName;
|
|
552
|
-
|
|
553
|
-
// For cloud providers with token limits (Groq = 12k), disable repo map entirely
|
|
554
|
-
const mapTokens = provider === 'groq' ? '0' : '1024';
|
|
555
|
-
|
|
556
|
-
const args = [
|
|
557
|
-
'--yes-always',
|
|
558
|
-
'--no-auto-commits', // Disable auto-commits to prevent auto-file-add
|
|
559
|
-
'--edit-format', 'diff', // Use diff format to prevent file destruction
|
|
560
|
-
'--model', fullModelName,
|
|
561
|
-
'--no-show-model-warnings',
|
|
562
|
-
'--map-tokens', mapTokens, // 0 for Groq (no repo map), 1024 for others
|
|
563
|
-
'--no-suggest-shell-commands', // Disable shell command suggestions
|
|
564
|
-
// Add files to context BEFORE --message so Aider can see them
|
|
565
|
-
...(filesToAdd && filesToAdd.length > 0 ? filesToAdd : []),
|
|
566
|
-
'--message', text
|
|
567
|
-
];
|
|
568
|
-
|
|
569
|
-
// Find aider command (same logic as runInBackground)
|
|
570
|
-
let aiderCommand = 'aider';
|
|
571
|
-
let finalArgs = args;
|
|
572
|
-
|
|
573
|
-
try {
|
|
574
|
-
execSync('which aider', { stdio: 'pipe' });
|
|
575
|
-
aiderCommand = 'aider';
|
|
576
|
-
finalArgs = args;
|
|
577
|
-
} catch {
|
|
578
|
-
try {
|
|
579
|
-
execSync('python3 -m aider --version', { stdio: 'pipe' });
|
|
580
|
-
aiderCommand = 'python3';
|
|
581
|
-
finalArgs = ['-m', 'aider', ...args];
|
|
582
|
-
} catch {
|
|
583
|
-
aiderCommand = 'python3';
|
|
584
|
-
finalArgs = ['-m', 'aider', ...args]; // Try anyway
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Suppress verbose logging for cleaner output
|
|
589
|
-
// this.logger.log(`Executing: ${aiderCommand} ${finalArgs.join(' ')}`);
|
|
590
|
-
// this.logger.log(`Working directory: ${cwd}`);
|
|
591
|
-
// this.logger.log(`Provider: ${provider}, Model: ${modelName}`);
|
|
592
|
-
|
|
593
|
-
return new Promise((resolve) => {
|
|
594
|
-
const startTime = Date.now();
|
|
595
|
-
const proc = spawn(aiderCommand, finalArgs, {
|
|
596
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
597
|
-
env: env,
|
|
598
|
-
cwd: cwd,
|
|
599
|
-
// Don't create a new process group - stay in parent's group to receive signals
|
|
600
|
-
detached: false
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// Store process for cleanup
|
|
604
|
-
this.runningProcesses.push(proc);
|
|
605
|
-
|
|
606
|
-
let stdout = '';
|
|
607
|
-
let stderr = '';
|
|
608
|
-
let lastOutputTime = Date.now();
|
|
609
|
-
let waitingForLLM = false;
|
|
610
|
-
|
|
611
|
-
// Loop detection - track repeated output
|
|
612
|
-
const recentOutputLines = [];
|
|
613
|
-
const MAX_RECENT_LINES = 50;
|
|
614
|
-
let loopDetected = false;
|
|
615
|
-
let rateLimitDetected = false;
|
|
616
|
-
|
|
617
|
-
// Status update interval - show progress every 10 seconds if no output
|
|
618
|
-
const statusInterval = setInterval(() => {
|
|
619
|
-
const timeSinceOutput = (Date.now() - lastOutputTime) / 1000;
|
|
620
|
-
if (timeSinceOutput > 10) {
|
|
621
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
622
|
-
const chalk = require('chalk');
|
|
623
|
-
const timestamp = getTimestamp();
|
|
624
|
-
console.log(chalk.yellow(`[${timestamp}] [AIDER] Still waiting... (${elapsed}s elapsed, ${timeSinceOutput.toFixed(0)}s since last output)`));
|
|
625
|
-
if (waitingForLLM) {
|
|
626
|
-
console.log(chalk.yellow(`[${timestamp}] [AIDER] LLM is processing - this may take 1-2 minutes`));
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}, 10000);
|
|
630
|
-
|
|
631
|
-
// Hard timeout - kill process if it runs too long
|
|
632
|
-
let timeoutKilled = false;
|
|
633
|
-
const hardTimeout = setTimeout(() => {
|
|
634
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
635
|
-
const chalk = require('chalk');
|
|
636
|
-
const timestamp = getTimestamp();
|
|
637
|
-
console.log(chalk.red(`\n[${timestamp}] ⏰ TIMEOUT: Aider process exceeded ${timeoutMs/1000}s limit (ran for ${elapsed}s)`));
|
|
638
|
-
console.log(chalk.red(`[${timestamp}] Killing Aider process to prevent hanging...`));
|
|
639
|
-
timeoutKilled = true;
|
|
640
|
-
clearInterval(statusInterval);
|
|
641
|
-
proc.kill('SIGTERM');
|
|
642
|
-
setTimeout(() => proc.kill('SIGKILL'), 2000); // Force kill after 2s
|
|
643
|
-
}, timeoutMs);
|
|
644
|
-
|
|
645
|
-
proc.stdout.on('data', (data) => {
|
|
646
|
-
const chunk = data.toString();
|
|
647
|
-
stdout += chunk;
|
|
648
|
-
lastOutputTime = Date.now();
|
|
649
|
-
const timestamp = getTimestamp();
|
|
650
|
-
|
|
651
|
-
// Rate limit detection - kill process if rate limited
|
|
652
|
-
if ((chunk.includes('Rate limit reached') || chunk.includes('rate_limit_exceeded')) && !rateLimitDetected) {
|
|
653
|
-
rateLimitDetected = true;
|
|
654
|
-
const chalk = require('chalk');
|
|
655
|
-
console.log(chalk.yellow(`\n[${timestamp}] ⚠️ RATE LIMIT: API rate limit reached`));
|
|
656
|
-
console.log(chalk.yellow(`[${timestamp}] Stopping Aider - please try again later or upgrade your plan`));
|
|
657
|
-
|
|
658
|
-
// Mark provider as rate limited (parse duration from stderr)
|
|
659
|
-
// We'll mark it in the close event when we have full stderr
|
|
660
|
-
|
|
661
|
-
clearInterval(statusInterval);
|
|
662
|
-
proc.kill('SIGTERM');
|
|
663
|
-
setTimeout(() => proc.kill('SIGKILL'), 1000); // Force kill after 1s
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Loop detection - check for repeated SEARCH/REPLACE blocks
|
|
668
|
-
if (chunk.includes('<<<<<<< SEARCH') || chunk.includes('>>>>>>> REPLACE')) {
|
|
669
|
-
const normalizedChunk = chunk.trim().substring(0, 200); // First 200 chars
|
|
670
|
-
recentOutputLines.push(normalizedChunk);
|
|
671
|
-
|
|
672
|
-
// Keep only recent lines
|
|
673
|
-
if (recentOutputLines.length > MAX_RECENT_LINES) {
|
|
674
|
-
recentOutputLines.shift();
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Check for loops - if we see the same block 3+ times
|
|
678
|
-
const occurrences = recentOutputLines.filter(line => line === normalizedChunk).length;
|
|
679
|
-
if (occurrences >= 3 && !loopDetected) {
|
|
680
|
-
loopDetected = true;
|
|
681
|
-
const chalk = require('chalk');
|
|
682
|
-
console.log(chalk.red(`\n[${timestamp}] ⚠️ LOOP DETECTED: Same output repeated ${occurrences} times!`));
|
|
683
|
-
console.log(chalk.red(`[${timestamp}] Killing Aider process to prevent infinite loop...`));
|
|
684
|
-
proc.kill('SIGTERM');
|
|
685
|
-
setTimeout(() => proc.kill('SIGKILL'), 2000); // Force kill after 2s
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Detect if we're waiting for LLM response
|
|
691
|
-
if (chunk.includes('Tokens:') || chunk.toLowerCase().includes('thinking')) {
|
|
692
|
-
waitingForLLM = true;
|
|
693
|
-
const chalk = require('chalk');
|
|
694
|
-
console.log(chalk.cyan(`[${timestamp}] [AIDER] Prompt sent to LLM, waiting for response...`));
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Detect when LLM starts responding
|
|
698
|
-
if (waitingForLLM && (chunk.includes('####') || chunk.includes('```') || chunk.includes('SEARCH/REPLACE'))) {
|
|
699
|
-
waitingForLLM = false;
|
|
700
|
-
const chalk = require('chalk');
|
|
701
|
-
console.log(chalk.green(`[${timestamp}] [AIDER] LLM responding...`));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Filter and show useful output (Assistant responses, file edits, etc.)
|
|
705
|
-
// Skip verbose startup messages and warnings
|
|
706
|
-
const lines = chunk.split('\n');
|
|
707
|
-
for (const line of lines) {
|
|
708
|
-
const trimmed = line.trim();
|
|
709
|
-
if (!trimmed) continue;
|
|
710
|
-
|
|
711
|
-
// Show assistant responses and important messages
|
|
712
|
-
const shouldShow = trimmed.startsWith('Assistant:') ||
|
|
713
|
-
trimmed.startsWith('User:') ||
|
|
714
|
-
trimmed.includes('file listing') ||
|
|
715
|
-
trimmed.includes('Updated') ||
|
|
716
|
-
trimmed.includes('Created') ||
|
|
717
|
-
trimmed.includes('Tokens:') ||
|
|
718
|
-
(trimmed.startsWith('###') && !trimmed.includes('ONE LINE')) ||
|
|
719
|
-
// Show meaningful content (not just startup noise)
|
|
720
|
-
(!trimmed.includes('NotOpenSSLWarning') &&
|
|
721
|
-
!trimmed.includes('Warning:') &&
|
|
722
|
-
!trimmed.includes('Detected dumb terminal') &&
|
|
723
|
-
!trimmed.includes('Aider v') &&
|
|
724
|
-
!trimmed.includes('Model:') &&
|
|
725
|
-
!trimmed.includes('Git repo:') &&
|
|
726
|
-
!trimmed.includes('Repo-map:') &&
|
|
727
|
-
!trimmed.includes('─────────────────') &&
|
|
728
|
-
trimmed.length > 10); // Only show substantial lines
|
|
729
|
-
|
|
730
|
-
if (shouldShow) {
|
|
731
|
-
if (onOutput) {
|
|
732
|
-
onOutput(line + '\n');
|
|
733
|
-
} else {
|
|
734
|
-
// Fallback: just write directly if no callback
|
|
735
|
-
try {
|
|
736
|
-
const chalk = require('chalk');
|
|
737
|
-
process.stdout.write(chalk.gray(line + '\n'));
|
|
738
|
-
} catch {
|
|
739
|
-
process.stdout.write(line + '\n');
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
proc.stderr.on('data', (data) => {
|
|
747
|
-
const chunk = data.toString();
|
|
748
|
-
stderr += chunk;
|
|
749
|
-
|
|
750
|
-
// Only show actual errors, suppress warnings
|
|
751
|
-
if (chunk.includes('Error:') || chunk.includes('Failed:') ||
|
|
752
|
-
(chunk.includes('error') && !chunk.includes('NotOpenSSLWarning') && !chunk.includes('Warning:'))) {
|
|
753
|
-
if (onError) {
|
|
754
|
-
onError(chunk.trim());
|
|
755
|
-
} else {
|
|
756
|
-
this.logger.error('[Aider Error]', chunk.trim());
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
proc.on('close', (code) => {
|
|
762
|
-
// Remove process from tracking array
|
|
763
|
-
const index = this.runningProcesses.indexOf(proc);
|
|
764
|
-
if (index > -1) {
|
|
765
|
-
this.runningProcesses.splice(index, 1);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
clearInterval(statusInterval);
|
|
769
|
-
clearTimeout(hardTimeout);
|
|
770
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
771
|
-
const chalk = require('chalk');
|
|
772
|
-
const timestamp = getTimestamp();
|
|
773
|
-
|
|
774
|
-
if (timeoutKilled) {
|
|
775
|
-
console.log(chalk.red(`[${timestamp}] [AIDER] Process killed due to timeout after ${elapsed}s`));
|
|
776
|
-
resolve({
|
|
777
|
-
success: false,
|
|
778
|
-
output: stdout,
|
|
779
|
-
error: 'Timeout: Process exceeded maximum execution time',
|
|
780
|
-
exitCode: -1,
|
|
781
|
-
timeout: true
|
|
782
|
-
});
|
|
783
|
-
} else if (rateLimitDetected) {
|
|
784
|
-
console.log(chalk.yellow(`[${timestamp}] [AIDER] Process stopped due to rate limit after ${elapsed}s`));
|
|
785
|
-
|
|
786
|
-
// Mark provider as rate limited with duration from error message
|
|
787
|
-
const errorMessage = stderr + stdout; // Full error with duration
|
|
788
|
-
this.providerManager.markRateLimited(provider, modelName, errorMessage);
|
|
789
|
-
|
|
790
|
-
resolve({
|
|
791
|
-
success: false,
|
|
792
|
-
output: stdout,
|
|
793
|
-
error: 'Rate limit: API rate limit reached - please try again later',
|
|
794
|
-
errorMessage: errorMessage, // Full error with duration
|
|
795
|
-
exitCode: -1,
|
|
796
|
-
rateLimitDetected: true,
|
|
797
|
-
provider: provider,
|
|
798
|
-
model: modelName
|
|
799
|
-
});
|
|
800
|
-
} else if (loopDetected) {
|
|
801
|
-
console.log(chalk.red(`[${timestamp}] [AIDER] Process killed due to loop detection after ${elapsed}s`));
|
|
802
|
-
resolve({
|
|
803
|
-
success: false,
|
|
804
|
-
output: stdout,
|
|
805
|
-
error: 'Loop detected: Same output repeated multiple times',
|
|
806
|
-
exitCode: -1,
|
|
807
|
-
loopDetected: true
|
|
808
|
-
});
|
|
809
|
-
} else {
|
|
810
|
-
console.log(chalk.gray(`[${timestamp}] [AIDER] Process closed after ${elapsed}s with code: ${code}`));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (code === 0 && !timeoutKilled && !rateLimitDetected && !loopDetected) {
|
|
814
|
-
resolve({
|
|
815
|
-
success: true,
|
|
816
|
-
output: stdout,
|
|
817
|
-
stderr: stderr,
|
|
818
|
-
exitCode: code
|
|
819
|
-
});
|
|
820
|
-
} else if (!timeoutKilled && !loopDetected) {
|
|
821
|
-
resolve({
|
|
822
|
-
success: false,
|
|
823
|
-
output: stdout,
|
|
824
|
-
error: stderr || `Process exited with code ${code}`,
|
|
825
|
-
exitCode: code
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
proc.on('error', (error) => {
|
|
831
|
-
clearInterval(statusInterval);
|
|
832
|
-
clearTimeout(hardTimeout);
|
|
833
|
-
resolve({
|
|
834
|
-
success: false,
|
|
835
|
-
error: error.message,
|
|
836
|
-
exitCode: -1
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
});
|
|
840
|
-
} catch (error) {
|
|
841
|
-
return {
|
|
842
|
-
success: false,
|
|
843
|
-
error: error.message
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
module.exports = { AiderCLIManager };
|
|
850
|
-
|
|
1
|
+
// Aider CLI Manager - handles Aider CLI installation and execution
|
|
2
|
+
const { execSync, spawn } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const ProviderManager = require('./provider-manager.cjs');
|
|
7
|
+
|
|
8
|
+
// Helper function to get formatted timestamp
|
|
9
|
+
function getTimestamp() {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
let hours = now.getHours();
|
|
12
|
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
13
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
14
|
+
hours = hours % 12;
|
|
15
|
+
hours = hours ? hours : 12;
|
|
16
|
+
const timeZoneString = now.toLocaleTimeString('en-US', { timeZoneName: 'short' });
|
|
17
|
+
const timezone = timeZoneString.split(' ').pop();
|
|
18
|
+
return `${hours}:${minutes} ${ampm} ${timezone}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class AiderCLIManager {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.logger = console;
|
|
24
|
+
this.runningProcesses = []; // Track all running Aider subprocesses for cleanup
|
|
25
|
+
this.providerManager = new ProviderManager(); // Track provider rate limits
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Kill all running Aider processes immediately (force kill all aider processes)
|
|
30
|
+
*/
|
|
31
|
+
killAllProcesses() {
|
|
32
|
+
const { execSync } = require('child_process');
|
|
33
|
+
|
|
34
|
+
// First, try to kill tracked processes
|
|
35
|
+
for (const proc of this.runningProcesses) {
|
|
36
|
+
if (proc && proc.pid) {
|
|
37
|
+
try {
|
|
38
|
+
proc.kill('SIGKILL');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Process already dead
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.runningProcesses = [];
|
|
45
|
+
|
|
46
|
+
// Then, force kill ALL aider processes (fallback to catch any orphans)
|
|
47
|
+
try {
|
|
48
|
+
execSync('pkill -9 -f "\\-m aider"', { stdio: 'ignore' });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// No processes to kill or pkill not available
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Kill the current Aider process if it's running (legacy - use killAllProcesses)
|
|
56
|
+
*/
|
|
57
|
+
killCurrentProcess() {
|
|
58
|
+
this.killAllProcesses();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if Aider CLI is installed
|
|
63
|
+
*/
|
|
64
|
+
isInstalled() {
|
|
65
|
+
try {
|
|
66
|
+
// Fast check: try which aider first (instant)
|
|
67
|
+
try {
|
|
68
|
+
execSync('which aider', { stdio: 'pipe' });
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
// Fast check: try common installation paths (file system check, no exec)
|
|
72
|
+
const os = require('os');
|
|
73
|
+
const fs = require('fs');
|
|
74
|
+
const homeDir = os.homedir();
|
|
75
|
+
const possiblePaths = [
|
|
76
|
+
`${homeDir}/.local/bin/aider`,
|
|
77
|
+
'/usr/local/bin/aider',
|
|
78
|
+
'/opt/homebrew/bin/aider'
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const checkPath of possiblePaths) {
|
|
82
|
+
try {
|
|
83
|
+
if (fs.existsSync(checkPath)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Continue checking other paths
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Last resort: try python3 -m aider (can be slow, but only if other checks fail)
|
|
92
|
+
// This is the slowest check, so we do it last
|
|
93
|
+
try {
|
|
94
|
+
execSync('python3 -m aider --version', { stdio: 'pipe' });
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get Aider CLI version
|
|
107
|
+
*/
|
|
108
|
+
getVersion() {
|
|
109
|
+
try {
|
|
110
|
+
const version = execSync('aider --version', { encoding: 'utf8', stdio: 'pipe' });
|
|
111
|
+
return version.trim();
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Install Aider CLI
|
|
119
|
+
*/
|
|
120
|
+
async install() {
|
|
121
|
+
try {
|
|
122
|
+
this.logger.log('Installing Aider CLI...');
|
|
123
|
+
|
|
124
|
+
// Try different pip commands in order of preference
|
|
125
|
+
let pipCommand = null;
|
|
126
|
+
|
|
127
|
+
// First try pip3 (common on macOS)
|
|
128
|
+
try {
|
|
129
|
+
execSync('which pip3', { stdio: 'pipe' });
|
|
130
|
+
pipCommand = 'pip3';
|
|
131
|
+
} catch {
|
|
132
|
+
// Try python3 -m pip (also common on macOS)
|
|
133
|
+
try {
|
|
134
|
+
execSync('which python3', { stdio: 'pipe' });
|
|
135
|
+
pipCommand = 'python3 -m pip';
|
|
136
|
+
} catch {
|
|
137
|
+
// Try regular pip
|
|
138
|
+
try {
|
|
139
|
+
execSync('which pip', { stdio: 'pipe' });
|
|
140
|
+
pipCommand = 'pip';
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error('No pip command found. Please install Python and pip.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.logger.log(`Using ${pipCommand} to install Aider CLI...`);
|
|
148
|
+
execSync(`${pipCommand} install aider-chat`, { stdio: 'inherit', timeout: 120000 });
|
|
149
|
+
return { success: true };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: error.message,
|
|
154
|
+
needsManualInstall: true,
|
|
155
|
+
suggestions: [
|
|
156
|
+
'Install Python: brew install python (if using Homebrew)',
|
|
157
|
+
'Or download from: https://www.python.org/downloads/',
|
|
158
|
+
'Then try: pip3 install aider-chat'
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if Ollama is installed and running
|
|
166
|
+
*/
|
|
167
|
+
isOllamaInstalled() {
|
|
168
|
+
try {
|
|
169
|
+
execSync('which ollama', { stdio: 'pipe' });
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Verify Ollama API is accessible
|
|
178
|
+
*/
|
|
179
|
+
async verifyOllamaAPI() {
|
|
180
|
+
try {
|
|
181
|
+
const http = require('http');
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
const req = http.request({
|
|
184
|
+
hostname: 'localhost',
|
|
185
|
+
port: 11434,
|
|
186
|
+
path: '/api/tags',
|
|
187
|
+
method: 'GET',
|
|
188
|
+
timeout: 2000
|
|
189
|
+
}, (res) => {
|
|
190
|
+
if (res.statusCode === 200) {
|
|
191
|
+
resolve({ success: true });
|
|
192
|
+
} else {
|
|
193
|
+
resolve({ success: false, error: `HTTP ${res.statusCode}` });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
req.on('error', () => {
|
|
198
|
+
resolve({ success: false, error: 'Connection refused' });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
req.on('timeout', () => {
|
|
202
|
+
req.destroy();
|
|
203
|
+
resolve({ success: false, error: 'Timeout' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
req.end();
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return { success: false, error: error.message };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Start Ollama service if not running
|
|
215
|
+
* @returns {Promise<boolean>} True if service is running (or was started), false otherwise
|
|
216
|
+
*/
|
|
217
|
+
async startOllamaService() {
|
|
218
|
+
try {
|
|
219
|
+
// First check if it's already running
|
|
220
|
+
const apiCheck = await this.verifyOllamaAPI();
|
|
221
|
+
if (apiCheck.success) {
|
|
222
|
+
return true; // Already running
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.logger.log('Starting Ollama service...');
|
|
226
|
+
|
|
227
|
+
// Try to start Ollama service in background
|
|
228
|
+
const platform = os.platform();
|
|
229
|
+
|
|
230
|
+
if (platform === 'darwin') {
|
|
231
|
+
// On macOS, try to launch Ollama.app first (doesn't require CLI to be in PATH)
|
|
232
|
+
try {
|
|
233
|
+
execSync('open -a Ollama', { stdio: 'pipe' });
|
|
234
|
+
this.logger.log('Launched Ollama.app');
|
|
235
|
+
} catch (appErr) {
|
|
236
|
+
// If app doesn't exist, try ollama serve (requires CLI in PATH)
|
|
237
|
+
if (!this.isOllamaInstalled()) {
|
|
238
|
+
this.logger.error('Ollama is not installed (neither Ollama.app nor ollama CLI found)');
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
spawn('ollama', ['serve'], {
|
|
243
|
+
detached: true,
|
|
244
|
+
stdio: 'ignore'
|
|
245
|
+
}).unref();
|
|
246
|
+
this.logger.log('Started ollama serve in background');
|
|
247
|
+
} catch (err) {
|
|
248
|
+
this.logger.error('Failed to start Ollama:', err.message);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// On Linux/Windows, use ollama serve (requires CLI in PATH)
|
|
254
|
+
if (!this.isOllamaInstalled()) {
|
|
255
|
+
this.logger.error('Ollama CLI is not installed');
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
spawn('ollama', ['serve'], {
|
|
260
|
+
detached: true,
|
|
261
|
+
stdio: 'ignore'
|
|
262
|
+
}).unref();
|
|
263
|
+
this.logger.log('Started ollama serve in background');
|
|
264
|
+
} catch (err) {
|
|
265
|
+
this.logger.error('Failed to start Ollama:', err.message);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Wait for service to be ready (max 15 seconds for initial startup)
|
|
271
|
+
for (let i = 0; i < 30; i++) {
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
273
|
+
const check = await this.verifyOllamaAPI();
|
|
274
|
+
if (check.success) {
|
|
275
|
+
this.logger.log('Ollama service is ready');
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Even if API isn't responding yet, Ollama may still be starting up
|
|
281
|
+
// Return true so Aider can try (it will handle connection errors)
|
|
282
|
+
this.logger.warn('Ollama service started but API not responding yet (may still be initializing)');
|
|
283
|
+
return true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
this.logger.error('Error starting Ollama service:', error.message);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get Ollama models
|
|
292
|
+
*/
|
|
293
|
+
async getOllamaModels() {
|
|
294
|
+
try {
|
|
295
|
+
const output = execSync('ollama list', { encoding: 'utf8', stdio: 'pipe' });
|
|
296
|
+
const lines = output.split('\n').slice(1); // Skip header
|
|
297
|
+
return lines
|
|
298
|
+
.filter(line => line.trim())
|
|
299
|
+
.map(line => {
|
|
300
|
+
const parts = line.trim().split(/\s+/);
|
|
301
|
+
return parts[0];
|
|
302
|
+
});
|
|
303
|
+
} catch {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Configure Aider CLI for Ollama
|
|
310
|
+
* @param {string} modelName - Model name (e.g., 'llama3.1:8b')
|
|
311
|
+
*/
|
|
312
|
+
configureForOllama(modelName = 'llama3.1:8b') {
|
|
313
|
+
// Aider uses environment variables for configuration
|
|
314
|
+
// No config file needed - just set env vars
|
|
315
|
+
return {
|
|
316
|
+
success: true,
|
|
317
|
+
model: modelName,
|
|
318
|
+
env: {
|
|
319
|
+
OPENAI_API_BASE: 'http://localhost:11434/v1',
|
|
320
|
+
OPENAI_API_KEY: 'ollama'
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Configure Aider CLI for AWS Bedrock
|
|
327
|
+
* @param {string} bedrockEndpoint - Bedrock endpoint URL (OpenAI-compatible proxy)
|
|
328
|
+
* @param {string} modelName - Model name
|
|
329
|
+
*/
|
|
330
|
+
configureForBedrock(bedrockEndpoint, modelName) {
|
|
331
|
+
return {
|
|
332
|
+
success: true,
|
|
333
|
+
model: modelName,
|
|
334
|
+
env: {
|
|
335
|
+
OPENAI_API_BASE: bedrockEndpoint,
|
|
336
|
+
OPENAI_API_KEY: 'bedrock' // Bedrock doesn't need a real key if using a proxy
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Run Aider CLI in background and return process
|
|
343
|
+
* @param {string} text - The instruction text
|
|
344
|
+
* @param {string} cwd - Working directory
|
|
345
|
+
* @param {string} provider - 'ollama' or 'bedrock'
|
|
346
|
+
* @param {string} modelName - Model name
|
|
347
|
+
* @param {string} bedrockEndpoint - Bedrock endpoint (if provider is bedrock)
|
|
348
|
+
* @param {Function} onOutput - Callback for stdout chunks
|
|
349
|
+
* @param {Function} onError - Callback for stderr chunks
|
|
350
|
+
* @returns {ChildProcess} The spawned process
|
|
351
|
+
*/
|
|
352
|
+
runInBackground(text, cwd = process.cwd(), provider = 'ollama', modelName = 'llama3.1:8b', bedrockEndpoint = null, onOutput, onError) {
|
|
353
|
+
// Build environment variables
|
|
354
|
+
const env = { ...process.env };
|
|
355
|
+
|
|
356
|
+
// CRITICAL: Disable browser opens from litellm/aider (prevents docs from opening in loop)
|
|
357
|
+
// Use /usr/bin/true as browser command (does nothing, returns success)
|
|
358
|
+
env.BROWSER = '/usr/bin/true';
|
|
359
|
+
env.AIDER_NO_BROWSER = '1';
|
|
360
|
+
env.AIDER_NO_AUTO_COMMITS = '1'; // Prevent auto-commits that might trigger browsers
|
|
361
|
+
|
|
362
|
+
// Disable Python webbrowser module completely
|
|
363
|
+
// This prevents litellm from opening docs when it encounters errors
|
|
364
|
+
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
365
|
+
env.PYTHONUNBUFFERED = '1';
|
|
366
|
+
|
|
367
|
+
if (provider === 'ollama') {
|
|
368
|
+
// Aider uses OLLAMA_API_BASE for Ollama models, not OPENAI_API_BASE
|
|
369
|
+
env.OLLAMA_API_BASE = 'http://localhost:11434';
|
|
370
|
+
// Also set OPENAI_API_BASE as fallback (some versions might use it)
|
|
371
|
+
env.OPENAI_API_BASE = 'http://localhost:11434/v1';
|
|
372
|
+
env.OPENAI_API_KEY = 'ollama';
|
|
373
|
+
} else if (provider === 'bedrock' && bedrockEndpoint) {
|
|
374
|
+
env.OPENAI_API_BASE = bedrockEndpoint;
|
|
375
|
+
env.OPENAI_API_KEY = 'bedrock';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Aider CLI arguments
|
|
379
|
+
// --yes-always: always auto-apply changes without asking (for autonomous mode)
|
|
380
|
+
// --auto-commits: enable git commits (allows rollback if changes are bad)
|
|
381
|
+
// --model: specify the model (must be prefixed with ollama/ for Ollama models)
|
|
382
|
+
// --edit-format: use diff format to prevent lazy coding / file destruction
|
|
383
|
+
// --message (-m): send a single message and exit (non-interactive mode)
|
|
384
|
+
// --no-show-model-warnings: suppress warnings for cleaner output
|
|
385
|
+
const fullModelName = provider === 'ollama' ? `ollama/${modelName}` : modelName;
|
|
386
|
+
const args = [
|
|
387
|
+
'--yes-always',
|
|
388
|
+
'--auto-commits', // Changed from --no-git to enable safety commits
|
|
389
|
+
'--edit-format', 'diff', // Use diff format to prevent file destruction
|
|
390
|
+
'--model', fullModelName,
|
|
391
|
+
'--no-show-model-warnings',
|
|
392
|
+
'--message', text
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
// Find aider command (try multiple locations)
|
|
396
|
+
let aiderCommand = 'aider';
|
|
397
|
+
let finalArgs = args;
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
// First try direct command
|
|
401
|
+
execSync('which aider', { stdio: 'pipe' });
|
|
402
|
+
aiderCommand = 'aider';
|
|
403
|
+
finalArgs = args;
|
|
404
|
+
} catch {
|
|
405
|
+
// Try python3 -m aider (most common on macOS)
|
|
406
|
+
try {
|
|
407
|
+
execSync('python3 -m aider --version', { stdio: 'pipe' });
|
|
408
|
+
aiderCommand = 'python3';
|
|
409
|
+
finalArgs = ['-m', 'aider', ...args];
|
|
410
|
+
} catch {
|
|
411
|
+
// Try common installation paths
|
|
412
|
+
const os = require('os');
|
|
413
|
+
const homeDir = os.homedir();
|
|
414
|
+
const possiblePaths = [
|
|
415
|
+
`${homeDir}/.local/bin/aider`,
|
|
416
|
+
'/usr/local/bin/aider',
|
|
417
|
+
'/opt/homebrew/bin/aider'
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
let found = false;
|
|
421
|
+
for (const path of possiblePaths) {
|
|
422
|
+
const fs = require('fs');
|
|
423
|
+
if (fs.existsSync(path)) {
|
|
424
|
+
aiderCommand = path;
|
|
425
|
+
finalArgs = args;
|
|
426
|
+
found = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!found) {
|
|
432
|
+
// Last resort: try python3 -m aider anyway (might work even if version check fails)
|
|
433
|
+
aiderCommand = 'python3';
|
|
434
|
+
finalArgs = ['-m', 'aider', ...args];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Add error handler for spawn failures
|
|
440
|
+
let proc;
|
|
441
|
+
try {
|
|
442
|
+
// Suppress verbose logging for cleaner output
|
|
443
|
+
// this.logger.log(`Spawning: ${aiderCommand} ${finalArgs.join(' ')}`);
|
|
444
|
+
proc = spawn(aiderCommand, finalArgs, {
|
|
445
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
446
|
+
env: env,
|
|
447
|
+
cwd: cwd
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Handle spawn errors (e.g., aider not found)
|
|
451
|
+
proc.on('error', (spawnError) => {
|
|
452
|
+
if (onError) {
|
|
453
|
+
onError(`Failed to spawn Aider CLI: ${spawnError.message}\n`);
|
|
454
|
+
if (spawnError.code === 'ENOENT') {
|
|
455
|
+
onError('Aider CLI is not installed or not in PATH. Install with: pip install aider-chat\n');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (onOutput) {
|
|
461
|
+
proc.stdout.on('data', (data) => {
|
|
462
|
+
onOutput(data.toString());
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (onError) {
|
|
467
|
+
proc.stderr.on('data', (data) => {
|
|
468
|
+
onError(data.toString());
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
} catch (spawnError) {
|
|
472
|
+
// If spawn itself fails (shouldn't happen, but handle it)
|
|
473
|
+
if (onError) {
|
|
474
|
+
onError(`Failed to start Aider CLI: ${spawnError.message}\n`);
|
|
475
|
+
}
|
|
476
|
+
// Return a mock process that will fail immediately
|
|
477
|
+
const { EventEmitter } = require('events');
|
|
478
|
+
const mockProc = new EventEmitter();
|
|
479
|
+
mockProc.pid = null;
|
|
480
|
+
mockProc.kill = () => {};
|
|
481
|
+
mockProc.on = () => {};
|
|
482
|
+
mockProc.stdout = { on: () => {} };
|
|
483
|
+
mockProc.stderr = { on: () => {} };
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
mockProc.emit('close', 1);
|
|
486
|
+
}, 0);
|
|
487
|
+
return mockProc;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return proc;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Send text to Aider CLI and execute (non-blocking, returns immediately)
|
|
495
|
+
* @param {string} text - The instruction text to send
|
|
496
|
+
* @param {string} cwd - Working directory (defaults to current)
|
|
497
|
+
* @param {string} provider - 'ollama' or 'bedrock'
|
|
498
|
+
* @param {string} modelName - Model name
|
|
499
|
+
* @param {string} bedrockEndpoint - Bedrock endpoint (if provider is bedrock)
|
|
500
|
+
* @param {Array<string>} filesToAdd - Optional array of file paths to add to Aider's context
|
|
501
|
+
* @param {Function} onOutput - Optional callback for output (filtered)
|
|
502
|
+
* @param {Function} onError - Optional callback for errors
|
|
503
|
+
* @returns {Promise<Object>} Result with success, output, and error
|
|
504
|
+
*/
|
|
505
|
+
async sendText(text, cwd = process.cwd(), provider = 'ollama', modelName = 'llama3.1:8b', bedrockEndpoint = null, filesToAdd = [], onOutput = null, onError = null, timeoutMs = 300000) {
|
|
506
|
+
if (!this.isInstalled()) {
|
|
507
|
+
return {
|
|
508
|
+
success: false,
|
|
509
|
+
error: 'Aider CLI is not installed. Run install() first.',
|
|
510
|
+
needsInstall: true
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Start Ollama service if using Ollama provider
|
|
515
|
+
if (provider === 'ollama') {
|
|
516
|
+
const ollamaStarted = await this.startOllamaService();
|
|
517
|
+
if (!ollamaStarted) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
error: 'Failed to start Ollama service. Please start it manually with: ollama serve',
|
|
521
|
+
needsOllama: true
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const env = { ...process.env };
|
|
528
|
+
|
|
529
|
+
// CRITICAL: Disable browser opens from litellm/aider (prevents docs from opening in loop)
|
|
530
|
+
// Use /usr/bin/true as browser command (does nothing, returns success)
|
|
531
|
+
env.BROWSER = '/usr/bin/true';
|
|
532
|
+
env.AIDER_NO_BROWSER = '1';
|
|
533
|
+
env.AIDER_NO_AUTO_COMMITS = '1'; // Prevent auto-commits that might trigger browsers
|
|
534
|
+
|
|
535
|
+
// Disable Python webbrowser module completely
|
|
536
|
+
// This prevents litellm from opening docs when it encounters errors
|
|
537
|
+
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
538
|
+
env.PYTHONUNBUFFERED = '1';
|
|
539
|
+
|
|
540
|
+
if (provider === 'ollama') {
|
|
541
|
+
// Aider uses OLLAMA_API_BASE for Ollama models, not OPENAI_API_BASE
|
|
542
|
+
env.OLLAMA_API_BASE = 'http://localhost:11434';
|
|
543
|
+
// Also set OPENAI_API_BASE as fallback (some versions might use it)
|
|
544
|
+
env.OPENAI_API_BASE = 'http://localhost:11434/v1';
|
|
545
|
+
env.OPENAI_API_KEY = 'ollama';
|
|
546
|
+
} else if (provider === 'bedrock' && bedrockEndpoint) {
|
|
547
|
+
env.OPENAI_API_BASE = bedrockEndpoint;
|
|
548
|
+
env.OPENAI_API_KEY = 'bedrock';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const fullModelName = provider === 'ollama' ? `ollama/${modelName}` : modelName;
|
|
552
|
+
|
|
553
|
+
// For cloud providers with token limits (Groq = 12k), disable repo map entirely
|
|
554
|
+
const mapTokens = provider === 'groq' ? '0' : '1024';
|
|
555
|
+
|
|
556
|
+
const args = [
|
|
557
|
+
'--yes-always',
|
|
558
|
+
'--no-auto-commits', // Disable auto-commits to prevent auto-file-add
|
|
559
|
+
'--edit-format', 'diff', // Use diff format to prevent file destruction
|
|
560
|
+
'--model', fullModelName,
|
|
561
|
+
'--no-show-model-warnings',
|
|
562
|
+
'--map-tokens', mapTokens, // 0 for Groq (no repo map), 1024 for others
|
|
563
|
+
'--no-suggest-shell-commands', // Disable shell command suggestions
|
|
564
|
+
// Add files to context BEFORE --message so Aider can see them
|
|
565
|
+
...(filesToAdd && filesToAdd.length > 0 ? filesToAdd : []),
|
|
566
|
+
'--message', text
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
// Find aider command (same logic as runInBackground)
|
|
570
|
+
let aiderCommand = 'aider';
|
|
571
|
+
let finalArgs = args;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
execSync('which aider', { stdio: 'pipe' });
|
|
575
|
+
aiderCommand = 'aider';
|
|
576
|
+
finalArgs = args;
|
|
577
|
+
} catch {
|
|
578
|
+
try {
|
|
579
|
+
execSync('python3 -m aider --version', { stdio: 'pipe' });
|
|
580
|
+
aiderCommand = 'python3';
|
|
581
|
+
finalArgs = ['-m', 'aider', ...args];
|
|
582
|
+
} catch {
|
|
583
|
+
aiderCommand = 'python3';
|
|
584
|
+
finalArgs = ['-m', 'aider', ...args]; // Try anyway
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Suppress verbose logging for cleaner output
|
|
589
|
+
// this.logger.log(`Executing: ${aiderCommand} ${finalArgs.join(' ')}`);
|
|
590
|
+
// this.logger.log(`Working directory: ${cwd}`);
|
|
591
|
+
// this.logger.log(`Provider: ${provider}, Model: ${modelName}`);
|
|
592
|
+
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
const startTime = Date.now();
|
|
595
|
+
const proc = spawn(aiderCommand, finalArgs, {
|
|
596
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
597
|
+
env: env,
|
|
598
|
+
cwd: cwd,
|
|
599
|
+
// Don't create a new process group - stay in parent's group to receive signals
|
|
600
|
+
detached: false
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Store process for cleanup
|
|
604
|
+
this.runningProcesses.push(proc);
|
|
605
|
+
|
|
606
|
+
let stdout = '';
|
|
607
|
+
let stderr = '';
|
|
608
|
+
let lastOutputTime = Date.now();
|
|
609
|
+
let waitingForLLM = false;
|
|
610
|
+
|
|
611
|
+
// Loop detection - track repeated output
|
|
612
|
+
const recentOutputLines = [];
|
|
613
|
+
const MAX_RECENT_LINES = 50;
|
|
614
|
+
let loopDetected = false;
|
|
615
|
+
let rateLimitDetected = false;
|
|
616
|
+
|
|
617
|
+
// Status update interval - show progress every 10 seconds if no output
|
|
618
|
+
const statusInterval = setInterval(() => {
|
|
619
|
+
const timeSinceOutput = (Date.now() - lastOutputTime) / 1000;
|
|
620
|
+
if (timeSinceOutput > 10) {
|
|
621
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
622
|
+
const chalk = require('chalk');
|
|
623
|
+
const timestamp = getTimestamp();
|
|
624
|
+
console.log(chalk.yellow(`[${timestamp}] [AIDER] Still waiting... (${elapsed}s elapsed, ${timeSinceOutput.toFixed(0)}s since last output)`));
|
|
625
|
+
if (waitingForLLM) {
|
|
626
|
+
console.log(chalk.yellow(`[${timestamp}] [AIDER] LLM is processing - this may take 1-2 minutes`));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}, 10000);
|
|
630
|
+
|
|
631
|
+
// Hard timeout - kill process if it runs too long
|
|
632
|
+
let timeoutKilled = false;
|
|
633
|
+
const hardTimeout = setTimeout(() => {
|
|
634
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
635
|
+
const chalk = require('chalk');
|
|
636
|
+
const timestamp = getTimestamp();
|
|
637
|
+
console.log(chalk.red(`\n[${timestamp}] ⏰ TIMEOUT: Aider process exceeded ${timeoutMs/1000}s limit (ran for ${elapsed}s)`));
|
|
638
|
+
console.log(chalk.red(`[${timestamp}] Killing Aider process to prevent hanging...`));
|
|
639
|
+
timeoutKilled = true;
|
|
640
|
+
clearInterval(statusInterval);
|
|
641
|
+
proc.kill('SIGTERM');
|
|
642
|
+
setTimeout(() => proc.kill('SIGKILL'), 2000); // Force kill after 2s
|
|
643
|
+
}, timeoutMs);
|
|
644
|
+
|
|
645
|
+
proc.stdout.on('data', (data) => {
|
|
646
|
+
const chunk = data.toString();
|
|
647
|
+
stdout += chunk;
|
|
648
|
+
lastOutputTime = Date.now();
|
|
649
|
+
const timestamp = getTimestamp();
|
|
650
|
+
|
|
651
|
+
// Rate limit detection - kill process if rate limited
|
|
652
|
+
if ((chunk.includes('Rate limit reached') || chunk.includes('rate_limit_exceeded')) && !rateLimitDetected) {
|
|
653
|
+
rateLimitDetected = true;
|
|
654
|
+
const chalk = require('chalk');
|
|
655
|
+
console.log(chalk.yellow(`\n[${timestamp}] ⚠️ RATE LIMIT: API rate limit reached`));
|
|
656
|
+
console.log(chalk.yellow(`[${timestamp}] Stopping Aider - please try again later or upgrade your plan`));
|
|
657
|
+
|
|
658
|
+
// Mark provider as rate limited (parse duration from stderr)
|
|
659
|
+
// We'll mark it in the close event when we have full stderr
|
|
660
|
+
|
|
661
|
+
clearInterval(statusInterval);
|
|
662
|
+
proc.kill('SIGTERM');
|
|
663
|
+
setTimeout(() => proc.kill('SIGKILL'), 1000); // Force kill after 1s
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Loop detection - check for repeated SEARCH/REPLACE blocks
|
|
668
|
+
if (chunk.includes('<<<<<<< SEARCH') || chunk.includes('>>>>>>> REPLACE')) {
|
|
669
|
+
const normalizedChunk = chunk.trim().substring(0, 200); // First 200 chars
|
|
670
|
+
recentOutputLines.push(normalizedChunk);
|
|
671
|
+
|
|
672
|
+
// Keep only recent lines
|
|
673
|
+
if (recentOutputLines.length > MAX_RECENT_LINES) {
|
|
674
|
+
recentOutputLines.shift();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Check for loops - if we see the same block 3+ times
|
|
678
|
+
const occurrences = recentOutputLines.filter(line => line === normalizedChunk).length;
|
|
679
|
+
if (occurrences >= 3 && !loopDetected) {
|
|
680
|
+
loopDetected = true;
|
|
681
|
+
const chalk = require('chalk');
|
|
682
|
+
console.log(chalk.red(`\n[${timestamp}] ⚠️ LOOP DETECTED: Same output repeated ${occurrences} times!`));
|
|
683
|
+
console.log(chalk.red(`[${timestamp}] Killing Aider process to prevent infinite loop...`));
|
|
684
|
+
proc.kill('SIGTERM');
|
|
685
|
+
setTimeout(() => proc.kill('SIGKILL'), 2000); // Force kill after 2s
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Detect if we're waiting for LLM response
|
|
691
|
+
if (chunk.includes('Tokens:') || chunk.toLowerCase().includes('thinking')) {
|
|
692
|
+
waitingForLLM = true;
|
|
693
|
+
const chalk = require('chalk');
|
|
694
|
+
console.log(chalk.cyan(`[${timestamp}] [AIDER] Prompt sent to LLM, waiting for response...`));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Detect when LLM starts responding
|
|
698
|
+
if (waitingForLLM && (chunk.includes('####') || chunk.includes('```') || chunk.includes('SEARCH/REPLACE'))) {
|
|
699
|
+
waitingForLLM = false;
|
|
700
|
+
const chalk = require('chalk');
|
|
701
|
+
console.log(chalk.green(`[${timestamp}] [AIDER] LLM responding...`));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Filter and show useful output (Assistant responses, file edits, etc.)
|
|
705
|
+
// Skip verbose startup messages and warnings
|
|
706
|
+
const lines = chunk.split('\n');
|
|
707
|
+
for (const line of lines) {
|
|
708
|
+
const trimmed = line.trim();
|
|
709
|
+
if (!trimmed) continue;
|
|
710
|
+
|
|
711
|
+
// Show assistant responses and important messages
|
|
712
|
+
const shouldShow = trimmed.startsWith('Assistant:') ||
|
|
713
|
+
trimmed.startsWith('User:') ||
|
|
714
|
+
trimmed.includes('file listing') ||
|
|
715
|
+
trimmed.includes('Updated') ||
|
|
716
|
+
trimmed.includes('Created') ||
|
|
717
|
+
trimmed.includes('Tokens:') ||
|
|
718
|
+
(trimmed.startsWith('###') && !trimmed.includes('ONE LINE')) ||
|
|
719
|
+
// Show meaningful content (not just startup noise)
|
|
720
|
+
(!trimmed.includes('NotOpenSSLWarning') &&
|
|
721
|
+
!trimmed.includes('Warning:') &&
|
|
722
|
+
!trimmed.includes('Detected dumb terminal') &&
|
|
723
|
+
!trimmed.includes('Aider v') &&
|
|
724
|
+
!trimmed.includes('Model:') &&
|
|
725
|
+
!trimmed.includes('Git repo:') &&
|
|
726
|
+
!trimmed.includes('Repo-map:') &&
|
|
727
|
+
!trimmed.includes('─────────────────') &&
|
|
728
|
+
trimmed.length > 10); // Only show substantial lines
|
|
729
|
+
|
|
730
|
+
if (shouldShow) {
|
|
731
|
+
if (onOutput) {
|
|
732
|
+
onOutput(line + '\n');
|
|
733
|
+
} else {
|
|
734
|
+
// Fallback: just write directly if no callback
|
|
735
|
+
try {
|
|
736
|
+
const chalk = require('chalk');
|
|
737
|
+
process.stdout.write(chalk.gray(line + '\n'));
|
|
738
|
+
} catch {
|
|
739
|
+
process.stdout.write(line + '\n');
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
proc.stderr.on('data', (data) => {
|
|
747
|
+
const chunk = data.toString();
|
|
748
|
+
stderr += chunk;
|
|
749
|
+
|
|
750
|
+
// Only show actual errors, suppress warnings
|
|
751
|
+
if (chunk.includes('Error:') || chunk.includes('Failed:') ||
|
|
752
|
+
(chunk.includes('error') && !chunk.includes('NotOpenSSLWarning') && !chunk.includes('Warning:'))) {
|
|
753
|
+
if (onError) {
|
|
754
|
+
onError(chunk.trim());
|
|
755
|
+
} else {
|
|
756
|
+
this.logger.error('[Aider Error]', chunk.trim());
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
proc.on('close', (code) => {
|
|
762
|
+
// Remove process from tracking array
|
|
763
|
+
const index = this.runningProcesses.indexOf(proc);
|
|
764
|
+
if (index > -1) {
|
|
765
|
+
this.runningProcesses.splice(index, 1);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
clearInterval(statusInterval);
|
|
769
|
+
clearTimeout(hardTimeout);
|
|
770
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
771
|
+
const chalk = require('chalk');
|
|
772
|
+
const timestamp = getTimestamp();
|
|
773
|
+
|
|
774
|
+
if (timeoutKilled) {
|
|
775
|
+
console.log(chalk.red(`[${timestamp}] [AIDER] Process killed due to timeout after ${elapsed}s`));
|
|
776
|
+
resolve({
|
|
777
|
+
success: false,
|
|
778
|
+
output: stdout,
|
|
779
|
+
error: 'Timeout: Process exceeded maximum execution time',
|
|
780
|
+
exitCode: -1,
|
|
781
|
+
timeout: true
|
|
782
|
+
});
|
|
783
|
+
} else if (rateLimitDetected) {
|
|
784
|
+
console.log(chalk.yellow(`[${timestamp}] [AIDER] Process stopped due to rate limit after ${elapsed}s`));
|
|
785
|
+
|
|
786
|
+
// Mark provider as rate limited with duration from error message
|
|
787
|
+
const errorMessage = stderr + stdout; // Full error with duration
|
|
788
|
+
this.providerManager.markRateLimited(provider, modelName, errorMessage);
|
|
789
|
+
|
|
790
|
+
resolve({
|
|
791
|
+
success: false,
|
|
792
|
+
output: stdout,
|
|
793
|
+
error: 'Rate limit: API rate limit reached - please try again later',
|
|
794
|
+
errorMessage: errorMessage, // Full error with duration
|
|
795
|
+
exitCode: -1,
|
|
796
|
+
rateLimitDetected: true,
|
|
797
|
+
provider: provider,
|
|
798
|
+
model: modelName
|
|
799
|
+
});
|
|
800
|
+
} else if (loopDetected) {
|
|
801
|
+
console.log(chalk.red(`[${timestamp}] [AIDER] Process killed due to loop detection after ${elapsed}s`));
|
|
802
|
+
resolve({
|
|
803
|
+
success: false,
|
|
804
|
+
output: stdout,
|
|
805
|
+
error: 'Loop detected: Same output repeated multiple times',
|
|
806
|
+
exitCode: -1,
|
|
807
|
+
loopDetected: true
|
|
808
|
+
});
|
|
809
|
+
} else {
|
|
810
|
+
console.log(chalk.gray(`[${timestamp}] [AIDER] Process closed after ${elapsed}s with code: ${code}`));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (code === 0 && !timeoutKilled && !rateLimitDetected && !loopDetected) {
|
|
814
|
+
resolve({
|
|
815
|
+
success: true,
|
|
816
|
+
output: stdout,
|
|
817
|
+
stderr: stderr,
|
|
818
|
+
exitCode: code
|
|
819
|
+
});
|
|
820
|
+
} else if (!timeoutKilled && !loopDetected) {
|
|
821
|
+
resolve({
|
|
822
|
+
success: false,
|
|
823
|
+
output: stdout,
|
|
824
|
+
error: stderr || `Process exited with code ${code}`,
|
|
825
|
+
exitCode: code
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
proc.on('error', (error) => {
|
|
831
|
+
clearInterval(statusInterval);
|
|
832
|
+
clearTimeout(hardTimeout);
|
|
833
|
+
resolve({
|
|
834
|
+
success: false,
|
|
835
|
+
error: error.message,
|
|
836
|
+
exitCode: -1
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
} catch (error) {
|
|
841
|
+
return {
|
|
842
|
+
success: false,
|
|
843
|
+
error: error.message
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
module.exports = { AiderCLIManager };
|
|
850
|
+
|