vibecodingmachine-core 1.0.0 → 1.0.1

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.
Files changed (48) hide show
  1. package/.babelrc +13 -13
  2. package/README.md +28 -28
  3. package/__tests__/applescript-manager-claude-fix.test.js +286 -286
  4. package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
  5. package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
  6. package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
  7. package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
  8. package/__tests__/requirement-7-status-tracking.test.js +332 -332
  9. package/jest.config.js +18 -18
  10. package/jest.setup.js +12 -12
  11. package/package.json +47 -45
  12. package/src/auth/access-denied.html +119 -119
  13. package/src/auth/shared-auth-storage.js +230 -230
  14. package/src/autonomous-mode/feature-implementer.cjs +70 -70
  15. package/src/autonomous-mode/feature-implementer.js +425 -425
  16. package/src/chat-management/chat-manager.cjs +71 -71
  17. package/src/chat-management/chat-manager.js +342 -342
  18. package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
  19. package/src/ide-integration/aider-cli-manager.cjs +850 -850
  20. package/src/ide-integration/applescript-manager.cjs +1088 -1088
  21. package/src/ide-integration/applescript-manager.js +2802 -2802
  22. package/src/ide-integration/applescript-utils.js +306 -306
  23. package/src/ide-integration/cdp-manager.cjs +221 -221
  24. package/src/ide-integration/cdp-manager.js +321 -321
  25. package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
  26. package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
  27. package/src/ide-integration/continue-cli-manager.js +431 -431
  28. package/src/ide-integration/provider-manager.cjs +354 -354
  29. package/src/ide-integration/quota-detector.cjs +34 -34
  30. package/src/ide-integration/quota-detector.js +349 -349
  31. package/src/ide-integration/windows-automation-manager.js +262 -262
  32. package/src/index.cjs +43 -43
  33. package/src/index.js +17 -17
  34. package/src/llm/direct-llm-manager.cjs +609 -609
  35. package/src/ui/ButtonComponents.js +247 -247
  36. package/src/ui/ChatInterface.js +499 -499
  37. package/src/ui/StateManager.js +259 -259
  38. package/src/utils/audit-logger.cjs +116 -116
  39. package/src/utils/config-helpers.cjs +94 -94
  40. package/src/utils/config-helpers.js +94 -94
  41. package/src/utils/electron-update-checker.js +85 -78
  42. package/src/utils/gcloud-auth.cjs +394 -394
  43. package/src/utils/logger.cjs +193 -193
  44. package/src/utils/logger.js +191 -191
  45. package/src/utils/repo-helpers.cjs +120 -120
  46. package/src/utils/repo-helpers.js +120 -120
  47. package/src/utils/requirement-helpers.js +432 -432
  48. package/src/utils/update-checker.js +167 -167
@@ -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
+