vibecodingmachine-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.allnightai/REQUIREMENTS.md +11 -0
  2. package/.allnightai/temp/auto-status.json +6 -0
  3. package/.env +7 -0
  4. package/.eslintrc.js +16 -0
  5. package/README.md +85 -0
  6. package/bin/vibecodingmachine.js +274 -0
  7. package/jest.config.js +8 -0
  8. package/logs/audit/2025-11-07.jsonl +2 -0
  9. package/package.json +64 -0
  10. package/scripts/README.md +128 -0
  11. package/scripts/auto-start-wrapper.sh +92 -0
  12. package/scripts/postinstall.js +81 -0
  13. package/src/commands/auth.js +96 -0
  14. package/src/commands/auto-direct.js +1748 -0
  15. package/src/commands/auto.js +4692 -0
  16. package/src/commands/auto.js.bak +710 -0
  17. package/src/commands/ide.js +70 -0
  18. package/src/commands/repo.js +159 -0
  19. package/src/commands/requirements.js +161 -0
  20. package/src/commands/setup.js +91 -0
  21. package/src/commands/status.js +88 -0
  22. package/src/components/RequirementPage.js +0 -0
  23. package/src/file.js +0 -0
  24. package/src/index.js +5 -0
  25. package/src/main.js +0 -0
  26. package/src/ui/requirements-page.js +0 -0
  27. package/src/utils/auth.js +548 -0
  28. package/src/utils/auto-mode-ansi-ui.js +238 -0
  29. package/src/utils/auto-mode-simple-ui.js +161 -0
  30. package/src/utils/auto-mode-ui.js.bak.blessed +207 -0
  31. package/src/utils/auto-mode.js +65 -0
  32. package/src/utils/config.js +64 -0
  33. package/src/utils/interactive.js +3616 -0
  34. package/src/utils/keyboard-handler.js +152 -0
  35. package/src/utils/logger.js +4 -0
  36. package/src/utils/persistent-header.js +116 -0
  37. package/src/utils/provider-registry.js +128 -0
  38. package/src/utils/requirementUtils.js +0 -0
  39. package/src/utils/status-card.js +120 -0
  40. package/src/utils/status-manager.js +0 -0
  41. package/src/utils/status.js +0 -0
  42. package/src/utils/stdout-interceptor.js +127 -0
  43. package/tests/auto-mode.test.js +37 -0
  44. package/tests/config.test.js +34 -0
@@ -0,0 +1,4692 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { AppleScriptManager, ClineCLIManager, AiderCLIManager, ClaudeCodeCLIManager, logIDEMessage, runContinueCLICommand, runContinueCLIAutoMode } = require('@vibecodingmachine/core');
6
+ const { getRepoPath, getAutoConfig, setAutoConfig } = require('../utils/config');
7
+ const { checkAutoModeStatus, startAutoMode, stopAutoMode, updateAutoModeStatus } = require('../utils/auto-mode');
8
+ const logger = require('../utils/logger');
9
+ const { createKeyboardHandler } = require('../utils/keyboard-handler');
10
+ const { getProviderDefinitions, getProviderPreferences } = require('../utils/provider-registry');
11
+ const PROVIDER_DEFINITIONS = getProviderDefinitions();
12
+ const DIRECT_AGENT_IDS = PROVIDER_DEFINITIONS.filter(def => def.type === 'direct').map(def => def.id);
13
+ const PROVIDER_DEFINITION_MAP = new Map(PROVIDER_DEFINITIONS.map(def => [def.id, def]));
14
+ const { handleAutoStart: handleDirectAutoStart } = require('./auto-direct');
15
+
16
+ const DEFAULT_INSTRUCTION_TEXT = 'Follow INSTRUCTIONS.md from .vibecodingmachine directory.\n\nCRITICAL FOR IDE AGENTS: You MUST work through ALL stages (ACT → CLEAN UP → VERIFY → DONE) without stopping. Update the "🚦 Current Status" section in the REQUIREMENTS file as you progress through each stage. DO NOT stop after acknowledging stages - you must COMPLETE the work and set status to DONE in the requirements file. The CLI is monitoring the file and waiting for you to finish.';
17
+
18
+ /**
19
+ * Get available LLM providers from config and environment variables
20
+ * @param {Object} savedConfig - Config object from getAutoConfig()
21
+ * @returns {Array<{provider: string, model: string, apiKey?: string}>}
22
+ */
23
+ function getAvailableProviders(savedConfig) {
24
+ const providers = [];
25
+
26
+ // Check for Groq (current primary)
27
+ const groqApiKey = process.env.GROQ_API_KEY || savedConfig.groqApiKey;
28
+ if (groqApiKey && groqApiKey.trim()) {
29
+ providers.push({
30
+ provider: 'groq',
31
+ model: savedConfig.aiderModel || 'groq/llama-3.3-70b-versatile',
32
+ apiKey: groqApiKey
33
+ });
34
+ }
35
+
36
+ // Check for Anthropic (Claude)
37
+ const anthropicApiKey = process.env.ANTHROPIC_API_KEY || savedConfig.anthropicApiKey;
38
+ if (anthropicApiKey && anthropicApiKey.trim()) {
39
+ providers.push({
40
+ provider: 'anthropic',
41
+ model: 'claude-3-5-sonnet-20241022',
42
+ apiKey: anthropicApiKey
43
+ });
44
+ }
45
+
46
+ // Check for OpenAI
47
+ const openaiApiKey = process.env.OPENAI_API_KEY || savedConfig.openaiApiKey;
48
+ if (openaiApiKey && openaiApiKey.trim()) {
49
+ providers.push({
50
+ provider: 'openai',
51
+ model: 'gpt-4-turbo-2024-04-09',
52
+ apiKey: openaiApiKey
53
+ });
54
+ }
55
+
56
+ // Always add Ollama as fallback (local, no API key needed)
57
+ providers.push({
58
+ provider: 'ollama',
59
+ model: 'qwen2.5-coder:32b'
60
+ });
61
+
62
+ return providers;
63
+ }
64
+
65
+ /**
66
+ * Check if current requirement is DONE and actually completed
67
+ * @param {string} repoPath - Repository path
68
+ * @returns {Promise<{isDone: boolean, actuallyComplete: boolean, reason?: string}>}
69
+ */
70
+ async function isRequirementDone(repoPath) {
71
+ try {
72
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
73
+ const fs = require('fs-extra');
74
+ const reqPath = await getRequirementsPath(repoPath);
75
+
76
+ if (!reqPath || !await fs.pathExists(reqPath)) {
77
+ return { isDone: false, actuallyComplete: false };
78
+ }
79
+
80
+ const content = await fs.readFile(reqPath, 'utf8');
81
+ const lines = content.split('\n');
82
+
83
+ // Check RESPONSE section for "Status updated from VERIFY to DONE"
84
+ let inResponseSection = false;
85
+ let responseContent = '';
86
+ let statusIsDone = false;
87
+
88
+ for (const line of lines) {
89
+ const trimmed = line.trim();
90
+
91
+ if (trimmed.includes('## RESPONSE FROM LAST CHAT')) {
92
+ inResponseSection = true;
93
+ continue;
94
+ }
95
+
96
+ if (inResponseSection) {
97
+ if (trimmed.startsWith('##') && !trimmed.includes('RESPONSE')) {
98
+ break;
99
+ }
100
+ if (trimmed && !trimmed.startsWith('###')) {
101
+ responseContent += trimmed + ' ';
102
+
103
+ // Check for status progression completion
104
+ const upperLine = trimmed.toUpperCase();
105
+ if (upperLine.includes('STATUS UPDATED FROM VERIFY TO DONE') ||
106
+ upperLine.includes('STATUS: DONE') ||
107
+ upperLine.includes('MOVED TO VERIFIED')) {
108
+ statusIsDone = true;
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ if (!statusIsDone) {
115
+ return { isDone: false, actuallyComplete: false };
116
+ }
117
+
118
+ // Check if response shows actual work was done
119
+ const responseUpper = responseContent.toUpperCase();
120
+ const hasSubstantialResponse = responseContent.length > 100 && (
121
+ responseUpper.includes('COMPLETE') ||
122
+ responseUpper.includes('IMPLEMENTED') ||
123
+ responseUpper.includes('CREATED') ||
124
+ responseUpper.includes('UPDATED') ||
125
+ responseUpper.includes('ADDED') ||
126
+ responseUpper.includes('FIXED') ||
127
+ responseUpper.includes('VERIFIED') ||
128
+ responseUpper.match(/DONE.*-.*completed/i) ||
129
+ responseUpper.includes('100') && responseUpper.includes('REQUIREMENT') // For "create 100 requirements"
130
+ );
131
+
132
+ // Also check for vague responses that indicate incomplete work
133
+ const isVagueResponse = responseUpper.includes('I WILL') ||
134
+ responseUpper.includes('I\'LL') ||
135
+ responseUpper.includes('LET ME') ||
136
+ responseUpper.includes('I CAN') ||
137
+ responseUpper.includes('I SHOULD') ||
138
+ (responseUpper.includes('READ') && !responseUpper.includes('CREATED')) ||
139
+ (responseUpper.includes('UNDERSTAND') && !responseUpper.includes('IMPLEMENTED'));
140
+
141
+ if (statusIsDone && hasSubstantialResponse && !isVagueResponse) {
142
+ return { isDone: true, actuallyComplete: true };
143
+ }
144
+
145
+ if (statusIsDone && (!hasSubstantialResponse || isVagueResponse)) {
146
+ return {
147
+ isDone: true,
148
+ actuallyComplete: false,
149
+ reason: isVagueResponse ? 'Vague response detected' : 'No substantial work in response'
150
+ };
151
+ }
152
+
153
+ return { isDone: false, actuallyComplete: false };
154
+ } catch (error) {
155
+ return { isDone: false, actuallyComplete: false };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Move a requirement from Verified back to Requirements not yet completed
161
+ * @param {string} repoPath - Repository path
162
+ * @param {string} requirementText - The requirement text to move back
163
+ * @returns {Promise<{success: boolean, error?: string}>}
164
+ */
165
+ async function moveRequirementBackToTodo(repoPath, requirementText) {
166
+ try {
167
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
168
+ const fs = require('fs-extra');
169
+ const reqPath = await getRequirementsPath(repoPath);
170
+
171
+ if (!reqPath || !await fs.pathExists(reqPath)) {
172
+ return { success: false, error: 'Requirements file not found' };
173
+ }
174
+
175
+ const content = await fs.readFile(reqPath, 'utf8');
176
+ const lines = content.split('\n');
177
+ const updatedLines = [];
178
+
179
+ let inVerifiedSection = false;
180
+ let requirementFound = false;
181
+ let inNotYetCompleted = false;
182
+
183
+ for (let i = 0; i < lines.length; i++) {
184
+ const line = lines[i];
185
+
186
+ // Find verified section
187
+ if (line.includes('## āœ… Verified by AI screenshot')) {
188
+ inVerifiedSection = true;
189
+ updatedLines.push(line);
190
+ continue;
191
+ }
192
+
193
+ // Find not yet completed section
194
+ if (line.includes('## ā³ Requirements not yet completed')) {
195
+ inNotYetCompleted = true;
196
+ inVerifiedSection = false;
197
+ updatedLines.push(line);
198
+ continue;
199
+ }
200
+
201
+ // Check if we're in verified section and this is the requirement to move
202
+ if (inVerifiedSection && line.trim().startsWith('- ')) {
203
+ const lineRequirement = line.substring(2).trim();
204
+ // Remove date prefix if present (format: "2025-11-05: **requirement**")
205
+ const cleanRequirement = lineRequirement.replace(/^\d{4}-\d{2}-\d{2}:\s*\*\*/, '').replace(/\*\*$/, '').trim();
206
+ const cleanRequirementText = requirementText.replace(/\*\*/g, '').trim();
207
+
208
+ if (lineRequirement.includes(requirementText) || cleanRequirement === cleanRequirementText) {
209
+ // Skip this line (don't add it to updatedLines)
210
+ requirementFound = true;
211
+ continue;
212
+ }
213
+ updatedLines.push(line);
214
+ continue;
215
+ }
216
+
217
+ // If we're in not yet completed section and haven't added the requirement yet, add it at the top
218
+ if (inNotYetCompleted && !requirementFound && line.trim().startsWith('- ')) {
219
+ // Add the requirement before the first existing requirement
220
+ updatedLines.push(`- ${requirementText}`);
221
+ updatedLines.push('');
222
+ requirementFound = true; // Mark as added so we don't add it again
223
+ }
224
+
225
+ // Reset section flags when hitting new sections
226
+ if (line.trim().startsWith('##')) {
227
+ inVerifiedSection = false;
228
+ inNotYetCompleted = false;
229
+ }
230
+
231
+ updatedLines.push(line);
232
+ }
233
+
234
+ // If requirement wasn't found in verified section, try to add it to not yet completed anyway
235
+ if (!requirementFound) {
236
+ // Find the not yet completed section and add requirement at the top
237
+ for (let i = 0; i < updatedLines.length; i++) {
238
+ if (updatedLines[i].includes('## ā³ Requirements not yet completed')) {
239
+ // Find the first requirement line after this section
240
+ let insertIndex = i + 1;
241
+ while (insertIndex < updatedLines.length &&
242
+ (updatedLines[insertIndex].trim() === '' || updatedLines[insertIndex].trim().startsWith('#'))) {
243
+ insertIndex++;
244
+ }
245
+ updatedLines.splice(insertIndex, 0, `- ${requirementText}`, '');
246
+ requirementFound = true;
247
+ break;
248
+ }
249
+ }
250
+ }
251
+
252
+ if (requirementFound) {
253
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
254
+ return { success: true };
255
+ } else {
256
+ return { success: false, error: 'Requirement not found in verified section' };
257
+ }
258
+ } catch (error) {
259
+ return { success: false, error: error.message };
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Move a requirement from TODO to "Requirements needing manual feedback" section
265
+ * @param {string} repoPath - Repository path
266
+ * @param {string} requirementText - The requirement text to move
267
+ * @param {string} questions - Clarifying questions to add
268
+ * @returns {Promise<{success: boolean, error?: string}>}
269
+ */
270
+ async function moveRequirementToFeedback(repoPath, requirementText, questions, findings = null) {
271
+ try {
272
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
273
+ const fs = require('fs-extra');
274
+ const reqPath = await getRequirementsPath(repoPath);
275
+
276
+ if (!reqPath || !await fs.pathExists(reqPath)) {
277
+ return { success: false, error: 'Requirements file not found' };
278
+ }
279
+
280
+ const content = await fs.readFile(reqPath, 'utf8');
281
+ const lines = content.split('\n');
282
+ const updatedLines = [];
283
+
284
+ let inTodoSection = false;
285
+ let inFeedbackSection = false;
286
+ let requirementRemoved = false;
287
+ let feedbackAdded = false;
288
+
289
+ for (const line of lines) {
290
+ const trimmed = line.trim();
291
+
292
+ // Find TODO section
293
+ if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
294
+ inTodoSection = true;
295
+ inFeedbackSection = false;
296
+ updatedLines.push(line);
297
+ continue;
298
+ }
299
+
300
+ // Find "Requirements needing manual feedback" section
301
+ if (trimmed.startsWith('##') && (trimmed.includes('Requirements needing manual feedback') || trimmed.includes('ā“ Requirements needing'))) {
302
+ inTodoSection = false;
303
+ inFeedbackSection = true;
304
+ updatedLines.push(line);
305
+
306
+ // Add blank line if not already present
307
+ if (updatedLines[updatedLines.length - 2]?.trim() !== '') {
308
+ updatedLines.push('');
309
+ }
310
+
311
+ // Add requirement with findings and questions at the top of feedback section
312
+ if (!feedbackAdded) {
313
+ const today = new Date().toISOString().split('T')[0];
314
+ updatedLines.push(`- ${today}: **${requirementText}**`);
315
+
316
+ // Add AI findings if available
317
+ if (findings) {
318
+ updatedLines.push(' **AI found in codebase:**');
319
+ updatedLines.push(` ${findings}`);
320
+ updatedLines.push('');
321
+ }
322
+
323
+ updatedLines.push(' **Clarifying questions:**');
324
+ const questionLines = questions.split('\n');
325
+ for (const q of questionLines) {
326
+ updatedLines.push(` ${q}`);
327
+ }
328
+ updatedLines.push('');
329
+ feedbackAdded = true;
330
+ }
331
+ continue;
332
+ }
333
+
334
+ // Stop sections at next header
335
+ if (trimmed.startsWith('##')) {
336
+ inTodoSection = false;
337
+ inFeedbackSection = false;
338
+ }
339
+
340
+ // Remove requirement from TODO section (match first occurrence)
341
+ if (inTodoSection && !requirementRemoved && trimmed.startsWith('- ')) {
342
+ const reqText = trimmed.substring(2).trim();
343
+ // Clean both for comparison
344
+ const cleanReq = reqText.replace(/\*\*/g, '').replace(/^\d{4}-\d{2}-\d{2}:\s*/, '').trim();
345
+ const cleanTarget = requirementText.replace(/\*\*/g, '').trim();
346
+
347
+ if (cleanReq === cleanTarget || reqText.includes(requirementText) || requirementText.includes(cleanReq)) {
348
+ requirementRemoved = true;
349
+ continue; // Skip this line
350
+ }
351
+ }
352
+
353
+ updatedLines.push(line);
354
+ }
355
+
356
+ // If feedback section doesn't exist, create it
357
+ if (!feedbackAdded) {
358
+ // Find where to insert the feedback section (after TODO, before VERIFIED)
359
+ let insertIndex = -1;
360
+ for (let i = 0; i < updatedLines.length; i++) {
361
+ if (updatedLines[i].includes('## āœ… Verified by AI') || updatedLines[i].includes('---')) {
362
+ insertIndex = i;
363
+ break;
364
+ }
365
+ }
366
+
367
+ if (insertIndex === -1) {
368
+ insertIndex = updatedLines.length;
369
+ }
370
+
371
+ const today = new Date().toISOString().split('T')[0];
372
+ const feedbackSection = [
373
+ '',
374
+ '## ā“ Requirements needing manual feedback',
375
+ '',
376
+ `- ${today}: **${requirementText}**`
377
+ ];
378
+
379
+ // Add AI findings if available
380
+ if (findings) {
381
+ feedbackSection.push(' **AI found in codebase:**');
382
+ feedbackSection.push(` ${findings}`);
383
+ feedbackSection.push('');
384
+ }
385
+
386
+ feedbackSection.push(' **Clarifying questions:**');
387
+
388
+ const questionLines = questions.split('\n');
389
+ for (const q of questionLines) {
390
+ feedbackSection.push(` ${q}`);
391
+ }
392
+ feedbackSection.push('');
393
+
394
+ updatedLines.splice(insertIndex, 0, ...feedbackSection);
395
+ feedbackAdded = true;
396
+ }
397
+
398
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
399
+ return { success: true };
400
+ } catch (error) {
401
+ return { success: false, error: error.message };
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Move completed requirement from TODO to TO VERIFY section (for human verification)
407
+ * @param {string} repoPath - Repository path
408
+ * @param {string} completedRequirement - The completed requirement text
409
+ * @returns {Promise<{success: boolean, error?: string}>}
410
+ */
411
+ async function moveCompletedRequirement(repoPath, completedRequirement) {
412
+ try {
413
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
414
+ const fs = require('fs-extra');
415
+ const reqPath = await getRequirementsPath(repoPath);
416
+
417
+ if (!reqPath || !await fs.pathExists(reqPath)) {
418
+ return { success: false, error: 'Requirements file not found' };
419
+ }
420
+
421
+ const content = await fs.readFile(reqPath, 'utf8');
422
+ const lines = content.split('\n');
423
+
424
+ const updatedLines = [];
425
+ let inTodoSection = false;
426
+ let inVerifiedSection = false;
427
+ let removedCompleted = false;
428
+ let addedToVerified = false;
429
+
430
+ for (const line of lines) {
431
+ const trimmed = line.trim();
432
+
433
+ // Find TODO section
434
+ if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
435
+ inTodoSection = true;
436
+ inVerifiedSection = false;
437
+ updatedLines.push(line);
438
+ continue;
439
+ }
440
+
441
+ // Find TO VERIFY section
442
+ if (trimmed.startsWith('##') && (trimmed.includes('TO VERIFY') || trimmed.includes('Verified by AI screenshot'))) {
443
+ inTodoSection = false;
444
+ inVerifiedSection = true;
445
+ updatedLines.push(line);
446
+
447
+ // Add completed requirement at the top of TO VERIFY section
448
+ if (!addedToVerified) {
449
+ const today = new Date().toISOString().split('T')[0];
450
+ updatedLines.push(`- ${today}: **${completedRequirement}**`);
451
+ addedToVerified = true;
452
+ }
453
+ continue;
454
+ }
455
+
456
+ // Stop sections at next header
457
+ if (trimmed.startsWith('##')) {
458
+ inTodoSection = false;
459
+ inVerifiedSection = false;
460
+ }
461
+
462
+ // Remove completed requirement from TODO section (match first occurrence)
463
+ if (inTodoSection && !removedCompleted && trimmed.startsWith('- ')) {
464
+ const reqText = trimmed.substring(2).trim();
465
+ // Simple match - just check if this is the completed requirement
466
+ if (reqText === completedRequirement ||
467
+ reqText.includes(completedRequirement) ||
468
+ completedRequirement.includes(reqText)) {
469
+ removedCompleted = true;
470
+ continue; // Skip this line
471
+ }
472
+ }
473
+
474
+ updatedLines.push(line);
475
+ }
476
+
477
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
478
+ return { success: true };
479
+ } catch (error) {
480
+ return { success: false, error: error.message };
481
+ }
482
+ }
483
+ // Helper function to get timestamp for logs
484
+ function getTimestamp() {
485
+ const now = new Date();
486
+
487
+ // Format as 12-hour time with AM/PM
488
+ let hours = now.getHours();
489
+ const minutes = now.getMinutes().toString().padStart(2, '0');
490
+ const ampm = hours >= 12 ? 'PM' : 'AM';
491
+ hours = hours % 12;
492
+ hours = hours ? hours : 12; // 0 should be 12
493
+
494
+ // Get timezone abbreviation (e.g., MST, EST, PST)
495
+ const timeZoneString = now.toLocaleTimeString('en-US', { timeZoneName: 'short' });
496
+ const timezone = timeZoneString.split(' ').pop(); // Get last part (timezone)
497
+
498
+ return `${hours}:${minutes} ${ampm} ${timezone}`;
499
+ }
500
+
501
+ async function start(options) {
502
+ // STRICT AUTH CHECK
503
+ const auth = require('../utils/auth');
504
+ const isAuth = await auth.isAuthenticated();
505
+ if (!isAuth) {
506
+ console.log(chalk.cyan('\nšŸ” Opening browser for authentication...\n'));
507
+ try {
508
+ await auth.login();
509
+ console.log(chalk.green('\nāœ“ Authentication successful!\n'));
510
+ } catch (error) {
511
+ console.log(chalk.red('\nāœ— Authentication failed:'), error.message);
512
+ process.exit(1);
513
+ }
514
+ }
515
+
516
+ // Check for pending updates and install if available
517
+ if (global.pendingUpdate) {
518
+ const updateSpinner = ora('Installing update...').start();
519
+ try {
520
+ const { spawn } = require('child_process');
521
+
522
+ // Install update using npm
523
+ await new Promise((resolve, reject) => {
524
+ const npmInstall = spawn('npm', ['install', '-g', 'allnightai-cli'], {
525
+ stdio: 'inherit'
526
+ });
527
+
528
+ npmInstall.on('close', (code) => {
529
+ if (code === 0) {
530
+ updateSpinner.succeed('Update installed successfully!');
531
+ console.log(chalk.green('\nāœ“ Restarting with new version...\n'));
532
+
533
+ // Restart CLI with same arguments
534
+ const { spawn } = require('child_process');
535
+ const args = process.argv.slice(2);
536
+ spawn(process.argv[0], [process.argv[1], ...args], {
537
+ stdio: 'inherit',
538
+ detached: true
539
+ }).unref();
540
+
541
+ process.exit(0);
542
+ } else {
543
+ reject(new Error(`npm install exited with code ${code}`));
544
+ }
545
+ });
546
+
547
+ npmInstall.on('error', reject);
548
+ });
549
+ } catch (error) {
550
+ updateSpinner.fail('Update installation failed');
551
+ console.log(chalk.yellow('āš ļø Continuing with current version...'));
552
+ console.log(chalk.gray('Error:', error.message));
553
+ // Continue with auto mode even if update fails
554
+ }
555
+ }
556
+
557
+ const spinner = ora('Starting autonomous mode...').start();
558
+
559
+ try {
560
+ const repoPath = await getRepoPath();
561
+ if (!repoPath) {
562
+ spinner.fail('No repository configured');
563
+ console.log(chalk.gray('Run'), chalk.cyan('allnightai repo:set <path>'), chalk.gray('or'), chalk.cyan('allnightai repo:init'));
564
+ throw new Error('No repository configured');
565
+ }
566
+
567
+ // Read saved auto config first, then override with options
568
+ const savedConfig = await getAutoConfig();
569
+ const prefs = await getProviderPreferences();
570
+ const firstEnabledFromPrefs = prefs.order.find(id => prefs.enabled[id] !== false);
571
+ const fallbackAgent = firstEnabledFromPrefs || 'claude-code';
572
+ const savedAgent = savedConfig.agent || savedConfig.ide;
573
+ const requestedAgent = options.ide || savedAgent || fallbackAgent;
574
+ const providerDef = PROVIDER_DEFINITION_MAP.get(requestedAgent) || PROVIDER_DEFINITION_MAP.get(fallbackAgent);
575
+ const effectiveAgent = providerDef ? providerDef.id : 'claude-code';
576
+
577
+ const resolvedNeverStop = (() => {
578
+ if (options.neverStop !== undefined) return options.neverStop;
579
+ if (savedConfig.neverStop !== undefined) return savedConfig.neverStop;
580
+ if (!options.maxChats && !savedConfig.maxChats) return true;
581
+ return false;
582
+ })();
583
+
584
+ const resolvedMaxChats = options.maxChats !== undefined
585
+ ? options.maxChats
586
+ : savedConfig.maxChats || null;
587
+
588
+ const config = {
589
+ ide: effectiveAgent,
590
+ agent: effectiveAgent,
591
+ maxChats: resolvedMaxChats,
592
+ neverStop: resolvedNeverStop,
593
+ // Include Aider/Groq config for fallback
594
+ aiderModel: savedConfig.aiderModel,
595
+ aiderProvider: savedConfig.aiderProvider,
596
+ groqApiKey: savedConfig.groqApiKey
597
+ };
598
+
599
+ await setAutoConfig(config);
600
+
601
+ const providerType = providerDef?.type || 'direct';
602
+
603
+ if (providerType === 'direct') {
604
+ spinner.stop();
605
+ console.log(chalk.cyan('\nšŸ¤– Vibe Coding Machine - Direct API Auto Mode\n'));
606
+ await handleDirectAutoStart({
607
+ maxChats: config.neverStop ? undefined : (config.maxChats || undefined)
608
+ });
609
+ return;
610
+ }
611
+
612
+ // Check if this is just a provider configuration request (for Continue CLI)
613
+ if (config.ide === 'continue' && options.configureOnly) {
614
+ spinner.stop();
615
+ console.log(chalk.cyan('\nšŸ” Configure Continue CLI AI Provider\n'));
616
+ console.log(chalk.gray(' Continue CLI uses models configured in ~/.continue/config.yaml'));
617
+ console.log();
618
+
619
+ const fs = require('fs');
620
+ const path = require('path');
621
+ const os = require('os');
622
+ const inquirer = require('inquirer');
623
+ const yaml = require('js-yaml');
624
+ const configPath = path.join(os.homedir(), '.continue', 'config.yaml');
625
+
626
+ let currentConfig = null;
627
+ let models = [];
628
+
629
+ if (fs.existsSync(configPath)) {
630
+ try {
631
+ currentConfig = yaml.load(fs.readFileSync(configPath, 'utf8'));
632
+ models = currentConfig.models || [];
633
+
634
+ if (models.length > 0) {
635
+ const firstModel = models[0];
636
+ console.log(chalk.green('āœ“ Current configuration:'));
637
+ console.log(chalk.gray(' Provider:'), chalk.cyan(firstModel.provider || 'unknown'));
638
+ if (firstModel.model) {
639
+ console.log(chalk.gray(' Model:'), chalk.cyan(firstModel.model));
640
+ }
641
+ console.log();
642
+ }
643
+ } catch (error) {
644
+ console.log(chalk.yellow(' Configuration file exists but could not be read.'));
645
+ console.log(chalk.gray(' Edit manually:'), chalk.cyan(configPath));
646
+ console.log();
647
+ return;
648
+ }
649
+ }
650
+
651
+ // Provide interactive provider selection
652
+ const { action } = await inquirer.prompt([{
653
+ type: 'list',
654
+ name: 'action',
655
+ message: 'What would you like to do?',
656
+ choices: [
657
+ { name: 'Open config file in editor', value: 'open' },
658
+ { name: 'Show configuration instructions', value: 'instructions' },
659
+ { name: 'Cancel', value: 'cancel' }
660
+ ]
661
+ }]);
662
+
663
+ if (action === 'open') {
664
+ const { exec } = require('child_process');
665
+ const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === 'darwin' ? 'open' : 'nano');
666
+ console.log(chalk.gray(`\nOpening ${configPath} in ${editor}...\n`));
667
+
668
+ // Create directory if it doesn't exist
669
+ const configDir = path.dirname(configPath);
670
+ if (!fs.existsSync(configDir)) {
671
+ fs.mkdirSync(configDir, { recursive: true });
672
+ }
673
+
674
+ // Create default config if it doesn't exist
675
+ if (!fs.existsSync(configPath)) {
676
+ const defaultConfig = {
677
+ models: [
678
+ {
679
+ provider: 'ollama',
680
+ model: 'qwen2.5:1.5b'
681
+ }
682
+ ]
683
+ };
684
+ fs.writeFileSync(configPath, yaml.dump(defaultConfig));
685
+ console.log(chalk.green('āœ“ Created default config file'));
686
+ }
687
+
688
+ // Open in editor (non-blocking)
689
+ exec(`${editor} "${configPath}"`, (error) => {
690
+ if (error) {
691
+ console.log(chalk.red(`\nāœ— Failed to open editor: ${error.message}`));
692
+ console.log(chalk.gray(` Please manually edit: ${configPath}`));
693
+ }
694
+ });
695
+
696
+ console.log(chalk.green('\nāœ“ Config file opened in editor'));
697
+ console.log(chalk.gray(' After editing, restart Auto Mode to use the new configuration.\n'));
698
+ } else if (action === 'instructions') {
699
+ console.log(chalk.yellow('\n To change the AI provider:'));
700
+ console.log(chalk.gray(' 1. Edit the config file:'), chalk.cyan(configPath));
701
+ console.log(chalk.gray(' 2. Modify the "models" section with your preferred provider'));
702
+ console.log(chalk.gray(' 3. For Ollama, use:'));
703
+ console.log(chalk.gray(' models:'));
704
+ console.log(chalk.gray(' - provider: ollama'));
705
+ console.log(chalk.gray(' model: qwen2.5:1.5b'));
706
+ console.log();
707
+ console.log(chalk.gray(' See Continue CLI documentation for other providers.'));
708
+ console.log();
709
+ }
710
+
711
+ return;
712
+ }
713
+
714
+ // Only start auto mode if not configureOnly
715
+ if (!options.configureOnly) {
716
+ await startAutoMode(repoPath, config);
717
+ }
718
+
719
+ // Send initial instruction to IDE or Continue/Cline CLI
720
+ const textToSend = options.text || DEFAULT_INSTRUCTION_TEXT;
721
+
722
+ if (config.ide === 'continue') {
723
+ // Use Continue CLI with command-line approach
724
+ spinner.stop();
725
+ console.log(chalk.cyan('\nšŸš€ Starting Continue CLI in auto mode...\n'));
726
+ console.log(chalk.gray(' Continue CLI will use models configured in ~/.continue/config.yaml'));
727
+ console.log();
728
+
729
+ // Start the auto-mode loop for Continue CLI with callbacks
730
+ let exitReason = 'completed';
731
+ try {
732
+ const result = await runContinueCLIAutoMode(repoPath, textToSend, config, {
733
+ updateAutoModeStatus,
734
+ isRequirementDone,
735
+ moveToNextRequirement
736
+ });
737
+ exitReason = result?.exitReason || 'completed';
738
+ } catch (error) {
739
+ exitReason = 'error';
740
+ console.log(chalk.red(`\nāœ— Auto mode error: ${error.message}\n`));
741
+ } finally {
742
+ // Always clean up Auto Mode status when Continue CLI exits
743
+ await stopAutoMode(exitReason);
744
+
745
+ // If there was an error, pause so user can read the messages
746
+ if (exitReason === 'error') {
747
+ console.log(chalk.gray('\nPress Enter to return to menu...'));
748
+ await new Promise(resolve => {
749
+ process.stdin.once('data', () => resolve());
750
+ });
751
+ }
752
+ }
753
+ } else if (config.ide === 'claude-code') {
754
+ // Use Claude Code CLI
755
+ const claudeCodeManager = new ClaudeCodeCLIManager();
756
+
757
+ // Define common variables
758
+ const maxChats = config.maxChats || 0; // 0 = unlimited
759
+
760
+ // Quick check without spinner
761
+ spinner.stop();
762
+ const isInstalled = claudeCodeManager.isInstalled();
763
+
764
+ if (!isInstalled) {
765
+ spinner.text = 'Installing Claude Code CLI...';
766
+ const installResult = await claudeCodeManager.install();
767
+
768
+ if (!installResult.success) {
769
+ spinner.fail('Failed to install Claude Code CLI');
770
+ console.log(chalk.red('\nāœ— Error:'), installResult.error);
771
+ console.log(chalk.yellow('\nšŸ’” How to fix:'));
772
+ if (installResult.suggestions) {
773
+ installResult.suggestions.forEach(suggestion => {
774
+ console.log(chalk.gray(` ${suggestion}`));
775
+ });
776
+ }
777
+ console.log(chalk.gray('\n Or install manually:'));
778
+ console.log(chalk.cyan(' npm install -g @anthropic-ai/claude-code'));
779
+ console.log();
780
+
781
+ // Fallback to Aider
782
+ console.log(chalk.yellow('āš ļø Falling back to Aider CLI...\n'));
783
+ const aiderManager = new AiderCLIManager();
784
+ config.ide = 'aider';
785
+ // Continue with Aider below
786
+ } else {
787
+ spinner.succeed('Claude Code CLI installed');
788
+ }
789
+ } else {
790
+ spinner.succeed('Claude Code CLI detected');
791
+ console.log(chalk.gray(` Version: ${claudeCodeManager.getVersion()}`));
792
+ }
793
+
794
+ // Check if we're still using Claude Code (didn't fallback to Aider)
795
+ if (config.ide === 'claude-code') {
796
+ console.log(chalk.green(`\nāœ“ Using Claude Code CLI for autonomous mode\n`));
797
+
798
+ // Claude Code doesn't need provider/model configuration - it uses Anthropic's Claude models
799
+ console.log(chalk.cyan(`[${getTimestamp()}] šŸš€ Starting Auto Mode...`));
800
+ console.log(chalk.gray(` Model: Claude (Anthropic)`));
801
+ if (maxChats > 0) {
802
+ console.log(chalk.gray(` Max chats: ${maxChats}`));
803
+ } else {
804
+ console.log(chalk.gray(` Max chats: unlimited (will run until all requirements complete)`));
805
+ }
806
+ console.log(chalk.gray(` To stop: Press x or Ctrl+C, or run 'ana auto:stop'\n`));
807
+
808
+ // Initialize control variables
809
+ let shouldExit = false;
810
+ let exitReason = '';
811
+ let chatCount = 0;
812
+
813
+ // Set up keyboard handler for 'x' and Ctrl+C
814
+ const keyboardHandler = createKeyboardHandler({
815
+ onExit: () => {
816
+ console.log(chalk.yellow('\nšŸ›‘ Exit requested via keyboard...'));
817
+ shouldExit = true;
818
+ exitReason = 'user-exit';
819
+ claudeCodeManager.killAllProcesses();
820
+
821
+ setTimeout(async () => {
822
+ try {
823
+ await stopAutoMode(exitReason);
824
+ } catch (err) {
825
+ // Ignore
826
+ }
827
+ process.exit(0);
828
+ }, 500);
829
+ }
830
+ });
831
+ keyboardHandler.start();
832
+
833
+ // Helper function to print status card
834
+ function printStatusCard(currentTitle, currentStatus) {
835
+ const statusIcons = {
836
+ 'PREPARE': 'šŸ”Ø',
837
+ 'ACT': 'ā³',
838
+ 'CLEAN UP': 'ā³',
839
+ 'VERIFY': 'ā³',
840
+ 'DONE': 'ā³'
841
+ };
842
+
843
+ const stages = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
844
+ const stageMap = {
845
+ 'PREPARE': 0,
846
+ 'ACT': 1,
847
+ 'CLEAN UP': 2,
848
+ 'VERIFY': 3,
849
+ 'DONE': 4
850
+ };
851
+
852
+ const currentIndex = stageMap[currentStatus] || 0;
853
+ const workflowLine = stages.map((stage, idx) => {
854
+ if (idx < currentIndex) {
855
+ return `āœ… ${stage}`;
856
+ } else if (idx === currentIndex) {
857
+ return `šŸ”Ø ${stage}`;
858
+ } else {
859
+ return `ā³ ${stage}`;
860
+ }
861
+ }).join(' → ');
862
+
863
+ const titleShort = currentTitle?.substring(0, 56) + (currentTitle?.length > 56 ? '...' : '');
864
+
865
+ console.log(chalk.magenta('\n╭────────────────────────────────────────────────────────────────╮'));
866
+ console.log(chalk.magenta('│') + ' '.repeat(64) + chalk.magenta('│'));
867
+ console.log(chalk.magenta('│ ') + chalk.white(workflowLine.padEnd(62)) + chalk.magenta(' │'));
868
+ console.log(chalk.magenta('│') + ' '.repeat(64) + chalk.magenta('│'));
869
+ console.log(chalk.magenta('│ ') + chalk.white('šŸŽÆ Working on: ' + titleShort.padEnd(47)) + chalk.magenta(' │'));
870
+ console.log(chalk.magenta('│') + ' '.repeat(64) + chalk.magenta('│'));
871
+ console.log(chalk.magenta('╰────────────────────────────────────────────────────────────────╯\n'));
872
+ }
873
+
874
+ // Helper function to get current requirement details
875
+ async function getCurrentRequirementDetails() {
876
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
877
+ const fs = require('fs-extra');
878
+ const reqPath = await getRequirementsPath(repoPath);
879
+
880
+ if (!reqPath || !await fs.pathExists(reqPath)) {
881
+ return { title: null, status: 'PREPARE' };
882
+ }
883
+
884
+ const content = await fs.readFile(reqPath, 'utf8');
885
+ const lines = content.split('\n');
886
+
887
+ let title = null;
888
+ let status = 'PREPARE';
889
+ let inTodoSection = false;
890
+
891
+ for (const line of lines) {
892
+ if (line.includes('Requirements not yet completed') && line.trim().startsWith('##')) {
893
+ inTodoSection = true;
894
+ continue;
895
+ }
896
+
897
+ if (inTodoSection && line.trim().startsWith('- ')) {
898
+ title = line.substring(2).trim();
899
+ status = 'PREPARE';
900
+ break;
901
+ }
902
+
903
+ if (inTodoSection && line.trim().startsWith('##')) {
904
+ break;
905
+ }
906
+ }
907
+
908
+ return { title, status };
909
+ }
910
+
911
+ // Helper function to count requirements
912
+ async function getRequirementCounts() {
913
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
914
+ const fs = require('fs-extra');
915
+ const reqPath = await getRequirementsPath(repoPath);
916
+
917
+ if (!reqPath || !await fs.pathExists(reqPath)) {
918
+ return null;
919
+ }
920
+
921
+ const content = await fs.readFile(reqPath, 'utf8');
922
+ let todoCount = 0;
923
+ let toVerifyCount = 0;
924
+ let verifiedCount = 0;
925
+
926
+ const lines = content.split('\n');
927
+ let currentSection = '';
928
+
929
+ for (const line of lines) {
930
+ const trimmed = line.trim();
931
+
932
+ if (trimmed.startsWith('##')) {
933
+ if (trimmed.includes('ā³ Requirements not yet completed') ||
934
+ trimmed.includes('Requirements not yet completed')) {
935
+ currentSection = 'todo';
936
+ } else if (trimmed.includes('āœ… Verified by AI') ||
937
+ trimmed.includes('Verified by AI')) {
938
+ currentSection = 'verified';
939
+ } else {
940
+ currentSection = '';
941
+ }
942
+ }
943
+
944
+ if (currentSection && trimmed.startsWith('-')) {
945
+ const requirementText = trimmed.substring(2).trim();
946
+ if (requirementText) {
947
+ if (currentSection === 'todo') {
948
+ todoCount++;
949
+ } else if (currentSection === 'verified') {
950
+ verifiedCount++;
951
+ }
952
+ }
953
+ }
954
+ }
955
+
956
+ return { todoCount, toVerifyCount, verifiedCount };
957
+ }
958
+
959
+ try {
960
+ console.log(chalk.gray('[DEBUG] Starting Claude Code auto mode loop'));
961
+
962
+ // Get initial requirement counts
963
+ const initialCounts = await getRequirementCounts();
964
+ console.log(chalk.gray(`[DEBUG] Initial counts: ${JSON.stringify(initialCounts)}`));
965
+
966
+ // Check if there's a current requirement
967
+ let { title: currentTitle, status: currentStatus } = await getCurrentRequirementDetails();
968
+ console.log(chalk.gray(`[DEBUG] Current requirement: ${currentTitle || 'none'}, status: ${currentStatus}`));
969
+
970
+ if (!currentTitle) {
971
+ console.log(chalk.yellow('\nāš ļø No requirements found in TODO list.'));
972
+ console.log(chalk.green('šŸŽ‰ All requirements are completed or verified.'));
973
+ exitReason = 'completed';
974
+ throw new Error('No requirements to process');
975
+ }
976
+
977
+ let requirementsCompleted = 0;
978
+
979
+ // Main auto mode loop
980
+ if (currentTitle) {
981
+ while (true) {
982
+ // Check if user requested exit
983
+ if (shouldExit) {
984
+ console.log(chalk.yellow(`\nšŸ›‘ Exiting Auto Mode...\n`));
985
+ break;
986
+ }
987
+
988
+ // Check max chats
989
+ if (maxChats > 0 && chatCount >= maxChats) {
990
+ console.log(chalk.yellow(`\nāš ļø Maximum chats reached (${maxChats}). Stopping auto mode.`));
991
+ exitReason = 'max-chats';
992
+ break;
993
+ }
994
+
995
+ // Get current requirement details
996
+ const details = await getCurrentRequirementDetails();
997
+ currentTitle = details.title;
998
+ currentStatus = details.status;
999
+
1000
+ // Check if there's a requirement to work on
1001
+ if (!currentTitle) {
1002
+ console.log(chalk.green('\nšŸŽ‰ No more requirements to work on!'));
1003
+ console.log(chalk.gray(' All requirements are completed or verified.'));
1004
+ exitReason = 'completed';
1005
+ break;
1006
+ }
1007
+
1008
+ chatCount++;
1009
+ if (maxChats > 0) {
1010
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“ Chat ${chatCount}/${maxChats}`));
1011
+ } else {
1012
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“ Chat ${chatCount}`));
1013
+ }
1014
+
1015
+ console.log(chalk.gray(` ${requirementsCompleted} requirement(s) completed. ${maxChats > 0 ? `Will stop after ${maxChats} chat(s).` : 'No max set - will continue until all complete.'}`));
1016
+
1017
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1018
+ const reqPath = await getRequirementsPath(repoPath);
1019
+ const reqFilename = reqPath ? path.basename(reqPath) : 'REQUIREMENTS.md';
1020
+
1021
+ // Display status
1022
+ const statusIcons = {
1023
+ 'PREPARE': 'šŸ“‹',
1024
+ 'ACT': 'šŸ”Ø',
1025
+ 'CLEAN UP': '🧹',
1026
+ 'VERIFY': 'āœ…',
1027
+ 'DONE': 'šŸŽ‰'
1028
+ };
1029
+ const statusIcon = statusIcons[currentStatus] || 'ā³';
1030
+ console.log(chalk.cyan(`\n[${getTimestamp()}] ${statusIcon} Status: ${currentStatus}`));
1031
+ if (currentTitle) {
1032
+ console.log(chalk.white(` Working on: ${currentTitle.substring(0, 60)}${currentTitle.length > 60 ? '...' : ''}`));
1033
+ }
1034
+
1035
+ // Run Claude Code
1036
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ¤– Running Claude Code to work on requirement...\n`));
1037
+
1038
+ printStatusCard(currentTitle, currentStatus);
1039
+
1040
+ // Build prompt for Claude Code
1041
+ const promptText = `You are Claude Code, an AI coding assistant. Help implement this requirement:
1042
+
1043
+ "${currentTitle}"
1044
+
1045
+ INSTRUCTIONS:
1046
+ 1. Read the relevant code files in this repository
1047
+ 2. Make the minimal changes needed to implement this requirement
1048
+ 3. Follow best practices: DRY, KISS, clean code
1049
+ 4. Do NOT add placeholder code or TODOs
1050
+ 5. Do NOT edit ${reqFilename} (requirements file)
1051
+ 6. Test your changes if possible
1052
+
1053
+ IMPORTANT:
1054
+ - Make only the necessary changes (typically 1-10 lines)
1055
+ - Focus on quality over quantity
1056
+ - Follow the existing code style
1057
+
1058
+ Please implement this requirement now.`;
1059
+
1060
+ console.log(chalk.bold.cyan('\nšŸ“¤ Prompt sent to Claude Code:'));
1061
+ console.log(chalk.gray('─'.repeat(80)));
1062
+ console.log(chalk.white(promptText));
1063
+ console.log(chalk.gray('─'.repeat(80)));
1064
+ console.log();
1065
+
1066
+ // Send to Claude Code
1067
+ const claudeResult = await claudeCodeManager.sendText(
1068
+ promptText,
1069
+ repoPath,
1070
+ {
1071
+ onOutput: (output) => {
1072
+ process.stdout.write(output);
1073
+ },
1074
+ onError: (error) => {
1075
+ console.error(chalk.red(error));
1076
+ },
1077
+ timeoutMs: 300000 // 5 minute timeout
1078
+ }
1079
+ );
1080
+
1081
+ if (!claudeResult.success) {
1082
+ const errorMsg = claudeResult.error || '';
1083
+ const fullOutput = claudeResult.output || '';
1084
+ console.log(chalk.red(`\n[${getTimestamp()}] āœ— Claude Code failed: ${errorMsg}`));
1085
+
1086
+ // Check if it's a session limit error (check both error and output)
1087
+ const isSessionLimit = errorMsg.includes('Session limit') ||
1088
+ errorMsg.includes('session limit') ||
1089
+ fullOutput.includes('Session limit') ||
1090
+ fullOutput.includes('session limit');
1091
+
1092
+ // Save session limit for display in menu
1093
+ if (isSessionLimit) {
1094
+ const ProviderManager = require('@vibecodingmachine/core/src/ide-integration/provider-manager.cjs');
1095
+ const providerManager = new ProviderManager();
1096
+ providerManager.markRateLimited('claude-code', 'claude-code-cli', fullOutput || errorMsg);
1097
+ }
1098
+
1099
+ if (isSessionLimit && config.aiderProvider && config.groqApiKey) {
1100
+ console.log(chalk.yellow(`\n[${getTimestamp()}] šŸ”„ Session limit detected. Falling back to Groq API via Aider...`));
1101
+
1102
+ // Switch to Aider mode temporarily
1103
+ const tempConfig = { ...config, ide: 'aider' };
1104
+
1105
+ // Re-run this iteration with Aider
1106
+ console.log(chalk.cyan(`[${getTimestamp()}] šŸ”„ Retrying with Aider + Groq (${config.aiderModel})...\n`));
1107
+
1108
+ // We'll break out of the Claude Code loop and restart with Aider
1109
+ shouldExit = true;
1110
+ exitReason = 'fallback-to-aider';
1111
+ keyboardHandler.stop();
1112
+
1113
+ // Save the config with aider and restart
1114
+ await setAutoConfig(tempConfig);
1115
+
1116
+ console.log(chalk.yellow(`\n[${getTimestamp()}] ā™»ļø Restarting in Aider mode...\n`));
1117
+
1118
+ // Re-run the command with Aider
1119
+ const { spawn } = require('child_process');
1120
+ const proc = spawn(process.argv[0], [
1121
+ ...process.argv.slice(1)
1122
+ ], {
1123
+ stdio: 'inherit',
1124
+ cwd: process.cwd()
1125
+ });
1126
+
1127
+ proc.on('close', (code) => {
1128
+ process.exit(code);
1129
+ });
1130
+
1131
+ return;
1132
+ }
1133
+
1134
+ // Don't exit immediately, just continue to next iteration
1135
+ console.log(chalk.yellow(`\n[${getTimestamp()}] āš ļø Continuing to next chat...`));
1136
+ continue;
1137
+ }
1138
+
1139
+ console.log(chalk.green(`\n[${getTimestamp()}] āœ“ Claude Code completed successfully`));
1140
+
1141
+ // For now, mark as done after each successful chat
1142
+ // TODO: Implement proper status tracking based on Claude's output
1143
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“ Marking requirement as DONE...`));
1144
+
1145
+ // Mark as DONE (updateRequirementsStatus is defined as a local function above)
1146
+ // Note: We don't have this function yet in Claude Code section
1147
+ // For now, we'll just log success - proper status updates coming soon
1148
+
1149
+ requirementsCompleted++;
1150
+ console.log(chalk.green(`\n[${getTimestamp()}] āœ… Requirement marked as DONE`));
1151
+ }
1152
+ }
1153
+
1154
+ // Stop auto mode
1155
+ await stopAutoMode(exitReason);
1156
+ keyboardHandler.stop();
1157
+ } catch (error) {
1158
+ if (error.message === 'No requirements to process') {
1159
+ // Not really an error, just no work to do
1160
+ await stopAutoMode('completed');
1161
+ } else {
1162
+ console.error(chalk.red(`\nāœ— Error in Claude Code auto mode: ${error.message}`));
1163
+ console.error(chalk.gray(error.stack));
1164
+ await stopAutoMode('error');
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ }
1169
+ } else if (config.ide === 'aider') {
1170
+ // Use Aider CLI
1171
+ const aiderManager = new AiderCLIManager();
1172
+
1173
+ // Quick check without spinner (spinner was causing hang)
1174
+ spinner.stop();
1175
+ const isInstalled = aiderManager.isInstalled();
1176
+
1177
+ if (!isInstalled) {
1178
+ spinner.text = 'Installing Aider CLI...';
1179
+ const installResult = await aiderManager.install();
1180
+
1181
+ if (!installResult.success) {
1182
+ spinner.fail('Failed to install Aider CLI');
1183
+ console.log(chalk.red('\nāœ— Error:'), installResult.error);
1184
+ console.log(chalk.yellow('\nšŸ’” How to fix:'));
1185
+ if (installResult.suggestions) {
1186
+ installResult.suggestions.forEach(suggestion => {
1187
+ console.log(chalk.gray(` ${suggestion}`));
1188
+ });
1189
+ } else {
1190
+ console.log(chalk.gray(' Try installing Python first:'), chalk.cyan('brew install python'));
1191
+ console.log(chalk.gray(' Then install Aider CLI:'), chalk.cyan('pip3 install aider-chat'));
1192
+ }
1193
+ console.log(chalk.gray('\n Or install manually:'));
1194
+ console.log(chalk.cyan(' pip3 install aider-chat'));
1195
+ console.log(chalk.cyan(' or'));
1196
+ console.log(chalk.cyan(' python3 -m pip install aider-chat'));
1197
+ throw new Error(`Failed to install Aider CLI: ${installResult.error}`);
1198
+ }
1199
+
1200
+ spinner.succeed('Aider CLI installed successfully');
1201
+ } else {
1202
+ // Restart spinner for next step
1203
+ spinner.start();
1204
+ }
1205
+
1206
+ // Check if this is just a provider configuration request
1207
+ if (options.configureOnly) {
1208
+ spinner.stop();
1209
+ console.log(chalk.cyan('\nšŸ¤– Aider CLI AI Provider Configuration\n'));
1210
+
1211
+ // Verify Ollama API is accessible (better check than just command availability)
1212
+ const apiCheck = await aiderManager.verifyOllamaAPI();
1213
+ if (!apiCheck.success) {
1214
+ const hasOllamaCmd = aiderManager.isOllamaInstalled();
1215
+ if (!hasOllamaCmd) {
1216
+ console.log(chalk.yellow(' āš ļø Ollama is not installed\n'));
1217
+ console.log(chalk.gray(' Install Ollama:'), chalk.cyan('https://ollama.ai/download\n'));
1218
+ } else {
1219
+ console.log(chalk.yellow(' āš ļø Ollama service is not running\n'));
1220
+ console.log(chalk.gray(' Start Ollama:'), chalk.cyan('ollama serve'));
1221
+ console.log(chalk.gray(' Or open the Ollama app if you installed the desktop version\n'));
1222
+ }
1223
+ const inquirer = require('inquirer');
1224
+ await inquirer.prompt([{
1225
+ type: 'input',
1226
+ name: 'continue',
1227
+ message: 'Press Enter to return to menu...',
1228
+ }]);
1229
+ return;
1230
+ }
1231
+
1232
+ // Get installed models
1233
+ const installedModels = await aiderManager.getOllamaModels();
1234
+
1235
+ // Recommended coding models (based on Aider leaderboard)
1236
+ const recommendedModels = [
1237
+ { name: 'qwen2.5-coder:32b', display: 'qwen2.5-coder:32b (74% Aider benchmark - Best)', description: 'Best coding model' },
1238
+ { name: 'deepseek-coder-v2', display: 'deepseek-coder-v2 (73% Aider benchmark)', description: 'Excellent coding model' },
1239
+ { name: 'deepseek-coder:33b', display: 'deepseek-coder:33b (Better instruction following)', description: 'Better at following complex instructions' },
1240
+ { name: 'codellama:34b', display: 'codellama:34b (More reliable for refactoring)', description: 'Reliable for code refactoring tasks' },
1241
+ { name: 'qwen2.5-coder:14b', display: 'qwen2.5-coder:14b (69% Aider benchmark)', description: 'Good coding model' },
1242
+ { name: 'qwen2.5-coder:7b', display: 'qwen2.5-coder:7b (58% Aider benchmark)', description: 'Smaller coding model' },
1243
+ { name: 'llama3.1:8b', display: 'llama3.1:8b (Fallback)', description: 'General purpose model' }
1244
+ ];
1245
+
1246
+ // Build choices with status indicators and rate limit info
1247
+ const inquirer = require('inquirer');
1248
+ const ProviderManager = require('@vibecodingmachine/core/src/ide-integration/provider-manager.cjs');
1249
+ const providerManager = new ProviderManager();
1250
+
1251
+ const choices = recommendedModels.map(model => {
1252
+ const isInstalled = installedModels.includes(model.name);
1253
+ const statusIcon = isInstalled ? chalk.green('āœ“') : chalk.yellow('⚠');
1254
+ let statusText = isInstalled ? chalk.gray('(installed)') : chalk.yellow('(needs download)');
1255
+
1256
+ // Check for rate limits on Ollama models
1257
+ const timeUntilReset = providerManager.getTimeUntilReset('ollama', model.name);
1258
+ if (timeUntilReset) {
1259
+ const resetTime = Date.now() + timeUntilReset;
1260
+ const resetDate = new Date(resetTime);
1261
+ const timeStr = resetDate.toLocaleString('en-US', {
1262
+ weekday: 'short',
1263
+ month: 'short',
1264
+ day: 'numeric',
1265
+ hour: 'numeric',
1266
+ minute: '2-digit',
1267
+ hour12: true,
1268
+ timeZoneName: 'short'
1269
+ });
1270
+ statusText += ` ${chalk.red('ā° Rate limited until ' + timeStr)}`;
1271
+ }
1272
+
1273
+ return {
1274
+ name: `${statusIcon} ${model.display} ${statusText}`,
1275
+ value: { name: model.name, isInstalled }
1276
+ };
1277
+ });
1278
+
1279
+ // Add option to see all installed models
1280
+ if (installedModels.length > 0) {
1281
+ choices.push({ name: chalk.gray('─────────────────────────────'), disabled: true });
1282
+ choices.push({ name: chalk.cyan('View all installed models'), value: { name: '__view_all__' } });
1283
+ }
1284
+
1285
+ choices.push({ name: chalk.gray('← Back to menu'), value: null });
1286
+
1287
+ const { selection } = await inquirer.prompt([{
1288
+ type: 'list',
1289
+ name: 'selection',
1290
+ message: 'Select model to use:',
1291
+ choices: choices
1292
+ }]);
1293
+
1294
+ if (!selection) {
1295
+ return;
1296
+ }
1297
+
1298
+ if (selection.name === '__view_all__') {
1299
+ console.log(chalk.cyan('\nšŸ“‹ Installed Models:\n'));
1300
+ installedModels.forEach(model => {
1301
+ console.log(chalk.gray(' •'), chalk.cyan(model));
1302
+ });
1303
+ console.log();
1304
+ await inquirer.prompt([{
1305
+ type: 'input',
1306
+ name: 'continue',
1307
+ message: 'Press Enter to return to menu...',
1308
+ }]);
1309
+ return;
1310
+ }
1311
+
1312
+ const { name: selectedModel, isInstalled } = selection;
1313
+
1314
+ // If not installed, download it first
1315
+ if (!isInstalled) {
1316
+ console.log(chalk.cyan(`\nšŸ“„ Downloading ${selectedModel}...\n`));
1317
+ console.log(chalk.gray('This may take a few minutes depending on model size...\n'));
1318
+
1319
+ // Use Ollama HTTP API for model download
1320
+ const http = require('http');
1321
+ await new Promise((resolve) => {
1322
+ const downloadRequest = http.request({
1323
+ hostname: 'localhost',
1324
+ port: 11434,
1325
+ path: '/api/pull',
1326
+ method: 'POST',
1327
+ headers: {
1328
+ 'Content-Type': 'application/json',
1329
+ }
1330
+ }, (res) => {
1331
+ let lastStatus = '';
1332
+ res.on('data', (chunk) => {
1333
+ try {
1334
+ const lines = chunk.toString().split('\n').filter(l => l.trim());
1335
+ lines.forEach(line => {
1336
+ const data = JSON.parse(line);
1337
+ if (data.status) {
1338
+ if (data.total && data.completed) {
1339
+ // Show progress bar with percentage
1340
+ const percent = Math.round((data.completed / data.total) * 100);
1341
+ const completedMB = Math.round(data.completed / 1024 / 1024);
1342
+ const totalMB = Math.round(data.total / 1024 / 1024);
1343
+ const remainingMB = totalMB - completedMB;
1344
+
1345
+ // Create progress bar (50 chars wide)
1346
+ const barWidth = 50;
1347
+ const filledWidth = Math.round((percent / 100) * barWidth);
1348
+ const emptyWidth = barWidth - filledWidth;
1349
+ const bar = chalk.green('ā–ˆ'.repeat(filledWidth)) + chalk.gray('ā–‘'.repeat(emptyWidth));
1350
+
1351
+ process.stdout.write(`\r${bar} ${chalk.cyan(percent + '%')} ${chalk.white(completedMB + 'MB')} / ${chalk.gray(totalMB + 'MB')} ${chalk.yellow('(' + remainingMB + 'MB remaining)')}`);
1352
+ } else if (data.status !== lastStatus) {
1353
+ lastStatus = data.status;
1354
+ process.stdout.write(`\r${chalk.gray(data.status)}`.padEnd(120));
1355
+ }
1356
+ }
1357
+ });
1358
+ } catch (e) {
1359
+ // Ignore JSON parse errors for streaming responses
1360
+ }
1361
+ });
1362
+ });
1363
+
1364
+ downloadRequest.on('error', (error) => {
1365
+ console.log(chalk.red(`\n\nāœ— Download failed: ${error.message}\n`));
1366
+ resolve();
1367
+ });
1368
+
1369
+ downloadRequest.write(JSON.stringify({ name: selectedModel }));
1370
+ downloadRequest.end();
1371
+
1372
+ downloadRequest.on('close', async () => {
1373
+ console.log(chalk.green(`\n\nāœ“ Successfully downloaded ${selectedModel}\n`));
1374
+ // Save model preference
1375
+ const savedConfig = await getAutoConfig();
1376
+ savedConfig.aiderModel = selectedModel;
1377
+ await setAutoConfig(savedConfig);
1378
+ console.log(chalk.green(`āœ“ Set ${selectedModel} as default model\n`));
1379
+ resolve();
1380
+ });
1381
+ });
1382
+ } else {
1383
+ // Save model preference
1384
+ const savedConfig = await getAutoConfig();
1385
+ savedConfig.aiderModel = selectedModel;
1386
+ await setAutoConfig(savedConfig);
1387
+ console.log(chalk.green(`āœ“ Set ${selectedModel} as default model\n`));
1388
+ }
1389
+
1390
+ return;
1391
+ }
1392
+
1393
+ // Determine provider and model
1394
+ // Read provider and model from config
1395
+ const savedConfig = await getAutoConfig();
1396
+ let provider = savedConfig.aiderProvider || 'ollama'; // Default: ollama (local)
1397
+ let modelName = savedConfig.aiderModel || 'qwen2.5-coder:32b'; // Default: Best coding model (74% Aider benchmark)
1398
+ let bedrockEndpoint = null;
1399
+
1400
+ // Quick Ollama check (only for Ollama provider, don't block if it's slow)
1401
+ spinner.stop();
1402
+ if (provider === 'ollama') {
1403
+ try {
1404
+ const hasOllama = aiderManager.isOllamaInstalled();
1405
+ if (hasOllama) {
1406
+ // Try to get models, but don't wait too long
1407
+ const models = await Promise.race([
1408
+ aiderManager.getOllamaModels(),
1409
+ new Promise(resolve => setTimeout(() => resolve([]), 2000))
1410
+ ]);
1411
+
1412
+ if (models.length > 0) {
1413
+ // Check if saved model is installed, otherwise use preferred models
1414
+ const savedModel = savedConfig.aiderModel;
1415
+ if (savedModel && models.includes(savedModel)) {
1416
+ // Use saved model if it's installed
1417
+ modelName = savedModel;
1418
+ } else {
1419
+ // Prefer coding-specialized models (based on Aider leaderboard)
1420
+ const preferredModels = [
1421
+ 'qwen2.5-coder:32b', // 74% on Aider benchmark
1422
+ 'deepseek-coder-v2', // 73% on Aider benchmark
1423
+ 'deepseek-coder:33b', // Better instruction following
1424
+ 'codellama:34b', // More reliable for refactoring
1425
+ 'qwen2.5-coder:14b', // 69% on Aider benchmark
1426
+ 'qwen2.5-coder:7b', // 58% on Aider benchmark
1427
+ 'llama3.1:8b' // Fallback
1428
+ ];
1429
+
1430
+ // Find first preferred model that's installed
1431
+ for (const preferred of preferredModels) {
1432
+ if (models.includes(preferred)) {
1433
+ modelName = preferred;
1434
+ break;
1435
+ }
1436
+ }
1437
+ }
1438
+
1439
+ console.log(chalk.green(`āœ“ Using Ollama with model: ${modelName}`));
1440
+ if (savedModel && savedModel !== modelName) {
1441
+ console.log(chalk.yellow(` āš ļø Saved model "${savedModel}" not found, using ${modelName} instead`));
1442
+ console.log(chalk.gray(` Configure provider to download "${savedModel}"`));
1443
+ }
1444
+ }
1445
+ }
1446
+ } catch (error) {
1447
+ // Use default model if check fails
1448
+ console.log(chalk.gray(`Using default model: ${modelName}`));
1449
+ }
1450
+ } else {
1451
+ // For cloud providers (groq, anthropic, etc.), use configured model directly
1452
+ console.log(chalk.green(`āœ“ Using ${provider} with model: ${modelName}`));
1453
+
1454
+ // Check for required API keys for cloud providers
1455
+ if (provider === 'groq') {
1456
+ // Check for API key in environment OR config file
1457
+ let groqApiKey = process.env.GROQ_API_KEY;
1458
+ if (!groqApiKey || groqApiKey.trim() === '') {
1459
+ // Try loading from config file
1460
+ if (savedConfig.groqApiKey) {
1461
+ groqApiKey = savedConfig.groqApiKey;
1462
+ process.env.GROQ_API_KEY = groqApiKey; // Set in environment for Aider
1463
+ }
1464
+ }
1465
+
1466
+ if (!groqApiKey || groqApiKey.trim() === '') {
1467
+ console.log(chalk.cyan('\nšŸ“‹ How to get a FREE Groq API key:\n'));
1468
+ console.log(chalk.white(' 1. Go to: ') + chalk.cyan.underline('https://console.groq.com/'));
1469
+ console.log(chalk.white(' 2. Sign up (no credit card required!)'));
1470
+ console.log(chalk.white(' 3. Click "API Keys" in the top corner'));
1471
+ console.log(chalk.white(' 4. Click "Create API Key". Name it whatever you want, like Vibe Coding Machine'));
1472
+ console.log(chalk.white(' 5. Copy your key (starts with "gsk_...")'));
1473
+ console.log(chalk.white('\n✨ Groq Free Tier includes:'));
1474
+ console.log(chalk.gray(' • 14,400 requests/day'));
1475
+ console.log(chalk.gray(' • 30 requests/minute'));
1476
+ console.log(chalk.gray(' • Ultra-fast inference (<2s per request)'));
1477
+ console.log(chalk.gray(' • No credit card required!\n'));
1478
+
1479
+ // Offer to open browser
1480
+ const inquirer = require('inquirer');
1481
+ const { openBrowser } = await inquirer.prompt([{
1482
+ type: 'confirm',
1483
+ name: 'openBrowser',
1484
+ message: 'Open Groq Console in your browser?',
1485
+ default: true
1486
+ }]);
1487
+
1488
+ if (openBrowser) {
1489
+ const { execSync } = require('child_process');
1490
+ try {
1491
+ execSync('open "https://console.groq.com/"', { stdio: 'ignore' });
1492
+ console.log(chalk.green('\nāœ“ Opened browser to Groq Console\n'));
1493
+ console.log(chalk.gray(' After signing up, the API Keys page will be at:'));
1494
+ console.log(chalk.gray(' https://console.groq.com/keys\n'));
1495
+ } catch (error) {
1496
+ console.log(chalk.yellow('\n⚠ Could not open browser automatically.'));
1497
+ console.log(chalk.gray(' Please visit: https://console.groq.com/\n'));
1498
+ }
1499
+ }
1500
+
1501
+ const { continueChoice } = await inquirer.prompt([{
1502
+ type: 'list',
1503
+ name: 'continueChoice',
1504
+ message: 'What would you like to do?',
1505
+ choices: [
1506
+ { name: 'I have my API key - enter it now', value: 'enter' },
1507
+ { name: 'Switch back to Ollama (local)', value: 'ollama' },
1508
+ { name: 'Exit', value: 'exit' }
1509
+ ]
1510
+ }]);
1511
+
1512
+ if (continueChoice === 'exit') {
1513
+ console.log(chalk.gray('\n Exiting...\n'));
1514
+ return;
1515
+ } else if (continueChoice === 'ollama') {
1516
+ // Switch back to ollama
1517
+ savedConfig.aiderProvider = 'ollama';
1518
+ savedConfig.aiderModel = 'qwen2.5-coder:32b';
1519
+ await setAutoConfig(savedConfig);
1520
+ provider = 'ollama';
1521
+ modelName = 'qwen2.5-coder:32b';
1522
+ console.log(chalk.green('\nāœ“ Switched to Ollama\n'));
1523
+ } else if (continueChoice === 'enter') {
1524
+ // Prompt for API key
1525
+ const { apiKey } = await inquirer.prompt([{
1526
+ type: 'password',
1527
+ name: 'apiKey',
1528
+ message: 'Paste your Groq API key:',
1529
+ mask: '*',
1530
+ validate: (input) => {
1531
+ if (!input || input.trim() === '') {
1532
+ return 'API key cannot be empty';
1533
+ }
1534
+ if (!input.startsWith('gsk_')) {
1535
+ return 'Groq API keys should start with "gsk_"';
1536
+ }
1537
+ return true;
1538
+ }
1539
+ }]);
1540
+
1541
+ const trimmedKey = apiKey.trim();
1542
+
1543
+ // Set for current session
1544
+ process.env.GROQ_API_KEY = trimmedKey;
1545
+ console.log(chalk.green('\nāœ“ API key set for current session\n'));
1546
+
1547
+ // Save to config file for immediate persistence
1548
+ const { setAutoConfig } = require('../utils/config.js');
1549
+ await setAutoConfig({ groqApiKey: trimmedKey });
1550
+ console.log(chalk.green('āœ“ API key saved to Vibe Coding Machine config\n'));
1551
+
1552
+ // Add to shell profile for persistence
1553
+ const os = require('os');
1554
+ const fs = require('fs-extra');
1555
+ const shell = process.env.SHELL || '';
1556
+ let shellProfile = '';
1557
+
1558
+ if (shell.includes('zsh')) {
1559
+ shellProfile = path.join(os.homedir(), '.zshrc');
1560
+ } else if (shell.includes('bash')) {
1561
+ shellProfile = path.join(os.homedir(), '.bashrc');
1562
+ } else {
1563
+ shellProfile = path.join(os.homedir(), '.profile');
1564
+ }
1565
+
1566
+ try {
1567
+ const exportLine = `export GROQ_API_KEY="${trimmedKey}"`;
1568
+
1569
+ // Check if already exists
1570
+ let profileContent = '';
1571
+ if (await fs.pathExists(shellProfile)) {
1572
+ profileContent = await fs.readFile(shellProfile, 'utf8');
1573
+ }
1574
+
1575
+ if (profileContent.includes('GROQ_API_KEY')) {
1576
+ console.log(chalk.yellow('⚠ GROQ_API_KEY already exists in your shell profile'));
1577
+ const { replaceKey } = await inquirer.prompt([{
1578
+ type: 'confirm',
1579
+ name: 'replaceKey',
1580
+ message: 'Replace existing key with new one?',
1581
+ default: true
1582
+ }]);
1583
+
1584
+ if (replaceKey) {
1585
+ // Replace existing line
1586
+ const updatedContent = profileContent.replace(
1587
+ /export GROQ_API_KEY=.*/g,
1588
+ exportLine
1589
+ );
1590
+ await fs.writeFile(shellProfile, updatedContent, 'utf8');
1591
+ console.log(chalk.green(`āœ“ Updated GROQ_API_KEY in ${shellProfile}\n`));
1592
+ }
1593
+ } else {
1594
+ // Append new line
1595
+ await fs.appendFile(shellProfile, `\n# Groq API Key for VibeCodingMachine\n${exportLine}\n`, 'utf8');
1596
+ console.log(chalk.green(`āœ“ Added GROQ_API_KEY to ${shellProfile}\n`));
1597
+ }
1598
+
1599
+ console.log(chalk.gray(' Your API key will now persist across terminal sessions.'));
1600
+ console.log(chalk.green('\nāœ“ Setup complete! Continuing with Groq...\n'));
1601
+ } catch (error) {
1602
+ console.log(chalk.yellow('\n⚠ Could not save to shell profile automatically.'));
1603
+ console.log(chalk.gray(' You can manually add this line to your ~/.zshrc or ~/.bashrc:'));
1604
+ console.log(chalk.gray(` export GROQ_API_KEY="${trimmedKey}"\n`));
1605
+ console.log(chalk.green('āœ“ API key set for current session. Continuing...\n'));
1606
+ }
1607
+ }
1608
+ }
1609
+ } else if (provider === 'anthropic') {
1610
+ const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
1611
+ if (!anthropicApiKey || anthropicApiKey.trim() === '') {
1612
+ console.log(chalk.red('\nāŒ ANTHROPIC_API_KEY not found!'));
1613
+ console.log(chalk.cyan('\nšŸ“‹ Get your Anthropic API key at: ') + chalk.cyan.underline('https://console.anthropic.com/settings/keys'));
1614
+ console.log(chalk.white('\nSet it with: ') + chalk.gray('export ANTHROPIC_API_KEY="your_key_here"\n'));
1615
+ return;
1616
+ }
1617
+ }
1618
+ }
1619
+
1620
+ // Auto-mode loop for Aider CLI
1621
+ let chatCount = 0;
1622
+ let exitReason = 'completed';
1623
+ const maxChats = config.maxChats || 0; // 0 = unlimited
1624
+ let shouldExit = false;
1625
+ let sigintHandler = null;
1626
+
1627
+ // SIGINT handler for Ctrl+C (works even when child processes are running)
1628
+ sigintHandler = () => {
1629
+ console.log(chalk.yellow('\nšŸ›‘ Ctrl+C detected - exiting immediately...'));
1630
+ shouldExit = true;
1631
+ exitReason = 'user-exit';
1632
+
1633
+ // Kill Aider processes
1634
+ try {
1635
+ aiderManager.killAllProcesses();
1636
+ } catch (err) {
1637
+ // Ignore
1638
+ }
1639
+
1640
+ // Force exit after a short delay to allow cleanup
1641
+ setTimeout(async () => {
1642
+ try {
1643
+ await stopAutoMode(exitReason);
1644
+ } catch (err) {
1645
+ // Ignore
1646
+ }
1647
+ process.exit(0);
1648
+ }, 500);
1649
+ };
1650
+ process.on('SIGINT', sigintHandler);
1651
+
1652
+ // Watchdog timer - forcibly exit if shouldExit is set
1653
+ // This runs periodically to check if user wants to exit
1654
+ // (workaround for SIGINT not being processed reliably when blocked on child processes)
1655
+ const fs = require('fs-extra');
1656
+ const stopFilePath = path.join(os.homedir(), '.config', 'allnightai', '.stop');
1657
+
1658
+ const watchdog = setInterval(async () => {
1659
+ // Check for stop file (created by 'vcm auto:stop' or user)
1660
+ if (await fs.pathExists(stopFilePath)) {
1661
+ console.log(chalk.yellow('\nšŸ›‘ Stop signal detected (via .stop file)'));
1662
+ shouldExit = true;
1663
+ exitReason = 'user-stop';
1664
+
1665
+ // Remove the stop file
1666
+ try {
1667
+ await fs.unlink(stopFilePath);
1668
+ } catch (err) {
1669
+ // Ignore errors
1670
+ }
1671
+ }
1672
+
1673
+ if (shouldExit) {
1674
+ clearInterval(watchdog);
1675
+ try {
1676
+ aiderManager.killAllProcesses();
1677
+ } catch (err) {
1678
+ // Ignore
1679
+ }
1680
+ // Don't call process.exit() here, just let the loop break naturally
1681
+ }
1682
+ }, 500);
1683
+
1684
+ // Set up keyboard handler for 'x' key (Ctrl+C is handled by SIGINT above)
1685
+ const keyboardHandler = createKeyboardHandler({
1686
+ onExit: () => {
1687
+ console.log(chalk.yellow('\nšŸ›‘ Exit requested via keyboard (x key)...'));
1688
+ shouldExit = true;
1689
+ exitReason = 'user-exit';
1690
+ aiderManager.killAllProcesses();
1691
+ clearInterval(watchdog);
1692
+
1693
+ // Force exit after a short delay to allow cleanup (async file operations)
1694
+ setTimeout(async () => {
1695
+ try {
1696
+ await stopAutoMode(exitReason);
1697
+ } catch (err) {
1698
+ // Ignore
1699
+ }
1700
+ process.exit(0);
1701
+ }, 500);
1702
+ }
1703
+ });
1704
+ keyboardHandler.start();
1705
+
1706
+ // Startup message
1707
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸš€ Starting Auto Mode...`));
1708
+ console.log(chalk.gray(` Model: ${modelName}`));
1709
+ if (maxChats > 0) {
1710
+ console.log(chalk.gray(` Max chats: ${maxChats}`));
1711
+ }
1712
+ console.log(chalk.gray(` To stop: Press `) + chalk.white.bold('x') + chalk.gray(' or ') + chalk.white.bold('Ctrl+C') + chalk.gray(', or run \'ana auto:stop\''));
1713
+ console.log();
1714
+
1715
+ // Helper function to print the purple status card
1716
+ function printStatusCard(currentTitle, currentStatus) {
1717
+ const statusIcons = {
1718
+ 'PREPARE': 'šŸ”Ø',
1719
+ 'ACT': 'ā³',
1720
+ 'CLEAN UP': 'ā³',
1721
+ 'VERIFY': 'ā³',
1722
+ 'DONE': 'ā³'
1723
+ };
1724
+
1725
+ // Build the workflow line with proper icons
1726
+ const stages = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
1727
+ const stageMap = {
1728
+ 'PREPARE': 0,
1729
+ 'ACT': 1,
1730
+ 'CLEAN UP': 2,
1731
+ 'VERIFY': 3,
1732
+ 'DONE': 4
1733
+ };
1734
+ const currentIndex = stageMap[currentStatus] || 0;
1735
+
1736
+ const workflowLine = stages.map((stage, idx) => {
1737
+ const icon = idx < currentIndex ? 'āœ…' : (idx === currentIndex ? 'šŸ”Ø' : 'ā³');
1738
+ const color = idx === currentIndex ? chalk.cyan.bold : chalk.gray;
1739
+ return `${icon} ${color(stage)}`;
1740
+ }).join(' → ');
1741
+
1742
+ // Use string-width library for accurate terminal width calculation
1743
+ const stringWidth = require('string-width');
1744
+
1745
+ // Fixed box width: 64 chars total (including borders)
1746
+ // Content area: 62 chars (64 - 2 for left/right borders)
1747
+ const contentWidth = 62;
1748
+
1749
+ const workflowVisualLen = stringWidth(workflowLine);
1750
+ const workflowPadding = Math.max(0, contentWidth - workflowVisualLen);
1751
+
1752
+ // Build requirement line and truncate to fit
1753
+ const prefix = 'šŸŽÆ Working on: ';
1754
+ const prefixWidth = stringWidth(chalk.bold.white(prefix));
1755
+ const maxTitleWidth = contentWidth - prefixWidth;
1756
+
1757
+ let displayReq = currentTitle || 'Loading...';
1758
+ while (stringWidth(displayReq) > maxTitleWidth && displayReq.length > 3) {
1759
+ displayReq = displayReq.substring(0, displayReq.length - 4) + '...';
1760
+ }
1761
+
1762
+ const reqLineContent = chalk.bold.white(prefix + displayReq);
1763
+ const reqVisualLen = stringWidth(reqLineContent);
1764
+ const reqPadding = Math.max(0, contentWidth - reqVisualLen);
1765
+
1766
+ console.log(chalk.magenta('\n╭────────────────────────────────────────────────────────────────╮'));
1767
+ console.log(chalk.magenta('│') + ' ' + chalk.magenta('│'));
1768
+ console.log(chalk.magenta('│') + ' ' + workflowLine + ' '.repeat(workflowPadding) + ' ' + chalk.magenta('│'));
1769
+ console.log(chalk.magenta('│') + ' ' + chalk.magenta('│'));
1770
+ console.log(chalk.magenta('│') + ' ' + reqLineContent + ' '.repeat(reqPadding) + ' ' + chalk.magenta('│'));
1771
+ console.log(chalk.magenta('│') + ' ' + chalk.magenta('│'));
1772
+ console.log(chalk.magenta('╰────────────────────────────────────────────────────────────────╯\n'));
1773
+ }
1774
+
1775
+ // Helper function to get current requirement details
1776
+ async function getCurrentRequirementDetails() {
1777
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1778
+ const fs = require('fs-extra');
1779
+ const reqPath = await getRequirementsPath(repoPath);
1780
+
1781
+ if (!reqPath || !await fs.pathExists(reqPath)) {
1782
+ return { title: null, status: 'PREPARE' };
1783
+ }
1784
+
1785
+ const content = await fs.readFile(reqPath, 'utf8');
1786
+ const lines = content.split('\n');
1787
+
1788
+ let title = null;
1789
+ let status = 'PREPARE';
1790
+ let inTodoSection = false;
1791
+
1792
+ // Read from TODO list instead of "Current In Progress Requirement" section
1793
+ for (const line of lines) {
1794
+ // Find TODO section
1795
+ if (line.includes('Requirements not yet completed') && line.trim().startsWith('##')) {
1796
+ inTodoSection = true;
1797
+ continue;
1798
+ }
1799
+
1800
+ // Get first TODO item
1801
+ if (inTodoSection && line.trim().startsWith('- ')) {
1802
+ title = line.substring(2).trim();
1803
+ // Always start at PREPARE for TODO items
1804
+ status = 'PREPARE';
1805
+ break;
1806
+ }
1807
+
1808
+ // Stop if we hit another section
1809
+ if (inTodoSection && line.trim().startsWith('##')) {
1810
+ break;
1811
+ }
1812
+ }
1813
+
1814
+ return { title, status };
1815
+ }
1816
+
1817
+ // Helper function to directly update REQUIREMENTS file status
1818
+ // This bypasses Aider to preserve file structure
1819
+ async function updateRequirementsStatus(newStatus, responseText) {
1820
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1821
+ const fs = require('fs-extra');
1822
+ const reqPath = await getRequirementsPath(repoPath);
1823
+
1824
+ if (!reqPath || !await fs.pathExists(reqPath)) {
1825
+ return { success: false, error: 'Requirements file not found' };
1826
+ }
1827
+
1828
+ try {
1829
+ const content = await fs.readFile(reqPath, 'utf8');
1830
+ const lines = content.split('\n');
1831
+ const updatedLines = [];
1832
+ let inResponseSection = false;
1833
+ let responseUpdated = false;
1834
+
1835
+ for (let i = 0; i < lines.length; i++) {
1836
+ const line = lines[i];
1837
+
1838
+ // Handle response section
1839
+ if (line.includes('## RESPONSE FROM LAST CHAT')) {
1840
+ inResponseSection = true;
1841
+ updatedLines.push(line);
1842
+ continue;
1843
+ }
1844
+
1845
+ // End of response section
1846
+ if (line.trim().startsWith('##') && inResponseSection && !line.includes('RESPONSE')) {
1847
+ inResponseSection = false;
1848
+ }
1849
+
1850
+ // Update response line - append to existing content
1851
+ if (inResponseSection && line.trim() && !line.trim().startsWith('#') && !responseUpdated) {
1852
+ updatedLines.push('');
1853
+ updatedLines.push(responseText);
1854
+ responseUpdated = true;
1855
+ continue;
1856
+ }
1857
+
1858
+ updatedLines.push(line);
1859
+ }
1860
+
1861
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
1862
+ return { success: true };
1863
+ } catch (error) {
1864
+ return { success: false, error: error.message };
1865
+ }
1866
+ }
1867
+
1868
+ // Helper function to count requirements
1869
+ async function getRequirementCounts() {
1870
+ const { getRequirementsPath, getVibeCodingMachineDir } = require('@vibecodingmachine/core');
1871
+ const fs = require('fs-extra');
1872
+ const reqPath = await getRequirementsPath(repoPath);
1873
+
1874
+ if (!reqPath || !await fs.pathExists(reqPath)) {
1875
+ return null;
1876
+ }
1877
+
1878
+ const content = await fs.readFile(reqPath, 'utf8');
1879
+ let todoCount = 0;
1880
+ let toVerifyCount = 0;
1881
+ let verifiedCount = 0;
1882
+
1883
+ const lines = content.split('\n');
1884
+ let currentSection = '';
1885
+
1886
+ for (const line of lines) {
1887
+ const trimmed = line.trim();
1888
+
1889
+ if (trimmed.startsWith('##')) {
1890
+ if (trimmed.includes('ā³ Requirements not yet completed') ||
1891
+ trimmed.includes('Requirements not yet completed')) {
1892
+ currentSection = 'todo';
1893
+ } else if (trimmed.includes('āœ… Verified by AI') ||
1894
+ trimmed.includes('Verified by AI')) {
1895
+ currentSection = 'verified';
1896
+ } else {
1897
+ currentSection = '';
1898
+ }
1899
+ }
1900
+
1901
+ if (currentSection && trimmed.startsWith('-')) {
1902
+ const requirementText = trimmed.substring(2).trim();
1903
+ if (requirementText) {
1904
+ if (currentSection === 'todo') {
1905
+ todoCount++;
1906
+ } else if (currentSection === 'verified') {
1907
+ verifiedCount++;
1908
+ }
1909
+ }
1910
+ }
1911
+ }
1912
+
1913
+ return { todoCount, toVerifyCount, verifiedCount };
1914
+ }
1915
+
1916
+ try {
1917
+ console.log(chalk.gray('[DEBUG] Starting aider auto mode loop'));
1918
+
1919
+ // Get initial requirement counts
1920
+ const initialCounts = await getRequirementCounts();
1921
+ console.log(chalk.gray(`[DEBUG] Initial counts: ${JSON.stringify(initialCounts)}`));
1922
+
1923
+ // Check if there's a current requirement - if not, move the first TODO to current
1924
+ let { title: currentTitle, status: currentStatus } = await getCurrentRequirementDetails();
1925
+ console.log(chalk.gray(`[DEBUG] Current requirement: ${currentTitle || 'none'}, status: ${currentStatus}`));
1926
+
1927
+ if (!currentTitle) {
1928
+ console.log(chalk.yellow('\nāš ļø No requirements found in TODO list.'));
1929
+ console.log(chalk.green('šŸŽ‰ All requirements are completed or verified.'));
1930
+ exitReason = 'completed';
1931
+ throw new Error('No requirements to process');
1932
+ }
1933
+
1934
+ // Track requirements completed counter
1935
+ let requirementsCompleted = 0;
1936
+
1937
+ // Only start the loop if we have a current requirement
1938
+ if (currentTitle) {
1939
+ while (true) {
1940
+ // Check if user requested exit via Ctrl+C
1941
+ if (shouldExit) {
1942
+ console.log(chalk.yellow(`\nšŸ›‘ Exiting Auto Mode...\n`));
1943
+ break;
1944
+ }
1945
+
1946
+ // Check max chats
1947
+ if (maxChats > 0 && chatCount >= maxChats) {
1948
+ console.log(chalk.yellow(`\nāš ļø Maximum chats reached (${maxChats}). Stopping auto mode.`));
1949
+ exitReason = 'max-chats';
1950
+ break;
1951
+ }
1952
+
1953
+ // Get current requirement details BEFORE incrementing chat count
1954
+ const details = await getCurrentRequirementDetails();
1955
+ currentTitle = details.title;
1956
+ currentStatus = details.status;
1957
+
1958
+ // Check if there's a requirement to work on
1959
+ if (!currentTitle) {
1960
+ console.log(chalk.green('\nšŸŽ‰ No more requirements to work on!'));
1961
+ console.log(chalk.gray(' All requirements are completed or verified.'));
1962
+ exitReason = 'completed';
1963
+ break;
1964
+ }
1965
+
1966
+ chatCount++;
1967
+ // Show progress without verbose prompt text
1968
+ if (maxChats > 0) {
1969
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“ Chat ${chatCount}/${maxChats}`));
1970
+ } else {
1971
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“ Chat ${chatCount}`));
1972
+ }
1973
+
1974
+ // Show requirements progress
1975
+ if (maxChats > 0) {
1976
+ console.log(chalk.gray(` ${requirementsCompleted} requirement(s) completed. Will stop after ${maxChats} chat(s).`));
1977
+ } else {
1978
+ console.log(chalk.gray(` ${requirementsCompleted} requirement(s) completed. No max set - will continue until all complete.`));
1979
+ }
1980
+
1981
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1982
+ const reqPath = await getRequirementsPath(repoPath);
1983
+ const reqFilename = reqPath ? path.basename(reqPath) : 'REQUIREMENTS.md';
1984
+
1985
+ // Display status progress
1986
+ const statusIcons = {
1987
+ 'PREPARE': 'šŸ“‹',
1988
+ 'ACT': 'šŸ”Ø',
1989
+ 'CLEAN UP': '🧹',
1990
+ 'VERIFY': 'āœ…',
1991
+ 'DONE': 'šŸŽ‰'
1992
+ };
1993
+ const statusIcon = statusIcons[currentStatus] || 'ā³';
1994
+ console.log(chalk.cyan(`\n[${getTimestamp()}] ${statusIcon} Status: ${currentStatus}`));
1995
+ if (currentTitle) {
1996
+ console.log(chalk.white(` Working on: ${currentTitle.substring(0, 60)}${currentTitle.length > 60 ? '...' : ''}`));
1997
+ }
1998
+
1999
+ // Run Aider to work on the requirement
2000
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ¤– Running Aider to work on requirement...\n`));
2001
+
2002
+ // Show status card before starting
2003
+ printStatusCard(currentTitle, currentStatus);
2004
+
2005
+ // Update status to ACT (programmatically, before Aider runs)
2006
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“ Updating status to ACT...`));
2007
+ await updateRequirementsStatus('ACT', `Starting implementation of: ${currentTitle}`);
2008
+ printStatusCard(currentTitle, 'ACT'); // Show status card after transition
2009
+
2010
+ // META-AI: Ask a fast model to choose the best model for this requirement
2011
+ // ONLY for Ollama (cloud providers use configured model)
2012
+ let selectedModel = modelName; // Default to configured model
2013
+
2014
+ if (provider === 'ollama') {
2015
+ console.log(chalk.cyan(`\n[${getTimestamp()}] 🧠 Selecting optimal model for this requirement...`));
2016
+
2017
+ const modelSelectionPrompt = `You are a model selection expert. Analyze this requirement and choose the BEST model from this list:
2018
+
2019
+ REQUIREMENT:
2020
+ "${currentTitle}"
2021
+
2022
+ AVAILABLE MODELS:
2023
+ 1. qwen2.5-coder:32b - 74% Aider benchmark, best overall code quality, slower
2024
+ 2. deepseek-coder:33b - 73% Aider benchmark, better instruction following, good for complex workflows
2025
+ 3. codellama:34b - More reliable for refactoring tasks
2026
+ 4. qwen2.5-coder:14b - 69% Aider benchmark, faster, good for simple tasks
2027
+ 5. qwen2.5-coder:7b - 58% Aider benchmark, fastest, only for trivial tasks
2028
+
2029
+ SELECTION CRITERIA:
2030
+ - Simple UI changes (add text, display info) → qwen2.5-coder:14b or 7b (fast)
2031
+ - Complex refactoring (DRY, extract functions, reorganize) → codellama:34b
2032
+ - Multi-step workflows with precise instructions → deepseek-coder:33b
2033
+ - Advanced code generation or algorithms → qwen2.5-coder:32b
2034
+ - Error fixing, debugging → qwen2.5-coder:32b or deepseek-coder:33b
2035
+
2036
+ OUTPUT FORMAT:
2037
+ Output ONLY the model name, nothing else. Example outputs:
2038
+ qwen2.5-coder:32b
2039
+ deepseek-coder:33b
2040
+ codellama:34b
2041
+ qwen2.5-coder:14b
2042
+ qwen2.5-coder:7b
2043
+
2044
+ NOW: Analyze the requirement and output the best model name.`;
2045
+
2046
+ try {
2047
+ // Use fast model for selection (7b)
2048
+ const selectorModel = 'qwen2.5-coder:7b';
2049
+ const selectionResult = await aiderManager.sendText(
2050
+ modelSelectionPrompt,
2051
+ repoPath,
2052
+ provider,
2053
+ selectorModel,
2054
+ null,
2055
+ [], // No files needed
2056
+ null,
2057
+ null,
2058
+ 30000 // 30 second timeout (increased from 10s to handle initial model loading)
2059
+ );
2060
+
2061
+ if (selectionResult.success && selectionResult.output) {
2062
+ // Parse the model name from output
2063
+ const lines = selectionResult.output.split('\n');
2064
+ for (const line of lines) {
2065
+ const trimmed = line.trim();
2066
+ // Look for model names
2067
+ if (trimmed.match(/^(qwen2\.5-coder:(32b|14b|7b)|deepseek-coder:(33b|v2)|codellama:34b)$/)) {
2068
+ selectedModel = trimmed;
2069
+ console.log(chalk.green(` āœ“ Selected model: ${selectedModel}`));
2070
+ break;
2071
+ }
2072
+ }
2073
+ }
2074
+
2075
+ if (selectedModel === modelName) {
2076
+ console.log(chalk.yellow(` ⚠ Could not parse model selection, using default: ${modelName}`));
2077
+ }
2078
+ } catch (error) {
2079
+ console.log(chalk.yellow(` ⚠ Model selection failed, using default: ${modelName}`));
2080
+ }
2081
+ } else {
2082
+ // For cloud providers, use configured model directly
2083
+ console.log(chalk.cyan(`\n[${getTimestamp()}] 🧠 Using configured model: ${modelName}`));
2084
+ }
2085
+
2086
+ // Use the selected model for the rest of the workflow
2087
+ modelName = selectedModel;
2088
+
2089
+ // VIBE CODING: Extract keywords and find relevant files BEFORE calling Aider
2090
+ // This way Aider has the right files in context from the start
2091
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ” Searching codebase for relevant files...`));
2092
+
2093
+ const { execSync } = require('child_process');
2094
+
2095
+ // Extract keywords from requirement: special patterns, quoted strings, function names
2096
+ const extractKeywords = (text) => {
2097
+ const keywords = [];
2098
+
2099
+ // HIGHEST PRIORITY: Extract special patterns like "d/y/n", "r/y/n" (contains slashes/special chars)
2100
+ const specialPatterns = text.match(/\b[a-z]\/[a-z](?:\/[a-z])?/gi);
2101
+ if (specialPatterns) {
2102
+ specialPatterns.forEach(match => {
2103
+ keywords.push(match);
2104
+ // Also search for the individual letters
2105
+ match.split('/').forEach(letter => keywords.push(`'${letter}'`));
2106
+ });
2107
+ }
2108
+
2109
+ // GENERIC CONTEXTUAL INFERENCE: Extract domain concepts from requirement
2110
+ const lowerText = text.toLowerCase();
2111
+
2112
+ // Extract action verbs (what to do)
2113
+ const actionVerbs = ['add', 'display', 'show', 'remove', 'delete', 'update', 'modify', 'change', 'move', 'create'];
2114
+ actionVerbs.forEach(verb => {
2115
+ if (lowerText.includes(verb)) {
2116
+ keywords.push(verb);
2117
+ }
2118
+ });
2119
+
2120
+ // Extract location context (where to make changes)
2121
+ const locationPhrases = [
2122
+ { pattern: /right below\s+"([^"]+)"/gi, type: 'after-element' },
2123
+ { pattern: /above\s+"([^"]+)"/gi, type: 'before-element' },
2124
+ { pattern: /in\s+the\s+([a-z]+\s+(?:screen|menu|panel|dialog|modal|section))/gi, type: 'in-component' }
2125
+ ];
2126
+
2127
+ locationPhrases.forEach(({ pattern, type }) => {
2128
+ const matches = text.matchAll(pattern);
2129
+ for (const match of matches) {
2130
+ // Extract the reference element/component
2131
+ const reference = match[1];
2132
+ // Extract key nouns from the reference (skip numbers and percentages)
2133
+ const cleanRef = reference.replace(/\d+\s*\(.*?\)/g, '').replace(/\d+%/g, '').trim();
2134
+ const nouns = cleanRef.split(/\s+/).filter(word =>
2135
+ word.length > 3 &&
2136
+ !['the', 'and', 'or', 'with', 'from', 'to'].includes(word.toLowerCase())
2137
+ );
2138
+ nouns.forEach(noun => keywords.push(noun));
2139
+ }
2140
+ });
2141
+
2142
+ // Extract key phrases from quoted text (e.g., "Next TODO Requirement" from Add "Next TODO Requirement: ...")
2143
+ const keyPhraseMatch = text.match(/(?:Add|Display|Show|Create)\s+"([^"]+)"/i);
2144
+ if (keyPhraseMatch) {
2145
+ const phrase = keyPhraseMatch[1];
2146
+ // Extract just the key term (before colons, ellipsis, etc)
2147
+ const cleanPhrase = phrase.replace(/[:.!?]+.*$/, '').replace(/\.\.\.$/, '').trim();
2148
+ if (cleanPhrase.length > 2 && cleanPhrase.length < 30) {
2149
+ // Split into words and add significant ones
2150
+ cleanPhrase.split(/\s+/).forEach(word => {
2151
+ if (word.length > 3 && !/^\d+$/.test(word)) {
2152
+ keywords.push(word);
2153
+ }
2154
+ });
2155
+ }
2156
+ }
2157
+
2158
+ // HIGH PRIORITY: Extract quoted strings, but clean UI output text
2159
+ const quotedMatches = text.match(/"([^"]+)"|'([^']+)'/g);
2160
+ if (quotedMatches) {
2161
+ quotedMatches.forEach(match => {
2162
+ const cleaned = match.replace(/['"]/g, '');
2163
+ // Skip UI output text (contains numbers, percentages, or is very long)
2164
+ if (cleaned.length > 2 && cleaned.length < 50 && !cleaned.match(/\d+\s*\(/) && !cleaned.match(/\d+%/)) {
2165
+ keywords.push(cleaned);
2166
+ } else if (cleaned.length >= 50) {
2167
+ // Extract just the key label from long UI text (e.g., "f) Requirements:" from "f) Requirements: 65 (47%)...")
2168
+ const labelMatch = cleaned.match(/^([a-z]\)\s*)?([A-Za-z\s]+):/);
2169
+ if (labelMatch) {
2170
+ keywords.push(labelMatch[2].trim());
2171
+ }
2172
+ }
2173
+ });
2174
+ }
2175
+
2176
+ // Extract common prompt phrases (unquoted)
2177
+ const promptPhrases = ['are you sure', 'confirm', 'Remove', 'Delete'];
2178
+ promptPhrases.forEach(phrase => {
2179
+ if (text.toLowerCase().includes(phrase.toLowerCase())) {
2180
+ keywords.push(phrase);
2181
+ }
2182
+ });
2183
+
2184
+ // MEDIUM PRIORITY: Extract action-related function names (camelCase)
2185
+ const actionWords = text.match(/\b(confirm|delete|remove|show|prompt|ask|verify|check|validate|handle)[A-Z][a-zA-Z0-9_]*/gi);
2186
+ if (actionWords) {
2187
+ actionWords.forEach(match => keywords.push(match));
2188
+ }
2189
+
2190
+ // Extract potential function suffixes
2191
+ const functionMatches = text.match(/\b[a-z][a-zA-Z0-9_]+(?:Action|Handler|Function|Manager|Component|Modal|Prompt|Dialog)\b/g);
2192
+ if (functionMatches) {
2193
+ functionMatches.forEach(match => {
2194
+ if (match.length > 5 && !['should', 'would', 'could', 'will', 'that', 'this', 'with', 'from'].includes(match)) {
2195
+ keywords.push(match);
2196
+ }
2197
+ });
2198
+ }
2199
+
2200
+ // LOW PRIORITY: UI components (capitalized words)
2201
+ const capitalizedMatches = text.match(/\b[A-Z][a-z]+(?:[A-Z][a-z]+)*\b/g);
2202
+ if (capitalizedMatches) {
2203
+ capitalizedMatches.forEach(match => {
2204
+ if (match.length > 4 && !['When', 'Then', 'After', 'Before', 'With', 'From', 'Pressing'].includes(match)) {
2205
+ keywords.push(match);
2206
+ }
2207
+ });
2208
+ }
2209
+
2210
+ return [...new Set(keywords)]; // Remove duplicates
2211
+ };
2212
+
2213
+ const keywords = extractKeywords(currentTitle);
2214
+ console.log(chalk.gray(` Keywords: ${keywords.slice(0, 5).join(', ')}${keywords.length > 5 ? ` (+${keywords.length - 5} more)` : ''}`));
2215
+
2216
+ // Use simple keyword grep (Aider search is too slow and unreliable)
2217
+ const fs = require('fs-extra');
2218
+ let filesToAdd = [];
2219
+
2220
+ // Try keyword search directly (fast and reliable)
2221
+ try {
2222
+
2223
+ const searchedFiles = new Set();
2224
+ for (const keyword of keywords.slice(0, 5)) { // Limit to first 5 keywords
2225
+ try {
2226
+ // Search from repo root, not assuming any specific directory structure
2227
+ const grepCmd = `grep -ril "${keyword}" "${repoPath}" --exclude-dir={node_modules,.git,dist,build,.next,coverage,vendor,target} --include=\\*.{js,jsx,ts,tsx,cjs,mjs,vue,py,rb,go,rs} 2>/dev/null | head -5`;
2228
+ const result = execSync(grepCmd, { encoding: 'utf8', timeout: 3000 }).trim();
2229
+
2230
+ if (result) {
2231
+ const foundFiles = result.split('\n').filter(f => f.trim());
2232
+ foundFiles.forEach(file => {
2233
+ const lowerFile = file.toLowerCase();
2234
+ const isTestFile =
2235
+ lowerFile.includes('.spec.') ||
2236
+ lowerFile.includes('.test.') ||
2237
+ lowerFile.includes('/test/') ||
2238
+ lowerFile.includes('/tests/') ||
2239
+ lowerFile.includes('__tests__') ||
2240
+ lowerFile.match(/test[^/]*\.js$/);
2241
+ const isTempFile =
2242
+ lowerFile.includes('temp_') ||
2243
+ lowerFile.includes('/temp/') ||
2244
+ lowerFile.includes('.tmp.');
2245
+ const isBuildArtifact =
2246
+ lowerFile.includes('/dist/') ||
2247
+ lowerFile.includes('/build/') ||
2248
+ lowerFile.includes('/node_modules/') ||
2249
+ lowerFile.includes('/.next/') ||
2250
+ lowerFile.includes('/vendor/') ||
2251
+ lowerFile.includes('/target/');
2252
+
2253
+ // Don't assume /src/ structure - just exclude test/temp/build files
2254
+ // Also exclude this file itself (auto.js contains example keywords)
2255
+ const isThisFile = file.includes('/commands/auto.js');
2256
+ if (!isTestFile && !isTempFile && !isBuildArtifact && !isThisFile && !searchedFiles.has(file)) {
2257
+ searchedFiles.add(file);
2258
+ filesToAdd.push(file);
2259
+ }
2260
+ });
2261
+ }
2262
+ } catch (error) {
2263
+ // Grep failed - continue
2264
+ }
2265
+ }
2266
+
2267
+ // Sort files: prioritize utils/ over commands/, then by path length (shorter = more specific)
2268
+ filesToAdd.sort((a, b) => {
2269
+ const aIsUtils = a.includes('/utils/');
2270
+ const bIsUtils = b.includes('/utils/');
2271
+ const aIsCommands = a.includes('/commands/');
2272
+ const bIsCommands = b.includes('/commands/');
2273
+
2274
+ // utils/ files first
2275
+ if (aIsUtils && !bIsUtils) return -1;
2276
+ if (!aIsUtils && bIsUtils) return 1;
2277
+ // commands/ files last
2278
+ if (aIsCommands && !bIsCommands) return 1;
2279
+ if (!aIsCommands && bIsCommands) return -1;
2280
+ // Otherwise sort by path length (shorter = more specific)
2281
+ return a.length - b.length;
2282
+ });
2283
+
2284
+ // Limit to top 3
2285
+ filesToAdd = filesToAdd.slice(0, 3);
2286
+ } catch (error) {
2287
+ console.log(chalk.yellow(` ⚠ Search failed: ${error.message}, using keyword fallback...`));
2288
+ // Fallback to keywords (code already above)
2289
+ }
2290
+
2291
+ // CRITICAL: Filter out files that are too large (prevent context overflow)
2292
+ // For Groq: 12k token limit means we can only handle small files
2293
+ const MAX_FILE_LINES = provider === 'groq' ? 300 : 500; // Stricter limit for Groq
2294
+ const filteredFiles = [];
2295
+ const skippedFiles = [];
2296
+ for (const file of filesToAdd) {
2297
+ try {
2298
+ const stats = fs.statSync(file);
2299
+ const content = fs.readFileSync(file, 'utf8');
2300
+ const lineCount = content.split('\n').length;
2301
+
2302
+ if (lineCount <= MAX_FILE_LINES) {
2303
+ filteredFiles.push(file);
2304
+ } else {
2305
+ const relativePath = path.relative(repoPath, file);
2306
+ console.log(chalk.yellow(` ⚠ Skipping ${relativePath} (${lineCount} lines, too large)`));
2307
+ skippedFiles.push({ file, lineCount });
2308
+ }
2309
+ } catch (error) {
2310
+ // Skip files that can't be read
2311
+ }
2312
+ }
2313
+
2314
+ // Save filtered list for Aider prompt (ONLY small files)
2315
+ const filesToTellAiderToAdd = [...filteredFiles];
2316
+
2317
+ // Don't pre-add files (causes context overflow) - we'll tell Aider to /add them
2318
+ filesToAdd = [];
2319
+ console.log(chalk.yellow(` ⚠ Skipping file pre-load to avoid context overflow`))
2320
+ if (filteredFiles.length > 0) {
2321
+ console.log(chalk.green(` āœ“ Grep found ${filteredFiles.length} file(s) - will tell Aider to add them:`));
2322
+ filteredFiles.forEach(f => console.log(chalk.gray(` - ${path.relative(repoPath, f)}`)));
2323
+ }
2324
+
2325
+ // For large files, we'll tell Aider to use /search instead
2326
+ const hasLargeFiles = skippedFiles.length > 0;
2327
+
2328
+ // Build simplified prompt for Aider - files are already in context
2329
+ // Status updates will be handled programmatically
2330
+ const promptText = filesToAdd.length > 0
2331
+ ? `You are a code editor. The relevant files have been added to your context.
2332
+
2333
+ "${currentTitle}"
2334
+
2335
+ STAGE WORKFLOW (output these markers as you progress):
2336
+ 1. PREPARE → ACT: Output "STAGE: PREPARE -> ACT" when you start making changes
2337
+ 2. ACT → CLEAN UP: Output "STAGE: ACT -> CLEAN UP" after implementing
2338
+ 3. CLEAN UP → VERIFY: Output "STAGE: CLEAN UP -> VERIFY" after cleanup
2339
+ 4. VERIFY → DONE: Output "STAGE: VERIFY -> DONE" when complete
2340
+
2341
+ INSTRUCTIONS:
2342
+ 1. Review the files in your context
2343
+ 2. Output: STAGE: PREPARE -> ACT
2344
+ 3. Make ONLY the specific changes needed (1-10 lines total)
2345
+ 4. Use search/replace blocks to show exact changes
2346
+ 5. Output: STAGE: ACT -> CLEAN UP
2347
+ 6. Clean up: lint, remove duplicates, apply DRY principles
2348
+ 7. Output: STAGE: CLEAN UP -> VERIFY
2349
+ 8. Verify the changes work correctly
2350
+ 9. Output: STAGE: VERIFY -> DONE
2351
+ 10. Do NOT edit ${reqFilename} (requirements file)
2352
+
2353
+ EXAMPLE OUTPUT FORMAT:
2354
+ \`\`\`
2355
+ STAGE: PREPARE -> ACT
2356
+
2357
+ path/to/file.js
2358
+ <<<<<<< SEARCH
2359
+ old code here
2360
+ =======
2361
+ new code here
2362
+ >>>>>>> REPLACE
2363
+
2364
+ STAGE: ACT -> CLEAN UP
2365
+
2366
+ STAGE: CLEAN UP -> VERIFY
2367
+
2368
+ STAGE: VERIFY -> DONE
2369
+ \`\`\`
2370
+
2371
+ CRITICAL:
2372
+ - The files are already in your context - DO NOT use /search or /add
2373
+ - Make minimal, surgical changes to 1-10 lines
2374
+ - You MUST output all 4 stage transitions
2375
+ - The final "STAGE: VERIFY -> DONE" signals completion
2376
+
2377
+ NOW: Make the minimal code changes needed.`
2378
+ : `You are a code editor. Follow these steps to implement this requirement:
2379
+
2380
+ "${currentTitle}"
2381
+
2382
+ ${filesToTellAiderToAdd.length > 0 ? `CRITICAL - SEARCH & ADD FILES:
2383
+ ${hasLargeFiles ? `Some files are too large to add directly. Use /search first:\n${skippedFiles.map(({ file, lineCount }) => `- ${path.relative(repoPath, file)} (${lineCount} lines - use /search to find specific functions)`).join('\n')}\n\nThen add small files:` : 'Add these files:'}
2384
+ ${filesToTellAiderToAdd.map(f => `- /add ${path.relative(repoPath, f)}`).join('\n')}
2385
+
2386
+ Use /search to find the exact code in large files, then make targeted edits.` : `CRITICAL - SEARCH FIRST:
2387
+ Use /search to find relevant files, then /add them before making changes.
2388
+ Keywords: ${keywords.slice(0, 5).join(', ')}`}
2389
+
2390
+ STAGE WORKFLOW (output these markers as you progress):
2391
+ 1. PREPARE → ACT: Output "STAGE: PREPARE -> ACT" when you find files and start changes
2392
+ 2. ACT → CLEAN UP: Output "STAGE: ACT -> CLEAN UP" after implementing
2393
+ 3. CLEAN UP → VERIFY: Output "STAGE: CLEAN UP -> VERIFY" after cleanup
2394
+ 4. VERIFY → DONE: Output "STAGE: VERIFY -> DONE" when complete
2395
+
2396
+ INSTRUCTIONS:
2397
+ 1. Use /search to find relevant code
2398
+ 2. Use /add [filepath] to add the files
2399
+ 3. Output: STAGE: PREPARE -> ACT
2400
+ 4. Make ONLY the specific changes needed (1-10 lines total)
2401
+ 5. Use search/replace blocks
2402
+ 6. Output: STAGE: ACT -> CLEAN UP
2403
+ 7. Clean up: lint, remove duplicates, apply DRY
2404
+ 8. Output: STAGE: CLEAN UP -> VERIFY
2405
+ 9. Verify changes work
2406
+ 10. Output: STAGE: VERIFY -> DONE
2407
+ 11. Do NOT edit ${reqFilename}
2408
+
2409
+ CRITICAL:
2410
+ - Use /search and /add first to find files
2411
+ - Make minimal changes (1-10 lines)
2412
+ - Output all 4 stage transitions
2413
+
2414
+ NOW: Search for and make the minimal code changes needed.`;
2415
+
2416
+ // ITERATIVE PROMPTING: Send multiple prompts to guide Aider through the task
2417
+ // This prevents file destruction and ensures proper completion
2418
+ const maxIterations = 5;
2419
+ const maxRetries = 2; // Retry up to 2 times on timeout/loop
2420
+ let iteration = 1;
2421
+ let allAiderOutput = '';
2422
+ let implementationComplete = false;
2423
+
2424
+ for (iteration = 1; iteration <= maxIterations; iteration++) {
2425
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ“Ø Aider iteration ${iteration}/${maxIterations}...\n`));
2426
+
2427
+ let currentPrompt = promptText;
2428
+ if (iteration > 1) {
2429
+ // Escalating instructions for subsequent iterations
2430
+ const escalation = [
2431
+ '', // iteration 1 (use original prompt)
2432
+ '\n\nREMINDER: Make MINIMAL changes. Follow the stage workflow and output ALL stage markers: PREPARE->ACT, ACT->CLEAN UP, CLEAN UP->VERIFY, VERIFY->DONE.',
2433
+ '\n\nWARNING: You are NOT making progress. Use /add [file] and /search [function] commands. Make ONLY 1-5 line changes. Output ALL stage transitions ending with "STAGE: VERIFY -> DONE".',
2434
+ '\n\nCRITICAL: You MUST:\n1. Use /add [file] and /search [function] commands\n2. Make 1-2 line changes\n3. Output ALL 4 stage transitions: PREPARE->ACT, ACT->CLEAN UP, CLEAN UP->VERIFY, VERIFY->DONE\n\nDO NOT rewrite entire functions.',
2435
+ '\n\nFINAL WARNING: Make surgical changes to 1-2 specific lines. You MUST output all stage transitions ending with "STAGE: VERIFY -> DONE" or this will be considered a failure.'
2436
+ ][iteration - 1];
2437
+ currentPrompt = promptText + escalation;
2438
+ }
2439
+
2440
+ // Retry loop for timeout/loop detection
2441
+ let aiderResult;
2442
+ let retryCount = 0;
2443
+
2444
+ while (retryCount <= maxRetries) {
2445
+ // Shorter timeout for later iterations (they should be faster)
2446
+ const timeoutMs = iteration === 1 ? 300000 : 180000; // 5min for first, 3min for others
2447
+
2448
+ // Display the prompt being sent to Aider
2449
+ console.log(chalk.bold.cyan('\nšŸ“¤ Prompt sent to Aider:'));
2450
+ console.log(chalk.gray('─'.repeat(80)));
2451
+ console.log(chalk.white(currentPrompt));
2452
+ console.log(chalk.gray('─'.repeat(80)));
2453
+ console.log();
2454
+
2455
+ // Run Aider with the current prompt and timeout
2456
+ aiderResult = await aiderManager.sendText(
2457
+ currentPrompt,
2458
+ repoPath,
2459
+ provider,
2460
+ modelName,
2461
+ null,
2462
+ filesToAdd, // Add files to Aider's context
2463
+ (output) => {
2464
+ // Show Aider output (already filtered by sendText)
2465
+ process.stdout.write(output);
2466
+ allAiderOutput += output;
2467
+ },
2468
+ (error) => {
2469
+ // Show errors
2470
+ console.error(chalk.red(error));
2471
+ },
2472
+ timeoutMs
2473
+ );
2474
+
2475
+ // Check if we need to retry
2476
+ if (aiderResult.timeout || aiderResult.loopDetected) {
2477
+ retryCount++;
2478
+ if (retryCount <= maxRetries) {
2479
+ const reason = aiderResult.timeout ? 'timeout' : 'loop detection';
2480
+ console.log(chalk.yellow(`\n[${getTimestamp()}] āš ļø Aider hit ${reason}. Retry ${retryCount}/${maxRetries}...\n`));
2481
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
2482
+ continue;
2483
+ } else {
2484
+ console.log(chalk.red(`\n[${getTimestamp()}] āœ— Max retries (${maxRetries}) reached after ${aiderResult.timeout ? 'timeout' : 'loop detection'}\n`));
2485
+ }
2486
+ }
2487
+
2488
+ // Success or non-retryable failure - break retry loop
2489
+ break;
2490
+ }
2491
+
2492
+ // Handle rate limit - switch to next available provider
2493
+ if (aiderResult.rateLimitDetected) {
2494
+ console.log(chalk.yellow(`\n[${getTimestamp()}] šŸ”„ Rate limit detected, checking for alternative providers...\n`));
2495
+
2496
+ // Get all available providers
2497
+ const availableProviders = getAvailableProviders(savedConfig);
2498
+
2499
+ // Use ProviderManager to find an available provider (not rate limited)
2500
+ const ProviderManager = require('@vibecodingmachine/core/src/ide-integration/provider-manager.cjs');
2501
+ const providerManager = new ProviderManager();
2502
+ const nextProvider = providerManager.getAvailableProvider(availableProviders);
2503
+
2504
+ if (nextProvider && (nextProvider.provider !== provider || nextProvider.model !== modelName)) {
2505
+ console.log(chalk.green(`āœ“ Switching to ${nextProvider.provider}/${nextProvider.model}`));
2506
+
2507
+ // Update provider and model for next iteration
2508
+ provider = nextProvider.provider;
2509
+ modelName = nextProvider.model;
2510
+
2511
+ // Set API key in environment if available
2512
+ if (nextProvider.apiKey) {
2513
+ if (nextProvider.provider === 'groq') {
2514
+ process.env.GROQ_API_KEY = nextProvider.apiKey;
2515
+ } else if (nextProvider.provider === 'anthropic') {
2516
+ process.env.ANTHROPIC_API_KEY = nextProvider.apiKey;
2517
+ } else if (nextProvider.provider === 'openai') {
2518
+ process.env.OPENAI_API_KEY = nextProvider.apiKey;
2519
+ }
2520
+ }
2521
+
2522
+ // Retry with new provider
2523
+ retryCount++;
2524
+ if (retryCount <= maxRetries) {
2525
+ console.log(chalk.yellow(`Retrying with new provider... (${retryCount}/${maxRetries})\n`));
2526
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
2527
+ continue;
2528
+ } else {
2529
+ console.log(chalk.red(`\nāœ— Max retries (${maxRetries}) reached\n`));
2530
+ exitReason = 'error';
2531
+ break;
2532
+ }
2533
+ } else {
2534
+ console.log(chalk.red(`āœ— No alternative providers available. All providers are rate limited.`));
2535
+
2536
+ // Show status of all providers
2537
+ const statuses = providerManager.getProviderStatus(availableProviders);
2538
+ console.log(chalk.yellow('\nšŸ“Š Provider Status:'));
2539
+ for (const status of statuses) {
2540
+ const icon = status.available ? 'āœ“' : 'āœ—';
2541
+ const resetInfo = status.resetIn ? ` (resets in ${status.resetIn})` : '';
2542
+ console.log(chalk.gray(` ${icon} ${status.provider}/${status.model}${resetInfo}`));
2543
+ }
2544
+ console.log();
2545
+
2546
+ exitReason = 'rate-limit';
2547
+ break;
2548
+ }
2549
+ }
2550
+
2551
+ if (!aiderResult.success) {
2552
+ const isRetryableError = aiderResult.timeout || aiderResult.loopDetected;
2553
+ if (isRetryableError && retryCount > maxRetries) {
2554
+ console.log(chalk.red(`\nāœ— Aider failed after ${retryCount} retries: ${aiderResult.error}`));
2555
+ } else if (!isRetryableError) {
2556
+ console.log(chalk.red(`\nāœ— Aider failed: ${aiderResult.error}`));
2557
+ }
2558
+
2559
+ // For timeout/loop after retries, continue to next iteration instead of stopping
2560
+ if (isRetryableError && iteration < maxIterations) {
2561
+ console.log(chalk.yellow(`āš ļø Will try next iteration with different approach...\n`));
2562
+ continue;
2563
+ }
2564
+
2565
+ exitReason = 'error';
2566
+ break;
2567
+ }
2568
+
2569
+ allAiderOutput += aiderResult.output || '';
2570
+
2571
+ // Check for explicit STAGE markers in output (all 5 stages)
2572
+ const stageTransitions = [
2573
+ { marker: 'STAGE: PREPARE -> ACT', from: 'PREPARE', to: 'ACT', icon: 'šŸ”Ø' },
2574
+ { marker: 'STAGE: ACT -> CLEAN UP', from: 'ACT', to: 'CLEAN UP', icon: '🧹' },
2575
+ { marker: 'STAGE: CLEAN UP -> VERIFY', from: 'CLEAN UP', to: 'VERIFY', icon: 'šŸ”' },
2576
+ { marker: 'STAGE: VERIFY -> DONE', from: 'VERIFY', to: 'DONE', icon: 'āœ…' }
2577
+ ];
2578
+
2579
+ // Track current stage and display progress card when stage changes
2580
+ let currentStage = 'PREPARE';
2581
+ const stageOrder = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
2582
+
2583
+ for (const transition of stageTransitions) {
2584
+ if (allAiderOutput.includes(transition.marker)) {
2585
+ currentStage = transition.to;
2586
+
2587
+ // Display progress card showing stage change
2588
+ const currentStageIndex = stageOrder.indexOf(currentStage);
2589
+ const stageDisplay = stageOrder.map((stage, idx) => {
2590
+ if (idx < currentStageIndex) {
2591
+ return `āœ“ ${stage}`; // Completed stages
2592
+ } else if (idx === currentStageIndex) {
2593
+ return `šŸ”Ø ${stage}`; // Current stage
2594
+ } else {
2595
+ return `ā³ ${stage}`; // Future stages
2596
+ }
2597
+ }).join(' → ');
2598
+
2599
+ console.log(chalk.cyan('\n╭────────────────────────────────────────────────────────────────╮'));
2600
+ console.log(chalk.cyan('│ │'));
2601
+ console.log(chalk.cyan(`│ ${stageDisplay.padEnd(62)} │`));
2602
+ console.log(chalk.cyan('│ │'));
2603
+ const titlePreview = currentTitle.length > 45 ? currentTitle.substring(0, 42) + '...' : currentTitle;
2604
+ console.log(chalk.cyan(`│ šŸŽÆ Working on: ${titlePreview.padEnd(45)} │`));
2605
+ console.log(chalk.cyan('│ │'));
2606
+ console.log(chalk.cyan('╰────────────────────────────────────────────────────────────────╯\n'));
2607
+
2608
+ console.log(chalk.green(`āœ“ Stage transition: ${transition.from} → ${transition.to}\n`));
2609
+ }
2610
+ }
2611
+
2612
+ // Check for completion
2613
+ if (allAiderOutput.includes('STAGE: VERIFY -> DONE')) {
2614
+ implementationComplete = true;
2615
+ break;
2616
+ }
2617
+
2618
+ if (iteration === maxIterations) {
2619
+ console.log(chalk.yellow(`\nāš ļø Maximum iterations (${maxIterations}) reached without completion.\n`));
2620
+ break;
2621
+ }
2622
+
2623
+ console.log(chalk.yellow(`\nāš ļø Iteration ${iteration} incomplete. Sending follow-up prompt...\n`));
2624
+ await new Promise(resolve => setTimeout(resolve, 500));
2625
+ }
2626
+
2627
+ // Only update status through workflow stages if work was actually completed
2628
+ if (implementationComplete) {
2629
+ // Update status through workflow stages programmatically
2630
+ // CLEAN UP phase
2631
+ console.log(chalk.cyan(`\n[${getTimestamp()}] 🧹 Updating status to CLEAN UP...`));
2632
+ await updateRequirementsStatus('CLEAN UP', 'Running linting and cleanup');
2633
+ printStatusCard(currentTitle, 'CLEAN UP'); // Show status card after transition
2634
+ await new Promise(resolve => setTimeout(resolve, 1000));
2635
+
2636
+ // VERIFY phase
2637
+ console.log(chalk.cyan(`\n[${getTimestamp()}] āœ… Updating status to VERIFY...`));
2638
+ await updateRequirementsStatus('VERIFY', 'Verifying implementation');
2639
+ printStatusCard(currentTitle, 'VERIFY'); // Show status card after transition
2640
+ await new Promise(resolve => setTimeout(resolve, 1000));
2641
+
2642
+ // DONE phase
2643
+ console.log(chalk.green(`\n[${getTimestamp()}] šŸŽ‰ Updating status to DONE...\n`));
2644
+ await updateRequirementsStatus('DONE', `Implementation complete (${iteration} iteration${iteration > 1 ? 's' : ''})`);
2645
+ printStatusCard(currentTitle, 'DONE'); // Show status card after transition;
2646
+ } else {
2647
+ console.log(chalk.yellow(`\nāš ļø Aider did not complete implementation.\n`));
2648
+
2649
+ // Check if requirement might be too vague by having Aider search the codebase first
2650
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ” Searching codebase for relevant code...\n`));
2651
+
2652
+ // STEP 1: Have Aider search the codebase for relevant files
2653
+ const searchPrompt = `Search the codebase to understand this requirement:
2654
+
2655
+ "${currentTitle}"
2656
+
2657
+ Use /search commands to find relevant code. Report what you found in 2-3 sentences.
2658
+ Focus on: What files/functions are related? What patterns exist?
2659
+
2660
+ If you cannot find any relevant code, say "NO RELEVANT CODE FOUND".`;
2661
+
2662
+ const searchResult = await aiderManager.sendText(
2663
+ searchPrompt,
2664
+ repoPath,
2665
+ provider,
2666
+ modelName,
2667
+ null,
2668
+ [], // No files - let Aider search
2669
+ (output) => {
2670
+ // Show search progress
2671
+ if (output.includes('Searching') || output.includes('Found')) {
2672
+ process.stdout.write(chalk.gray(output));
2673
+ }
2674
+ },
2675
+ () => { },
2676
+ 90000 // 1.5 minute timeout for search
2677
+ );
2678
+
2679
+ const searchFindings = searchResult.output || 'Search completed but no findings reported.';
2680
+ console.log(chalk.gray(`\nFindings: ${searchFindings}\n`));
2681
+
2682
+ // STEP 2: Check if requirement is vague and generate questions focused on USER INTENT
2683
+ console.log(chalk.cyan(`\n[${getTimestamp()}] šŸ¤” Analyzing if clarification is needed...\n`));
2684
+
2685
+ const vagueCheckPrompt = `Based on these findings from the codebase:
2686
+ "${searchFindings}"
2687
+
2688
+ Analyze if this requirement needs clarification:
2689
+ "${currentTitle}"
2690
+
2691
+ If you found relevant code and understand what to change, respond with: "REQUIREMENT IS CLEAR"
2692
+
2693
+ If the requirement is too vague or you need to know USER INTENT, respond with:
2694
+ "REQUIREMENT NEEDS CLARIFICATION"
2695
+
2696
+ Then list 2-4 questions about USER INTENT ONLY (NOT about file locations - you already found those):
2697
+ 1. [Question about desired behavior - should X apply to all cases or just Y?]
2698
+ 2. [Question about user preference - want option A or option B?]
2699
+ 3. [Question about scope - should this affect Z as well?]
2700
+
2701
+ CRITICAL: Do NOT ask "what file" or "which component" - you already searched for that.
2702
+ ONLY ask about what the USER WANTS, not where the code is.
2703
+
2704
+ Example GOOD questions:
2705
+ - Should this change affect ALL confirmation prompts or just the Remove action?
2706
+ - Do you want lowercase 'r/y/n' or uppercase 'R/Y/N'?
2707
+
2708
+ Example BAD questions (never ask these):
2709
+ - What file contains the confirmation prompt? (You should search for this yourself)
2710
+ - Which component needs to be modified? (You should find this yourself)`;
2711
+
2712
+ const vagueCheckResult = await aiderManager.sendText(
2713
+ vagueCheckPrompt,
2714
+ repoPath,
2715
+ provider,
2716
+ modelName,
2717
+ null,
2718
+ [], // No files needed for vague check
2719
+ () => { }, // Silent mode
2720
+ () => { },
2721
+ 60000 // 1 minute timeout
2722
+ );
2723
+
2724
+ const vagueCheckOutput = vagueCheckResult.output || '';
2725
+ const isVague = vagueCheckOutput.toUpperCase().includes('REQUIREMENT NEEDS CLARIFICATION');
2726
+
2727
+ if (isVague) {
2728
+ // Extract clarifying questions from output
2729
+ const questionMatch = vagueCheckOutput.match(/\d+\.\s*(.+)/g);
2730
+ const questions = questionMatch ? questionMatch.map(q => q.trim()).join('\n') :
2731
+ '1. What is the desired behavior for this feature?\n2. Should this apply to all cases or specific scenarios?';
2732
+
2733
+ console.log(chalk.yellow(`\nā“ Requirement flagged as TOO VAGUE. Moving to "Requirements needing manual feedback" section.\n`));
2734
+ console.log(chalk.gray('AI Findings:'));
2735
+ console.log(chalk.white(searchFindings));
2736
+ console.log(chalk.gray('\nClarifying questions:'));
2737
+ console.log(chalk.white(questions));
2738
+
2739
+ // Move requirement to "Requirements needing manual feedback" section with findings
2740
+ const moveToFeedbackResult = await moveRequirementToFeedback(repoPath, currentTitle, questions, searchFindings);
2741
+
2742
+ if (moveToFeedbackResult.success) {
2743
+ console.log(chalk.green(`\nāœ“ Requirement moved to feedback section. Please provide clarification.\n`));
2744
+ requirementsCompleted++; // Count as "completed" so we move to next requirement
2745
+ } else {
2746
+ console.log(chalk.yellow(`\nāš ļø Could not move requirement: ${moveToFeedbackResult.error}\n`));
2747
+ console.log(chalk.gray(' Requirement remains in TODO for retry.\n'));
2748
+ }
2749
+ } else {
2750
+ console.log(chalk.cyan(`\nāœ“ Requirement appears clear. Will retry on next run.\n`));
2751
+ }
2752
+ }
2753
+
2754
+
2755
+ // Only check completion and move requirement if implementation was actually completed
2756
+ if (implementationComplete) {
2757
+ // Re-read the current status after updates
2758
+ const updatedDetails = await getCurrentRequirementDetails();
2759
+ currentStatus = updatedDetails.status;
2760
+
2761
+ // Check if requirement is DONE and actually complete
2762
+ const doneCheck = await isRequirementDone(repoPath);
2763
+
2764
+ if (doneCheck.isDone) {
2765
+ if (doneCheck.actuallyComplete) {
2766
+ console.log(chalk.green('\nāœ… Requirement marked as DONE and verified complete!'));
2767
+
2768
+ // Move completed requirement to TO VERIFY section (for human verification)
2769
+ const moveResult = await moveCompletedRequirement(repoPath, currentTitle);
2770
+
2771
+ if (!moveResult.success) {
2772
+ console.log(chalk.yellow(`\nāš ļø Error moving requirement: ${moveResult.error}`));
2773
+ console.log(' Auto mode stopping.');
2774
+ exitReason = 'error';
2775
+ break;
2776
+ }
2777
+
2778
+ console.log(chalk.green(`\nāœ“ Moved requirement to TO VERIFY section (awaiting human verification)`));
2779
+
2780
+ // Increment requirements completed counter
2781
+ requirementsCompleted++;
2782
+
2783
+ // Show progress update
2784
+ const currentCounts = await getRequirementCounts();
2785
+ if (initialCounts && currentCounts) {
2786
+ const total = currentCounts.todoCount + currentCounts.toVerifyCount + currentCounts.verifiedCount;
2787
+ const todoPercent = total > 0 ? Math.round((currentCounts.todoCount / total) * 100) : 0;
2788
+ const toVerifyPercent = total > 0 ? Math.round((currentCounts.toVerifyCount / total) * 100) : 0;
2789
+ const verifiedPercent = total > 0 ? Math.round((currentCounts.verifiedCount / total) * 100) : 0;
2790
+
2791
+ const initialTotal = initialCounts.todoCount + initialCounts.toVerifyCount + initialCounts.verifiedCount;
2792
+ const initialTodoPercent = initialTotal > 0 ? Math.round((initialCounts.todoCount / initialTotal) * 100) : 0;
2793
+ const initialVerifiedPercent = initialTotal > 0 ? Math.round((initialCounts.verifiedCount / initialTotal) * 100) : 0;
2794
+
2795
+ console.log(chalk.bold.green('\nšŸ“Š Progress Update:'));
2796
+ console.log(chalk.gray(` Before: ${initialCounts.todoCount} (${initialTodoPercent}%) TODO, ${initialCounts.verifiedCount} (${initialVerifiedPercent}%) VERIFIED`));
2797
+ console.log(chalk.cyan(` Now: ${currentCounts.todoCount} (${todoPercent}%) TODO, ${currentCounts.toVerifyCount} (${toVerifyPercent}%) TO VERIFY, ${currentCounts.verifiedCount} (${verifiedPercent}%) VERIFIED`));
2798
+
2799
+ // Ask if user wants to stop with countdown
2800
+ const readline = require('readline');
2801
+ console.log(chalk.yellow('\nāøļø Continue to next requirement or stop?'));
2802
+ console.log(chalk.gray(' Press any key to stop, or wait 10 seconds to continue...'));
2803
+
2804
+ let countdown = 10;
2805
+ let dots = '.'.repeat(countdown);
2806
+ process.stdout.write(chalk.cyan(` ${dots}`));
2807
+
2808
+ const shouldStop = await new Promise((resolve) => {
2809
+ let stopped = false;
2810
+ const countdownInterval = setInterval(() => {
2811
+ if (stopped) return;
2812
+
2813
+ countdown--;
2814
+ dots = '.'.repeat(countdown);
2815
+ process.stdout.write('\r' + chalk.cyan(` ${dots}${' '.repeat(10 - countdown)}`));
2816
+
2817
+ if (countdown <= 0) {
2818
+ clearInterval(countdownInterval);
2819
+ if (!stopped) {
2820
+ stopped = true;
2821
+ readline.createInterface({ input: process.stdin }).close();
2822
+ process.stdin.setRawMode(false);
2823
+ console.log('\n');
2824
+ resolve(false);
2825
+ }
2826
+ }
2827
+ }, 1000);
2828
+
2829
+ // Listen for any keypress
2830
+ process.stdin.setRawMode(true);
2831
+ process.stdin.resume();
2832
+ process.stdin.once('data', () => {
2833
+ if (stopped) return;
2834
+ stopped = true;
2835
+ clearInterval(countdownInterval);
2836
+ process.stdin.setRawMode(false);
2837
+ console.log(chalk.yellow('\n Stopping auto mode...'));
2838
+ resolve(true);
2839
+ });
2840
+ });
2841
+
2842
+ if (shouldStop) {
2843
+ exitReason = 'user-stop';
2844
+ break;
2845
+ }
2846
+
2847
+ console.log(chalk.green(' Continuing...\n'));
2848
+ } else {
2849
+ await new Promise(resolve => setTimeout(resolve, 1000));
2850
+ }
2851
+ } else {
2852
+ // Status is DONE but work appears incomplete
2853
+ // For test requirements or status-only updates, consider them complete anyway
2854
+ console.log(chalk.yellow(`\nāš ļø Status shows DONE: ${doneCheck.reason || 'No substantial work detected'}`));
2855
+ console.log(chalk.gray(' Marking as complete and moving to next requirement...'));
2856
+
2857
+ // Move completed requirement to TO VERIFY section (for human verification)
2858
+ const moveResult = await moveCompletedRequirement(repoPath, currentTitle);
2859
+
2860
+ if (!moveResult.success) {
2861
+ console.log(chalk.yellow(`\nāš ļø Error moving requirement: ${moveResult.error}`));
2862
+ console.log(' Auto mode stopping.');
2863
+ exitReason = 'error';
2864
+ break;
2865
+ }
2866
+
2867
+ console.log(chalk.green(`\nāœ“ Moved requirement to TO VERIFY section (awaiting human verification)`));
2868
+
2869
+ // Increment requirements completed counter
2870
+ requirementsCompleted++;
2871
+
2872
+ // Show progress update
2873
+ const currentCounts = await getRequirementCounts();
2874
+ if (initialCounts && currentCounts) {
2875
+ const total = currentCounts.todoCount + currentCounts.toVerifyCount + currentCounts.verifiedCount;
2876
+ const todoPercent = total > 0 ? Math.round((currentCounts.todoCount / total) * 100) : 0;
2877
+ const toVerifyPercent = total > 0 ? Math.round((currentCounts.toVerifyCount / total) * 100) : 0;
2878
+ const verifiedPercent = total > 0 ? Math.round((currentCounts.verifiedCount / total) * 100) : 0;
2879
+
2880
+ const initialTotal = initialCounts.todoCount + initialCounts.toVerifyCount + initialCounts.verifiedCount;
2881
+ const initialTodoPercent = initialTotal > 0 ? Math.round((initialCounts.todoCount / initialTotal) * 100) : 0;
2882
+ const initialVerifiedPercent = initialTotal > 0 ? Math.round((initialCounts.verifiedCount / initialTotal) * 100) : 0;
2883
+
2884
+ console.log(chalk.bold.green('\nšŸ“Š Progress Update:'));
2885
+ console.log(chalk.gray(` Before: ${initialCounts.todoCount} (${initialTodoPercent}%) TODO, ${initialCounts.verifiedCount} (${initialVerifiedPercent}%) VERIFIED`));
2886
+ console.log(chalk.cyan(` Now: ${currentCounts.todoCount} (${todoPercent}%) TODO, ${currentCounts.toVerifyCount} (${toVerifyPercent}%) TO VERIFY, ${currentCounts.verifiedCount} (${verifiedPercent}%) VERIFIED`));
2887
+
2888
+ // Ask if user wants to stop with countdown
2889
+ const readline = require('readline');
2890
+ console.log(chalk.yellow('\nāøļø Continue to next requirement or stop?'));
2891
+ console.log(chalk.gray(' Press any key to stop, or wait 10 seconds to continue...'));
2892
+
2893
+ let countdown = 10;
2894
+ let dots = '.'.repeat(countdown);
2895
+ process.stdout.write(chalk.cyan(` ${dots}`));
2896
+
2897
+ const shouldStop = await new Promise((resolve) => {
2898
+ let stopped = false;
2899
+ const countdownInterval = setInterval(() => {
2900
+ if (stopped) return;
2901
+
2902
+ countdown--;
2903
+ dots = '.'.repeat(countdown);
2904
+ process.stdout.write('\r' + chalk.cyan(` ${dots}${' '.repeat(10 - countdown)}`));
2905
+
2906
+ if (countdown <= 0) {
2907
+ clearInterval(countdownInterval);
2908
+ if (!stopped) {
2909
+ stopped = true;
2910
+ readline.createInterface({ input: process.stdin }).close();
2911
+ process.stdin.setRawMode(false);
2912
+ console.log('\n');
2913
+ resolve(false);
2914
+ }
2915
+ }
2916
+ }, 1000);
2917
+
2918
+ // Listen for any keypress
2919
+ process.stdin.setRawMode(true);
2920
+ process.stdin.resume();
2921
+ process.stdin.once('data', () => {
2922
+ if (stopped) return;
2923
+ stopped = true;
2924
+ clearInterval(countdownInterval);
2925
+ process.stdin.setRawMode(false);
2926
+ console.log(chalk.yellow('\n Stopping auto mode...'));
2927
+ resolve(true);
2928
+ });
2929
+ });
2930
+
2931
+ if (shouldStop) {
2932
+ exitReason = 'user-stop';
2933
+ break;
2934
+ }
2935
+
2936
+ console.log(chalk.green(' Continuing...\n'));
2937
+ } else {
2938
+ await new Promise(resolve => setTimeout(resolve, 1000));
2939
+ }
2940
+ }
2941
+ } else {
2942
+ // Status is not DONE yet
2943
+ console.log(chalk.yellow(`\nāš ļø Requirement not yet DONE (current status: ${currentStatus}).`));
2944
+ console.log(chalk.gray(' Continuing to next iteration (Aider may need multiple passes)...'));
2945
+
2946
+ // Continue to next iteration - Aider might need multiple passes to complete
2947
+ // Don't move to next requirement yet - keep working on the same one
2948
+ await new Promise(resolve => setTimeout(resolve, 1000));
2949
+ }
2950
+ } // Close if (implementationComplete)
2951
+ }
2952
+
2953
+ console.log(chalk.green(`\nšŸ Auto mode completed. Processed ${chatCount} chat(s).`));
2954
+ } else {
2955
+ // No current requirement and couldn't move one - auto mode can't start
2956
+ console.log(chalk.yellow('\nāš ļø Auto mode cannot start: No current requirement available.'));
2957
+ }
2958
+
2959
+ } catch (error) {
2960
+ exitReason = 'error';
2961
+ console.log(chalk.red(`\nāœ— Auto mode error: ${error.message}`));
2962
+ if (error.stack) {
2963
+ console.log(chalk.gray('\nStack trace:'));
2964
+ console.log(chalk.gray(error.stack.split('\n').slice(0, 15).join('\n')));
2965
+ }
2966
+ // Re-throw so interactive.js can handle it properly
2967
+ throw error;
2968
+ } finally {
2969
+ // Remove SIGINT handler
2970
+ if (sigintHandler) {
2971
+ process.removeListener('SIGINT', sigintHandler);
2972
+ }
2973
+ // Stop keyboard handler
2974
+ if (typeof keyboardHandler !== 'undefined') {
2975
+ keyboardHandler.stop();
2976
+ }
2977
+ // Clear watchdog
2978
+ if (typeof watchdog !== 'undefined') {
2979
+ clearInterval(watchdog);
2980
+ }
2981
+ // Pause stdin
2982
+ try {
2983
+ process.stdin.pause();
2984
+ } catch (err) {
2985
+ // Ignore errors
2986
+ }
2987
+ await stopAutoMode(exitReason);
2988
+ }
2989
+
2990
+ } else if (config.ide === 'cline') {
2991
+ // Use Cline CLI
2992
+ spinner.text = 'Checking Cline CLI installation...';
2993
+ const clineManager = new ClineCLIManager();
2994
+
2995
+ if (!clineManager.isInstalled()) {
2996
+ spinner.text = 'Installing Cline CLI...';
2997
+ const installResult = await clineManager.install();
2998
+
2999
+ if (!installResult.success) {
3000
+ spinner.fail('Failed to install Cline CLI');
3001
+ console.log(chalk.red('\nāœ— Error:'), installResult.error);
3002
+ console.log(chalk.gray(' You can manually install with:'), chalk.cyan('npm install -g @yaegaki/cline-cli'));
3003
+ process.exit(1);
3004
+ }
3005
+
3006
+ spinner.succeed('Cline CLI installed successfully');
3007
+ }
3008
+
3009
+ // Check if Cline CLI is configured (with valid API key)
3010
+ // This will also auto-sync keys from saved files if needed
3011
+ const isConfigured = clineManager.isConfigured();
3012
+
3013
+ // If configured but user wants to switch, we can check for a force flag
3014
+ // For now, if not configured or if configured provider is unsupported (like gemini),
3015
+ // we'll prompt for setup
3016
+ if (!isConfigured || (isConfigured && options.forceProviderSetup)) {
3017
+ spinner.stop();
3018
+ console.log(chalk.cyan('\nšŸ” Setting up API for Cline CLI'));
3019
+ console.log(chalk.gray(' No valid API key found - setting up now...\n'));
3020
+
3021
+ // Prompt for provider selection
3022
+ const readline = require('readline');
3023
+ const inquirer = require('inquirer');
3024
+
3025
+ // Check if any keys exist or providers are configured
3026
+ const hasOpenRouterKey = !!clineManager.getSavedOpenRouterKey();
3027
+ const hasAnthropicKey = !!clineManager.getSavedAnthropicKey();
3028
+ const hasGeminiKey = !!clineManager.getSavedGeminiKey();
3029
+ const hasOllama = clineManager.isOllamaInstalled();
3030
+
3031
+ // Default to Ollama if nothing is configured (even if not installed - will prompt to install)
3032
+ let provider = 'ollama';
3033
+ let autoOpenBrowser = false;
3034
+
3035
+ if (!hasOpenRouterKey && !hasAnthropicKey && !hasGeminiKey) {
3036
+ // No API keys configured - default to Ollama (100% free, local, no limits!)
3037
+ provider = 'ollama';
3038
+ autoOpenBrowser = false;
3039
+ console.log(chalk.cyan('\nšŸ” Setting up AI Provider for Cline CLI'));
3040
+ console.log(chalk.gray(' No provider found. Defaulting to Ollama (100% FREE LOCAL AI!).\n'));
3041
+ } else {
3042
+ // At least one API key exists, ask user to choose
3043
+ const choices = [
3044
+ { name: 'Ollama (100% FREE - Local AI, no limits, offline!)', value: 'ollama' }
3045
+ ];
3046
+
3047
+ // Always offer Gemini as a cloud option
3048
+ choices.push({ name: 'Google Gemini (FREE - Best cloud option, no credit card!)', value: 'gemini' });
3049
+
3050
+ if (hasOpenRouterKey) {
3051
+ choices.push({ name: 'OpenRouter (Free but limited - 20 req/min)', value: 'openrouter' });
3052
+ }
3053
+ if (hasAnthropicKey) {
3054
+ choices.push({ name: 'Anthropic Claude (Requires credit card)', value: 'anthropic' });
3055
+ }
3056
+
3057
+ const { selectedProvider } = await inquirer.prompt([{
3058
+ type: 'list',
3059
+ name: 'selectedProvider',
3060
+ message: 'Select AI provider:',
3061
+ choices,
3062
+ default: hasOllama ? 'ollama' : (hasGeminiKey ? 'gemini' : (hasOpenRouterKey ? 'openrouter' : 'anthropic'))
3063
+ }]);
3064
+ provider = selectedProvider;
3065
+ autoOpenBrowser = false;
3066
+ }
3067
+
3068
+ let apiKey = null;
3069
+ let providerName = '';
3070
+ let modelId = '';
3071
+ let apiKeyUrl = '';
3072
+ let apiKeyPrompt = '';
3073
+ let savedKeyFile = '';
3074
+
3075
+ // Handle Ollama setup separately (no API key needed)
3076
+ if (provider === 'ollama') {
3077
+ spinner.stop();
3078
+ console.log(chalk.cyan('\nšŸ¤– Setting up Ollama (Local AI)'));
3079
+
3080
+ // Check if Ollama is installed
3081
+ if (!clineManager.isOllamaInstalled()) {
3082
+ console.log(chalk.yellow('\n⚠ Ollama is not installed on your system.'));
3083
+ console.log(chalk.gray(' Ollama is a local AI runtime that runs models on your computer.'));
3084
+ console.log(chalk.gray(' Benefits: 100% free, no rate limits, works offline, private\n'));
3085
+
3086
+ // Use list prompt to ensure Enter selects Yes (more reliable than confirm)
3087
+ const { shouldInstallChoice } = await inquirer.prompt([{
3088
+ type: 'list',
3089
+ name: 'shouldInstallChoice',
3090
+ message: 'Would you like to install Ollama now?',
3091
+ choices: [
3092
+ { name: 'Yes', value: true },
3093
+ { name: 'No', value: false }
3094
+ ],
3095
+ default: 0 // Default to first option (Yes)
3096
+ }]);
3097
+
3098
+ const shouldInstall = shouldInstallChoice;
3099
+
3100
+ if (!shouldInstall) {
3101
+ console.log(chalk.yellow('\n⚠ Cannot continue without Ollama. Please choose a different provider.'));
3102
+ process.exit(1);
3103
+ }
3104
+
3105
+ // Install Ollama
3106
+ spinner.start('Installing Ollama (this may take a few minutes)...');
3107
+ const installResult = await clineManager.installOllama();
3108
+
3109
+ if (!installResult.success) {
3110
+ if (installResult.needsManualInstall) {
3111
+ // macOS/Windows - requires manual installation
3112
+ spinner.stop();
3113
+ console.log(chalk.yellow('\n⚠ Ollama requires manual installation on macOS/Windows\n'));
3114
+ console.log(chalk.cyan('Your browser should open to: https://ollama.com/download'));
3115
+ console.log(chalk.gray('\nSteps:'));
3116
+ console.log(chalk.gray(' 1. Download Ollama for your platform'));
3117
+ console.log(chalk.gray(' 2. Install the application'));
3118
+ console.log(chalk.gray(' 3. Run this command again\n'));
3119
+
3120
+ const { continueAnyway } = await inquirer.prompt([{
3121
+ type: 'confirm',
3122
+ name: 'continueAnyway',
3123
+ message: 'Did you install Ollama and want to continue?',
3124
+ default: true
3125
+ }]);
3126
+
3127
+ if (!continueAnyway) {
3128
+ console.log(chalk.yellow('\n⚠ Please install Ollama and try again later.'));
3129
+ console.log(chalk.gray(' Or choose a different provider (Gemini is 100% free and works immediately!)\n'));
3130
+ process.exit(0);
3131
+ }
3132
+
3133
+ // Verify installation
3134
+ if (!clineManager.isOllamaInstalled()) {
3135
+ console.log(chalk.red('\nāœ— Ollama is still not detected. Please ensure it\'s installed correctly.\n'));
3136
+ process.exit(1);
3137
+ }
3138
+ } else {
3139
+ spinner.fail('Failed to install Ollama');
3140
+ console.log(chalk.red('\nāœ— Error:'), installResult.error);
3141
+ console.log(chalk.gray('\nYou can manually install Ollama from: https://ollama.com'));
3142
+ process.exit(1);
3143
+ }
3144
+ } else {
3145
+ spinner.succeed('Ollama installed successfully');
3146
+ }
3147
+ } else {
3148
+ console.log(chalk.green('āœ“ Ollama is already installed'));
3149
+ }
3150
+
3151
+ // Get list of installed models
3152
+ const installedModels = await clineManager.getOllamaModels();
3153
+
3154
+ if (installedModels.length === 0) {
3155
+ console.log(chalk.yellow('\n⚠ No Ollama models installed yet.'));
3156
+ console.log(chalk.gray(' You need to download a coding model for autonomous development.\n'));
3157
+
3158
+ // Recommend coding models with tool support for Continue CLI
3159
+ const recommendedModels = [
3160
+ { name: 'qwen2.5:1.5b (Recommended - Tool support, fast, 1GB)', value: 'qwen2.5:1.5b' },
3161
+ { name: 'llama3.1:8b (Tool support, good balance, 4.7GB)', value: 'llama3.1:8b' },
3162
+ { name: 'mistral:7b (Tool support, efficient, 4.1GB)', value: 'mistral:7b' },
3163
+ { name: 'qwen2.5:7b (Tool support, larger, 4.7GB)', value: 'qwen2.5:7b' }
3164
+ ];
3165
+
3166
+ // Retry loop for model download
3167
+ let modelDownloaded = false;
3168
+ while (!modelDownloaded) {
3169
+ const { selectedModel } = await inquirer.prompt([{
3170
+ type: 'list',
3171
+ name: 'selectedModel',
3172
+ message: 'Select a model to download:',
3173
+ choices: recommendedModels
3174
+ }]);
3175
+
3176
+ modelId = selectedModel;
3177
+
3178
+ // Download the model
3179
+ console.log(chalk.cyan(`\nšŸ“„ Downloading ${modelId}...`));
3180
+ console.log(chalk.gray(' This may take several minutes depending on your connection.\n'));
3181
+
3182
+ spinner.start(`Pulling ${modelId}...`);
3183
+
3184
+ const pullResult = await clineManager.pullOllamaModel(modelId, (progressInfo) => {
3185
+ if (typeof progressInfo === 'object' && progressInfo.percentage !== undefined) {
3186
+ // Enhanced progress with size info and progress bar
3187
+ const { percentage, downloaded, total, progressBar } = progressInfo;
3188
+ if (total !== 'Unknown') {
3189
+ spinner.text = `Pulling ${modelId}... ${downloaded} / ${total} (${percentage}%) ${progressBar}`;
3190
+ } else {
3191
+ spinner.text = `Pulling ${modelId}... ${percentage}% ${progressBar || ''}`;
3192
+ }
3193
+ } else {
3194
+ // Fallback for simple percentage
3195
+ const percent = typeof progressInfo === 'number' ? progressInfo : (progressInfo || '0');
3196
+ spinner.text = `Pulling ${modelId}... ${percent}%`;
3197
+ }
3198
+ });
3199
+
3200
+ if (!pullResult.success) {
3201
+ spinner.fail(`Failed to download ${modelId}`);
3202
+ console.log(chalk.red('\nāœ— Error:'), pullResult.error);
3203
+
3204
+ if (pullResult.resolution) {
3205
+ console.log(chalk.yellow('\nšŸ’” How to fix:'));
3206
+ console.log(chalk.gray(pullResult.resolution));
3207
+ console.log();
3208
+ } else {
3209
+ console.log(chalk.gray('\n Please check your internet connection and try again.'));
3210
+ console.log(chalk.gray(' If the problem persists, you can manually download the model:'));
3211
+ console.log(chalk.cyan(` ollama pull ${modelId}`));
3212
+ console.log();
3213
+ }
3214
+
3215
+ // Ask if they want to try again or choose a different provider
3216
+ const { action } = await inquirer.prompt([{
3217
+ type: 'list',
3218
+ name: 'action',
3219
+ message: 'What would you like to do?',
3220
+ choices: [
3221
+ { name: 'Try downloading again', value: 'retry' },
3222
+ { name: 'Choose a different AI provider (Gemini/OpenRouter/Anthropic)', value: 'switch' },
3223
+ { name: 'Exit and fix manually', value: 'exit' }
3224
+ ]
3225
+ }]);
3226
+
3227
+ if (action === 'retry') {
3228
+ // Continue loop to retry
3229
+ continue;
3230
+ } else if (action === 'switch') {
3231
+ console.log(chalk.yellow('\n⚠ Please restart and choose a different provider.'));
3232
+ console.log(chalk.gray(' Available providers: Gemini (free), OpenRouter (free), Anthropic'));
3233
+ process.exit(0);
3234
+ } else {
3235
+ console.log(chalk.gray('\n To download manually, run:'));
3236
+ console.log(chalk.cyan(` ollama pull ${modelId}`));
3237
+ console.log(chalk.gray('\n After downloading, restart this setup to continue.'));
3238
+ process.exit(0);
3239
+ }
3240
+ } else {
3241
+ spinner.succeed(`Downloaded ${modelId}`);
3242
+ modelDownloaded = true;
3243
+ }
3244
+ }
3245
+ } else {
3246
+ // User has models, let them choose
3247
+ console.log(chalk.green('\nāœ“ Found installed models:'));
3248
+ installedModels.forEach(model => {
3249
+ console.log(chalk.gray(` - ${model}`));
3250
+ });
3251
+
3252
+ const modelChoices = installedModels.map(model => ({ name: model, value: model }));
3253
+ modelChoices.push({ name: 'šŸ“„ Download a new model', value: '_download_new' });
3254
+
3255
+ const { selectedModel } = await inquirer.prompt([{
3256
+ type: 'list',
3257
+ name: 'selectedModel',
3258
+ message: 'Select a model to use:',
3259
+ choices: modelChoices,
3260
+ default: installedModels[0]
3261
+ }]);
3262
+
3263
+ if (selectedModel === '_download_new') {
3264
+ const recommendedModels = [
3265
+ { name: 'qwen2.5:1.5b (Recommended - Tool support, fast, 1GB)', value: 'qwen2.5:1.5b' },
3266
+ { name: 'llama3.1:8b (Tool support, good balance, 4.7GB)', value: 'llama3.1:8b' },
3267
+ { name: 'mistral:7b (Tool support, efficient, 4.1GB)', value: 'mistral:7b' },
3268
+ { name: 'qwen2.5:7b (Tool support, larger, 4.7GB)', value: 'qwen2.5:7b' }
3269
+ ];
3270
+
3271
+ // Retry loop for downloading new model
3272
+ let newModelDownloaded = false;
3273
+ while (!newModelDownloaded) {
3274
+ const { newModel } = await inquirer.prompt([{
3275
+ type: 'list',
3276
+ name: 'newModel',
3277
+ message: 'Select a model to download:',
3278
+ choices: recommendedModels
3279
+ }]);
3280
+
3281
+ modelId = newModel;
3282
+
3283
+ console.log(chalk.cyan(`\nšŸ“„ Downloading ${modelId}...`));
3284
+ spinner.start(`Pulling ${modelId}...`);
3285
+
3286
+ const pullResult = await clineManager.pullOllamaModel(modelId, (progressInfo) => {
3287
+ if (typeof progressInfo === 'object' && progressInfo.percentage !== undefined) {
3288
+ // Enhanced progress with size info and progress bar
3289
+ const { percentage, downloaded, total, progressBar } = progressInfo;
3290
+ if (total !== 'Unknown') {
3291
+ spinner.text = `Pulling ${modelId}... ${downloaded} / ${total} (${percentage}%) ${progressBar}`;
3292
+ } else {
3293
+ spinner.text = `Pulling ${modelId}... ${percentage}% ${progressBar || ''}`;
3294
+ }
3295
+ } else {
3296
+ // Fallback for simple percentage
3297
+ const percent = typeof progressInfo === 'number' ? progressInfo : (progressInfo || '0');
3298
+ spinner.text = `Pulling ${modelId}... ${percent}%`;
3299
+ }
3300
+ });
3301
+
3302
+ if (!pullResult.success) {
3303
+ spinner.fail(`Failed to download ${modelId}`);
3304
+ console.log(chalk.red('\nāœ— Error:'), pullResult.error);
3305
+
3306
+ if (pullResult.resolution) {
3307
+ console.log(chalk.yellow('\nšŸ’” How to fix:'));
3308
+ console.log(chalk.gray(pullResult.resolution));
3309
+ console.log();
3310
+ } else {
3311
+ console.log(chalk.gray('\n Please check your internet connection and try again.'));
3312
+ console.log(chalk.gray(' If the problem persists, you can manually download the model:'));
3313
+ console.log(chalk.cyan(` ollama pull ${modelId}`));
3314
+ console.log();
3315
+ }
3316
+
3317
+ // Ask if they want to try again or use existing model
3318
+ const { action } = await inquirer.prompt([{
3319
+ type: 'list',
3320
+ name: 'action',
3321
+ message: 'What would you like to do?',
3322
+ choices: [
3323
+ { name: 'Try downloading again', value: 'retry' },
3324
+ { name: 'Use an existing model instead', value: 'use-existing' },
3325
+ { name: 'Exit and fix manually', value: 'exit' }
3326
+ ]
3327
+ }]);
3328
+
3329
+ if (action === 'retry') {
3330
+ // Continue loop to retry
3331
+ continue;
3332
+ } else if (action === 'use-existing') {
3333
+ // Break out and go back to model selection
3334
+ modelId = null;
3335
+ break;
3336
+ } else {
3337
+ console.log(chalk.gray('\n To download manually, run:'));
3338
+ console.log(chalk.cyan(` ollama pull ${modelId}`));
3339
+ console.log(chalk.gray('\n After downloading, restart this setup to continue.'));
3340
+ process.exit(0);
3341
+ }
3342
+ } else {
3343
+ spinner.succeed(`Downloaded ${modelId}`);
3344
+ newModelDownloaded = true;
3345
+ }
3346
+ }
3347
+
3348
+ // If model download was cancelled, go back to model selection
3349
+ if (!newModelDownloaded && modelId === null) {
3350
+ // Re-prompt for model selection
3351
+ const { reselectedModel } = await inquirer.prompt([{
3352
+ type: 'list',
3353
+ name: 'reselectedModel',
3354
+ message: 'Select a model to use:',
3355
+ choices: installedModels.map(model => ({ name: model, value: model })),
3356
+ default: installedModels[0]
3357
+ }]);
3358
+ modelId = reselectedModel;
3359
+ }
3360
+ } else {
3361
+ modelId = selectedModel;
3362
+ }
3363
+ }
3364
+
3365
+ // Configure Cline CLI with Ollama
3366
+ spinner.start('Configuring Cline CLI with Ollama...');
3367
+ const configResult = await clineManager.configureWithOllama(modelId);
3368
+
3369
+ if (!configResult.success) {
3370
+ spinner.fail('Failed to configure Cline CLI');
3371
+ console.log(chalk.red('\nāœ— Error:'), configResult.error);
3372
+ console.log(chalk.yellow('\nšŸ’” How to fix:'));
3373
+ console.log(chalk.gray(' The configuration failed. This might be due to:'));
3374
+ console.log(chalk.gray(' 1. Ollama CLI not being in your PATH'));
3375
+ console.log(chalk.gray(' 2. Ollama service not running'));
3376
+ console.log(chalk.gray('\n Try:'));
3377
+ console.log(chalk.cyan(' 1. Launch Ollama.app from /Applications'));
3378
+ console.log(chalk.cyan(' 2. Or run: ollama serve'));
3379
+ console.log(chalk.gray('\n Then restart this setup.'));
3380
+ process.exit(0);
3381
+ }
3382
+
3383
+ spinner.succeed('Cline CLI configured with Ollama');
3384
+ console.log(chalk.green('\nāœ“ Ollama setup complete!'));
3385
+ console.log(chalk.gray(` Model: ${modelId}`));
3386
+ console.log(chalk.gray(' Endpoint: http://localhost:11434/v1'));
3387
+ console.log(chalk.gray(' 100% local, no API keys, no rate limits!\n'));
3388
+
3389
+ // Skip API key collection since Ollama doesn't need it
3390
+ // Set providerName for display
3391
+ providerName = 'Ollama';
3392
+ apiKey = null; // Explicitly set to null to skip API key prompt
3393
+ } else if (provider === 'gemini') {
3394
+ providerName = 'Google Gemini';
3395
+ modelId = 'gemini-2.0-flash-exp';
3396
+ apiKeyUrl = 'https://aistudio.google.com/apikey';
3397
+ apiKeyPrompt = 'Enter your Google Gemini API Key (starts with "AIza"):';
3398
+ savedKeyFile = 'gemini-api-key.txt';
3399
+ apiKey = clineManager.getSavedGeminiKey();
3400
+ } else if (provider === 'openrouter') {
3401
+ providerName = 'OpenRouter';
3402
+ modelId = 'meta-llama/llama-3.3-70b-instruct:free';
3403
+ apiKeyUrl = 'https://openrouter.ai/keys';
3404
+ apiKeyPrompt = 'Enter your OpenRouter API Key (starts with "sk-or-"):';
3405
+ savedKeyFile = 'openrouter-api-key.txt';
3406
+ apiKey = clineManager.getSavedOpenRouterKey();
3407
+ } else {
3408
+ providerName = 'Anthropic';
3409
+ modelId = 'claude-3-5-sonnet-20241022';
3410
+ apiKeyUrl = 'https://console.anthropic.com/settings/keys';
3411
+ apiKeyPrompt = 'Enter your Anthropic API key:';
3412
+ savedKeyFile = 'anthropic-api-key.txt';
3413
+ apiKey = clineManager.getSavedAnthropicKey();
3414
+ }
3415
+
3416
+ if (!apiKey && provider !== 'ollama') {
3417
+ // Display prominent instructions (Ollama doesn't need API key)
3418
+ console.log('\n╔══════════════════════════════════════════════════════════════════╗');
3419
+ console.log('ā•‘ ā•‘');
3420
+ console.log(`ā•‘ āš ļø SETTING UP ${providerName.toUpperCase().padEnd(25)} āš ļø ā•‘`);
3421
+ console.log('ā•‘ ā•‘');
3422
+
3423
+ if (provider === 'gemini') {
3424
+ console.log('ā•‘ ╔══════════════════════════════════════════════════════════╗ ā•‘');
3425
+ console.log('ā•‘ ā•‘ āœ“ 100% FREE - NO CREDIT CARD EVER! ā•‘ ā•‘');
3426
+ console.log('ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• ā•‘');
3427
+ console.log('ā•‘ ā•‘');
3428
+ console.log('ā•‘ Your browser will open to Google AI Studio. ā•‘');
3429
+ console.log('ā•‘ ā•‘');
3430
+ console.log('ā•‘ ╔══════════════════════════════════════════════════════════╗ ā•‘');
3431
+ console.log('ā•‘ ā•‘ āœ“ BEST RATE LIMITS (250K tokens/min!) ā•‘ ā•‘');
3432
+ console.log('ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• ā•‘');
3433
+ console.log('ā•‘ ā•‘');
3434
+ console.log('ā•‘ 1. Sign in with your Google account ā•‘');
3435
+ console.log('ā•‘ 2. Click "Create API Key" button ā•‘');
3436
+ console.log('ā•‘ 3. Choose an imported project: "Default Gemini Project" ā•‘');
3437
+ console.log('ā•‘ 4. Give it a name (e.g., "Vibe Coding Machine") ā•‘');
3438
+ console.log('ā•‘ 5. Click the Copy icon next to the API key ā•‘');
3439
+ console.log('ā•‘ 6. Paste it below when prompted ā•‘');
3440
+ console.log('ā•‘ ā•‘');
3441
+ console.log('ā•‘ Note: Completely free with excellent rate limits! ā•‘');
3442
+ } else if (provider === 'openrouter') {
3443
+ console.log('ā•‘ ╔═══════════════════════════════════════════════════════════╗ ā•‘');
3444
+ console.log('ā•‘ ā•‘ āœ“ FREE - NO CREDIT CARD REQUIRED! ā•‘ ā•‘');
3445
+ console.log('ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• ā•‘');
3446
+ console.log('ā•‘ ā•‘');
3447
+ console.log('ā•‘ Your browser will open to OpenRouter API Keys page. ā•‘');
3448
+ console.log('ā•‘ ā•‘');
3449
+ console.log('ā•‘ ╔═══════════════════════════════════════════════════════════╗ ā•‘');
3450
+ console.log('ā•‘ ā•‘ āœ“ CREATE API KEY (FREE MODELS AVAILABLE) ā•‘ ā•‘');
3451
+ console.log('ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• ā•‘');
3452
+ console.log('ā•‘ ā•‘');
3453
+ console.log('ā•‘ Step-by-step instructions: ā•‘');
3454
+ console.log('ā•‘ ā•‘');
3455
+ console.log('ā•‘ 1. Sign up/login to OpenRouter (free account, no CC required) ā•‘');
3456
+ console.log('ā•‘ - Visit https://openrouter.ai if you need to sign up ā•‘');
3457
+ console.log('ā•‘ 2. Click "Create API Key" button ā•‘');
3458
+ console.log('ā•‘ 3. Give it a name (e.g., "Vibe Coding Machine") ā•‘');
3459
+ console.log('ā•‘ 4. Leave "Reset limit every..." at "N/A" ā•‘');
3460
+ console.log('ā•‘ 5. Click "Create Key" (or similar button) ā•‘');
3461
+ console.log('ā•‘ 6. Copy the API key that appears (starts with "sk-or-") ā•‘');
3462
+ console.log('ā•‘ 7. Paste it below when prompted ā•‘');
3463
+ console.log('ā•‘ ā•‘');
3464
+ console.log('ā•‘ ⚠ Important notes: ā•‘');
3465
+ console.log('ā•‘ • The key is shown only once - copy it immediately! ā•‘');
3466
+ console.log('ā•‘ • Free keys have usage limits (20 requests/minute) ā•‘');
3467
+ console.log('ā•‘ • You can create unlimited new keys when limits are hit ā•‘');
3468
+ } else {
3469
+ console.log('ā•‘ ╔══════════════════════════════════════════════════════╗ ā•‘');
3470
+ console.log('ā•‘ ā•‘ āœ“ SELECT WORKSPACE & CREATE API KEY ā•‘ ā•‘');
3471
+ console.log('ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• ā•‘');
3472
+ console.log('ā•‘ ā•‘');
3473
+ console.log('ā•‘ 1. Select a workspace (choose "Default" or create new) ā•‘');
3474
+ console.log('ā•‘ 2. Click "+ Create Key" button ā•‘');
3475
+ console.log('ā•‘ 3. Give it a name (e.g., "Vibe Coding Machine") ā•‘');
3476
+ console.log('ā•‘ 4. Copy the API key that appears ā•‘');
3477
+ console.log('ā•‘ 5. Paste it below when prompted ā•‘');
3478
+ console.log('ā•‘ ā•‘');
3479
+ console.log('ā•‘ Note: Anthropic requires a credit card for API access. ā•‘');
3480
+ }
3481
+
3482
+ console.log('ā•‘ ā•‘');
3483
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
3484
+
3485
+ if (autoOpenBrowser) {
3486
+ console.log('\nOpening browser now...\n');
3487
+ } else {
3488
+ console.log('\nOpening browser in 3 seconds...\n');
3489
+ // Give user time to read the instructions
3490
+ await new Promise(resolve => setTimeout(resolve, 3000));
3491
+ }
3492
+
3493
+ // Open browser to API keys page
3494
+ const { execSync } = require('child_process');
3495
+ try {
3496
+ const platform = require('os').platform();
3497
+
3498
+ if (platform === 'darwin') {
3499
+ execSync(`open "${apiKeyUrl}"`);
3500
+ } else if (platform === 'win32') {
3501
+ execSync(`start "${apiKeyUrl}"`);
3502
+ } else {
3503
+ execSync(`xdg-open "${apiKeyUrl}"`);
3504
+ }
3505
+
3506
+ console.log(chalk.green(`āœ“ Browser opened to ${providerName} API Keys page\n`));
3507
+ } catch (error) {
3508
+ console.log(chalk.yellow('⚠ Could not open browser automatically'));
3509
+ console.log(chalk.gray(` Please visit: ${apiKeyUrl}\n`));
3510
+ }
3511
+
3512
+ // Prompt for API key
3513
+ const rl = readline.createInterface({
3514
+ input: process.stdin,
3515
+ output: process.stdout
3516
+ });
3517
+
3518
+ apiKey = await new Promise((resolve) => {
3519
+ rl.question(chalk.cyan(apiKeyPrompt + ' '), (answer) => {
3520
+ rl.close();
3521
+ resolve(answer.trim());
3522
+ });
3523
+ });
3524
+
3525
+ if (!apiKey || apiKey.length === 0) {
3526
+ console.log(chalk.red('\nāœ— Error: Access Token is required'));
3527
+ console.log(chalk.gray(` Get an Access Token at: ${apiKeyUrl}`));
3528
+ console.log(chalk.gray(` The token should start with "hf_"`));
3529
+ process.exit(1);
3530
+ }
3531
+
3532
+ // Validate key format
3533
+ if (provider === 'gemini' && !apiKey.startsWith('AIza')) {
3534
+ console.log(chalk.yellow('\n⚠ Warning: Google Gemini API keys typically start with "AIza"'));
3535
+ console.log(chalk.gray(' Please double-check that you copied the correct key.'));
3536
+ console.log(chalk.gray(' If this is correct, you can continue.\n'));
3537
+ } else if (provider === 'openrouter' && !apiKey.startsWith('sk-or-')) {
3538
+ console.log(chalk.yellow('\n⚠ Warning: OpenRouter API keys typically start with "sk-or-"'));
3539
+ console.log(chalk.gray(' Please double-check that you copied the correct key.'));
3540
+ console.log(chalk.gray(' If this is correct, you can continue.\n'));
3541
+ }
3542
+
3543
+ // Validate API key by making a test API call (skip for Ollama - no API key needed)
3544
+ if (provider !== 'ollama') {
3545
+ spinner.start(`Validating ${providerName} API key...`);
3546
+ const validationResult = await clineManager.validateApiKey(provider, apiKey);
3547
+
3548
+ if (!validationResult.valid) {
3549
+ spinner.fail(`Invalid ${providerName} API key`);
3550
+ console.log(chalk.red(`\nāœ— Error: ${validationResult.error}`));
3551
+ console.log(chalk.yellow('\n⚠ The API key you provided is invalid or expired.'));
3552
+ console.log(chalk.gray(` Please get a new API key from: ${apiKeyUrl}`));
3553
+ if (provider === 'openrouter') {
3554
+ console.log(chalk.gray(' Make sure your key starts with "sk-or-" and you have access to free models.'));
3555
+ }
3556
+ process.exit(1);
3557
+ } else {
3558
+ if (validationResult.warning) {
3559
+ spinner.warn(`Validation incomplete: ${validationResult.warning}`);
3560
+ console.log(chalk.yellow(` ⚠ ${validationResult.warning}`));
3561
+ console.log(chalk.gray(' Continuing anyway - key format looks correct.'));
3562
+ } else {
3563
+ spinner.succeed(`${providerName} API key is valid`);
3564
+ }
3565
+ }
3566
+ }
3567
+
3568
+ // Save API key (skip for Ollama - no API key needed)
3569
+ if (provider !== 'ollama') {
3570
+ let saveResult = false;
3571
+ if (provider === 'gemini') {
3572
+ saveResult = clineManager.saveGeminiKey(apiKey);
3573
+ } else if (provider === 'openrouter') {
3574
+ saveResult = clineManager.saveOpenRouterKey(apiKey);
3575
+ } else {
3576
+ saveResult = clineManager.saveAnthropicKey(apiKey);
3577
+ }
3578
+
3579
+ if (!saveResult) {
3580
+ console.log(chalk.yellow('⚠ Failed to save API key (will use for this session only)'));
3581
+ } else {
3582
+ console.log(chalk.green(`āœ“ API key saved to ~/.vibecodingmachine/${savedKeyFile}`));
3583
+ }
3584
+ }
3585
+ } else if (provider !== 'ollama') {
3586
+ console.log(chalk.green(`āœ“ Using saved API key from ~/.vibecodingmachine/${savedKeyFile}`));
3587
+ }
3588
+
3589
+ // Ollama was already configured above, skip API-based provider setup
3590
+ if (provider !== 'ollama') {
3591
+ // Initialize Cline CLI
3592
+ spinner.start('Initializing Cline CLI...');
3593
+ const initResult = await clineManager.init();
3594
+
3595
+ if (!initResult.success) {
3596
+ spinner.fail('Failed to initialize Cline CLI');
3597
+ console.log(chalk.red('\nāœ— Error:'), initResult.error);
3598
+ process.exit(1);
3599
+ }
3600
+
3601
+ // Configure Cline CLI with selected provider
3602
+ spinner.text = `Configuring Cline CLI with ${providerName} API...`;
3603
+ let configResult;
3604
+
3605
+ if (provider === 'gemini') {
3606
+ configResult = await clineManager.configureWithGemini(apiKey, modelId);
3607
+ } else if (provider === 'openrouter') {
3608
+ configResult = await clineManager.configureWithOpenRouter(apiKey, modelId);
3609
+ } else {
3610
+ configResult = await clineManager.configureWithAnthropic(apiKey);
3611
+ }
3612
+
3613
+ if (!configResult.success) {
3614
+ spinner.fail('Failed to configure Cline CLI');
3615
+ console.log(chalk.red('\nāœ— Error:'), configResult.error);
3616
+ process.exit(1);
3617
+ }
3618
+
3619
+ spinner.succeed('Cline CLI configured successfully');
3620
+ console.log(chalk.green('\nāœ“ Setup complete!'));
3621
+ console.log(chalk.gray(' Provider:'), chalk.cyan(providerName));
3622
+ console.log(chalk.gray(' Model:'), chalk.cyan(modelId));
3623
+ if (provider !== 'ollama' && apiKey) {
3624
+ console.log(chalk.gray(' API Key:'), chalk.cyan(apiKey.substring(0, 20) + '...'));
3625
+ }
3626
+ console.log(chalk.gray(' Cline config:'), chalk.cyan(configResult.configPath));
3627
+
3628
+ // If configureOnly flag is set, return here without starting auto mode
3629
+ if (options.configureOnly) {
3630
+ console.log(chalk.gray('\n Provider configured successfully.\n'));
3631
+ return;
3632
+ }
3633
+
3634
+ console.log(chalk.gray('\n Starting Cline CLI now...\n'));
3635
+ } else {
3636
+ // Ollama was already configured - show summary
3637
+ console.log(chalk.green('\nāœ“ Ready to start!'));
3638
+ console.log(chalk.gray(' Provider:'), chalk.cyan('Ollama (Local)'));
3639
+ console.log(chalk.gray(' Model:'), chalk.cyan(modelId || 'configured'));
3640
+ console.log(chalk.gray(' Endpoint:'), chalk.cyan('http://localhost:11434/v1'));
3641
+
3642
+ // If configureOnly flag is set, return here without starting auto mode
3643
+ if (options.configureOnly) {
3644
+ console.log(chalk.gray('\n Provider configured successfully.\n'));
3645
+ return;
3646
+ }
3647
+
3648
+ console.log(chalk.gray('\n Starting Cline CLI now...\n'));
3649
+ }
3650
+ }
3651
+
3652
+ // Safety check: if configureOnly is set, stop here
3653
+ if (options.configureOnly) {
3654
+ console.log(chalk.gray('\n Provider configured successfully.\n'));
3655
+ return;
3656
+ }
3657
+
3658
+ // Check for invalid key markers before running
3659
+ const fs = require('fs');
3660
+ const path = require('path');
3661
+ const os = require('os');
3662
+ const allnightDir = path.join(os.homedir(), '.vibecodingmachine');
3663
+ const anthropicInvalidMarker = path.join(allnightDir, '.anthropic-key-invalid');
3664
+ const openrouterInvalidMarker = path.join(allnightDir, '.openrouter-key-invalid');
3665
+ const geminiInvalidMarker = path.join(allnightDir, '.gemini-key-invalid');
3666
+
3667
+ if (fs.existsSync(anthropicInvalidMarker) || fs.existsSync(openrouterInvalidMarker) || fs.existsSync(geminiInvalidMarker)) {
3668
+ spinner.fail('API key was marked as invalid');
3669
+ console.log(chalk.red('\nāœ— Error: Previous API key failed authentication'));
3670
+ console.log(chalk.yellow('\n⚠ You need to set up a new API key.'));
3671
+ console.log(chalk.gray(' Please restart auto mode to be prompted for a new API key.'));
3672
+ console.log(chalk.gray(' You can choose Google Gemini (free, no credit card), OpenRouter (free), or Anthropic.'));
3673
+ process.exit(1);
3674
+ }
3675
+
3676
+ // Double-check configuration before running (in case it changed)
3677
+ // This will also trigger auto-sync if key is in saved file but not config
3678
+ const finalConfigCheck = clineManager.isConfigured();
3679
+ if (!finalConfigCheck) {
3680
+ spinner.fail('Cline CLI is not properly configured');
3681
+ console.log(chalk.red('\nāœ— Error: No valid API key found'));
3682
+ console.log(chalk.yellow('\n⚠ Cline CLI config exists but API key is missing or invalid.'));
3683
+ console.log(chalk.gray(' Please stop auto mode and restart it to set up your API key.'));
3684
+ console.log(chalk.gray(' You will be prompted to configure Google Gemini (free), OpenRouter (free), or Anthropic API.'));
3685
+ process.exit(1);
3686
+ }
3687
+
3688
+ // Pre-flight API key validation - test the key before starting Cline CLI
3689
+ // This catches invalid keys immediately instead of waiting for 15-second timeout
3690
+ // Skip for Ollama (local, no API key needed)
3691
+ const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
3692
+ if (fs.existsSync(configPath)) {
3693
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
3694
+ const apiProvider = config.globalState?.apiProvider;
3695
+ const isOllama = apiProvider === 'openai-native' && config.globalState?.openAiBaseUrl === 'http://localhost:11434/v1';
3696
+
3697
+ if (isOllama) {
3698
+ // Ollama - verify it's installed and running
3699
+ spinner.text = 'Checking Ollama installation...';
3700
+ const isInstalled = clineManager.isOllamaInstalled();
3701
+ if (!isInstalled) {
3702
+ spinner.fail('Ollama is not installed');
3703
+ console.log(chalk.red('\nāœ— Error: Ollama is configured but not installed'));
3704
+ console.log(chalk.gray(' Please install Ollama or choose a different provider.'));
3705
+ process.exit(1);
3706
+ }
3707
+
3708
+ spinner.succeed('Ollama is installed');
3709
+ } else {
3710
+ // API-based providers - validate keys
3711
+ spinner.text = 'Validating API key before starting...';
3712
+ let apiKey = null;
3713
+
3714
+ if (apiProvider === 'anthropic') {
3715
+ apiKey = config.globalState?.anthropicApiKey || process.env.ANTHROPIC_API_KEY || clineManager.getSavedAnthropicKey();
3716
+ } else if (apiProvider === 'openrouter') {
3717
+ apiKey = config.globalState?.openRouterApiKey || process.env.OPENROUTER_API_KEY || clineManager.getSavedOpenRouterKey();
3718
+ } else if (apiProvider === 'gemini' || (apiProvider === 'openai-native' && config.globalState?.openAiBaseUrl?.includes('generativelanguage.googleapis.com'))) {
3719
+ apiKey = config.globalState?.geminiApiKey || config.globalState?.openAiApiKey || process.env.GEMINI_API_KEY || clineManager.getSavedGeminiKey();
3720
+ }
3721
+
3722
+ if (apiKey) {
3723
+ const validationResult = await clineManager.validateApiKey(
3724
+ apiProvider === 'openai-native' ? 'gemini' : apiProvider,
3725
+ apiKey
3726
+ );
3727
+
3728
+ if (!validationResult.valid) {
3729
+ spinner.fail('API key validation failed');
3730
+ console.log(chalk.red(`\nāœ— Error: ${validationResult.error}`));
3731
+
3732
+ // Check if it's a rate limit error
3733
+ if (validationResult.rateLimited) {
3734
+ console.log(chalk.yellow('\n⚠ Your OpenRouter free API key has exceeded its usage limit.'));
3735
+ console.log(chalk.cyan('\nšŸ“ Step-by-step instructions to create a new OpenRouter API key:'));
3736
+ console.log(chalk.gray(''));
3737
+ console.log(chalk.white(' 1. Open your browser and visit:'), chalk.cyan('https://openrouter.ai/keys'));
3738
+ console.log(chalk.white(' 2. Sign in to your OpenRouter account (or create a free account if needed)'));
3739
+ console.log(chalk.white(' 3. Click the'), chalk.cyan('"Create API Key"'), chalk.white('button'));
3740
+ console.log(chalk.white(' 4. Give it a name (e.g.,'), chalk.cyan('"Vibe Coding Machine 2"'), chalk.white('or'), chalk.cyan('"Vibe Coding Machine Backup"'), chalk.white(')'));
3741
+ console.log(chalk.white(' 5. Leave'), chalk.cyan('"Reset limit every..."'), chalk.white('at'), chalk.cyan('"N/A"'));
3742
+ console.log(chalk.white(' 6. Click'), chalk.cyan('"Create Key"'), chalk.white('(or similar button)'));
3743
+ console.log(chalk.white(' 7. Copy the API key that appears (starts with'), chalk.cyan('"sk-or-"'), chalk.white(')'));
3744
+ console.log(chalk.white(' 8. Save it somewhere safe - it will only be shown once!'));
3745
+ console.log(chalk.gray(''));
3746
+ console.log(chalk.cyan(' 9. In Vibe Coding Machine:'));
3747
+ console.log(chalk.white(' • Stop auto mode (if running)'));
3748
+ console.log(chalk.white(' • Start auto mode again'));
3749
+ console.log(chalk.white(' • Select'), chalk.cyan('"OpenRouter"'), chalk.white('when prompted'));
3750
+ console.log(chalk.white(' • Paste your new API key'));
3751
+ console.log(chalk.gray(''));
3752
+ console.log(chalk.cyan(' šŸ’” Important:'));
3753
+ console.log(chalk.gray(' • Free OpenRouter keys have usage limits (typically 20 requests/minute)'));
3754
+ console.log(chalk.gray(' • You can create unlimited new keys for free'));
3755
+ console.log(chalk.gray(' • When one key hits its limit, just create a new one!'));
3756
+ console.log(chalk.gray(''));
3757
+ } else {
3758
+ console.log(chalk.yellow('\n⚠ Your API key is invalid or expired.'));
3759
+ }
3760
+
3761
+ // Mark key as invalid immediately
3762
+ try {
3763
+ if (apiProvider === 'anthropic') {
3764
+ delete config.globalState.anthropicApiKey;
3765
+ delete config.globalState.apiProvider;
3766
+ delete config.globalState.apiModelId;
3767
+ const savedKeyFile = path.join(allnightDir, 'anthropic-api-key.txt');
3768
+ if (fs.existsSync(savedKeyFile)) {
3769
+ fs.unlinkSync(savedKeyFile);
3770
+ }
3771
+ const invalidMarker = path.join(allnightDir, '.anthropic-key-invalid');
3772
+ fs.writeFileSync(invalidMarker, new Date().toISOString());
3773
+ } else if (apiProvider === 'openrouter') {
3774
+ delete config.globalState.openRouterApiKey;
3775
+ delete config.globalState.apiProvider;
3776
+ delete config.globalState.apiModelId;
3777
+ const savedKeyFile = path.join(allnightDir, 'openrouter-api-key.txt');
3778
+ if (fs.existsSync(savedKeyFile)) {
3779
+ fs.unlinkSync(savedKeyFile);
3780
+ }
3781
+ const invalidMarker = path.join(allnightDir, '.openrouter-key-invalid');
3782
+ fs.writeFileSync(invalidMarker, new Date().toISOString());
3783
+ } else if (apiProvider === 'gemini' || apiProvider === 'openai-native') {
3784
+ delete config.globalState.geminiApiKey;
3785
+ delete config.globalState.openAiApiKey;
3786
+ delete config.globalState.apiProvider;
3787
+ delete config.globalState.apiModelId;
3788
+ const savedKeyFile = path.join(allnightDir, 'gemini-api-key.txt');
3789
+ if (fs.existsSync(savedKeyFile)) {
3790
+ fs.unlinkSync(savedKeyFile);
3791
+ }
3792
+ const invalidMarker = path.join(allnightDir, '.gemini-key-invalid');
3793
+ fs.writeFileSync(invalidMarker, new Date().toISOString());
3794
+ }
3795
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
3796
+ } catch (error) {
3797
+ // Non-fatal
3798
+ }
3799
+
3800
+ console.log(chalk.gray('\n Invalid key has been removed. Restart auto mode to set up a new API key.'));
3801
+ if (!validationResult.rateLimited) {
3802
+ console.log(chalk.gray(' The browser will automatically open to get a new key.'));
3803
+ }
3804
+ process.exit(1);
3805
+ } else {
3806
+ if (validationResult.warning) {
3807
+ spinner.warn(`Validation incomplete: ${validationResult.warning}`);
3808
+ } else {
3809
+ spinner.succeed('API key validated successfully');
3810
+ }
3811
+ }
3812
+ }
3813
+ }
3814
+ }
3815
+
3816
+ // Force re-sync just before running to ensure key is in config file
3817
+ // This handles edge cases where sync might have failed
3818
+ try {
3819
+ const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
3820
+ if (fs.existsSync(configPath)) {
3821
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
3822
+ const apiProvider = config.globalState?.apiProvider;
3823
+
3824
+ if (apiProvider === 'anthropic' && !config.globalState?.anthropicApiKey) {
3825
+ const savedKey = clineManager.getSavedAnthropicKey();
3826
+ if (savedKey && savedKey.trim().length > 0) {
3827
+ config.globalState.anthropicApiKey = savedKey;
3828
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
3829
+ console.log(chalk.green('āœ“ Synced API key to Cline CLI config before starting'));
3830
+ }
3831
+ } else if (apiProvider === 'openrouter' && !config.globalState?.openRouterApiKey) {
3832
+ const savedKey = clineManager.getSavedOpenRouterKey();
3833
+ if (savedKey && savedKey.trim().length > 0) {
3834
+ config.globalState.openRouterApiKey = savedKey;
3835
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
3836
+ console.log(chalk.green('āœ“ Synced API key to Cline CLI config before starting'));
3837
+ }
3838
+ } else if (apiProvider === 'gemini' && !config.globalState?.geminiApiKey) {
3839
+ const savedKey = clineManager.getSavedGeminiKey();
3840
+ if (savedKey && savedKey.trim().length > 0) {
3841
+ config.globalState.geminiApiKey = savedKey;
3842
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
3843
+ console.log(chalk.green('āœ“ Synced API key to Cline CLI config before starting'));
3844
+ }
3845
+ }
3846
+ }
3847
+ } catch (error) {
3848
+ // Non-fatal - just log and continue
3849
+ console.log(chalk.yellow(' ⚠ Could not pre-sync API key:', error.message));
3850
+ }
3851
+
3852
+ spinner.text = 'Executing Cline CLI...';
3853
+ console.log(chalk.gray('\n Running: cline-cli task "') + chalk.cyan(textToSend.substring(0, 60) + '...') + chalk.gray('" --workspace ') + chalk.cyan(repoPath) + chalk.gray(' --full-auto'));
3854
+
3855
+ // Log the command to audit log
3856
+ logIDEMessage('cline', textToSend);
3857
+
3858
+ // Execute Cline CLI in background (non-blocking)
3859
+ try {
3860
+ console.log(chalk.gray('\n─────────────────────────────────────────────────────────'));
3861
+ console.log(chalk.bold.cyan('Cline CLI Output:'));
3862
+ console.log(chalk.gray('─────────────────────────────────────────────────────────\n'));
3863
+
3864
+ // Track error output for authentication detection
3865
+ let errorOutputBuffer = '';
3866
+
3867
+ // Track if we've seen any output (to detect silent failures)
3868
+ let hasSeenOutput = false;
3869
+ let outputCheckInterval;
3870
+
3871
+ const proc = clineManager.runInBackground(
3872
+ textToSend,
3873
+ repoPath,
3874
+ (output) => {
3875
+ // Mark that we've seen output
3876
+ hasSeenOutput = true;
3877
+ if (outputCheckInterval) clearInterval(outputCheckInterval);
3878
+ // Output to console in real-time (tee to stdout)
3879
+ process.stdout.write(chalk.gray(output));
3880
+ // Also log via logger for audit trail
3881
+ if (output.trim()) {
3882
+ logger.info('[Cline]', output.trim());
3883
+ }
3884
+ },
3885
+ (error) => {
3886
+ // Mark that we've seen output (errors count too)
3887
+ hasSeenOutput = true;
3888
+ if (outputCheckInterval) clearInterval(outputCheckInterval);
3889
+ // Collect error output for analysis
3890
+ errorOutputBuffer += error;
3891
+
3892
+ // Output errors to console in real-time with full details
3893
+ const errorText = error.toString();
3894
+ process.stderr.write(chalk.red(errorText));
3895
+ // Also log via logger for audit trail
3896
+ if (errorText.trim()) {
3897
+ logger.error('[Cline Error]', errorText.trim());
3898
+ }
3899
+
3900
+ // If we see "Maximum retries reached", show more diagnostic info
3901
+ if (errorText.toLowerCase().includes('maximum retries reached')) {
3902
+ console.log(chalk.yellow('\n⚠ Diagnostic: Cline CLI is failing to connect to Ollama'));
3903
+ console.log(chalk.gray(' This usually means:'));
3904
+ console.log(chalk.gray(' 1. Cline CLI cannot reach Ollama API'));
3905
+ console.log(chalk.gray(' 2. Model name format is incorrect'));
3906
+ console.log(chalk.gray(' 3. Cline CLI configuration is incorrect\n'));
3907
+ }
3908
+ }
3909
+ );
3910
+
3911
+ spinner.succeed('Cline CLI started in background');
3912
+ console.log(chalk.green('āœ“ Cline CLI is now running autonomously'));
3913
+ console.log(chalk.gray(' Process ID:'), chalk.cyan(proc.pid));
3914
+ console.log(chalk.gray(' Output is displayed below'));
3915
+ console.log(chalk.gray(' Press Ctrl+C to stop\n'));
3916
+
3917
+ // If no output after 10 seconds, warn the user
3918
+ outputCheckInterval = setInterval(() => {
3919
+ if (!hasSeenOutput) {
3920
+ console.log(chalk.yellow('\n⚠ No output from Cline CLI after 10 seconds.'));
3921
+ console.log(chalk.gray(' This might indicate a configuration issue.'));
3922
+
3923
+ // Detect current provider to give context-aware suggestions
3924
+ try {
3925
+ const fs = require('fs');
3926
+ const path = require('path');
3927
+ const os = require('os');
3928
+ const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
3929
+ if (fs.existsSync(configPath)) {
3930
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
3931
+ const baseUrl = config.globalState?.openAiBaseUrl || '';
3932
+ const apiProvider = config.globalState?.apiProvider;
3933
+
3934
+ if (baseUrl.includes('generativelanguage.googleapis.com')) {
3935
+ console.log(chalk.gray(' You\'re using Gemini. Try switching to Ollama (100% free, no limits) or OpenRouter.'));
3936
+ } else if (baseUrl.includes('localhost:11434') || baseUrl.includes('127.0.0.1:11434')) {
3937
+ console.log(chalk.gray(' You\'re using Ollama. Check that ollama serve is running.'));
3938
+ } else if (baseUrl.includes('openrouter.ai') || apiProvider === 'openrouter') {
3939
+ console.log(chalk.gray(' You\'re using OpenRouter. Try switching to Ollama (100% free, no limits) or Gemini.'));
3940
+ } else if (apiProvider === 'anthropic') {
3941
+ console.log(chalk.gray(' You\'re using Anthropic. Try switching to Ollama (100% free, no limits) or Gemini.'));
3942
+ } else {
3943
+ console.log(chalk.gray(' Try switching to Ollama (100% free, no limits) or another provider.'));
3944
+ }
3945
+ } else {
3946
+ console.log(chalk.gray(' Try configuring an AI provider (Ollama, Gemini, OpenRouter, or Anthropic).'));
3947
+ }
3948
+ } catch (e) {
3949
+ console.log(chalk.gray(' Try switching AI providers or checking your configuration.'));
3950
+ }
3951
+ }
3952
+ }, 10000);
3953
+
3954
+ // Handle process exit
3955
+ proc.on('close', async (code) => {
3956
+ if (outputCheckInterval) clearInterval(outputCheckInterval);
3957
+ console.log(chalk.gray('\n─────────────────────────────────────────────────────────'));
3958
+ if (code === 0) {
3959
+ if (!hasSeenOutput && errorOutputBuffer.trim().length === 0) {
3960
+ console.log(chalk.yellow('⚠ Cline CLI exited with no output'));
3961
+ console.log(chalk.yellow(' This might indicate a configuration issue.'));
3962
+
3963
+ // Detect current provider to give context-aware suggestions
3964
+ try {
3965
+ const fs = require('fs');
3966
+ const path = require('path');
3967
+ const os = require('os');
3968
+ const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
3969
+ if (fs.existsSync(configPath)) {
3970
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
3971
+ const baseUrl = config.globalState?.openAiBaseUrl || '';
3972
+ const apiProvider = config.globalState?.apiProvider;
3973
+
3974
+ if (baseUrl.includes('generativelanguage.googleapis.com')) {
3975
+ console.log(chalk.gray(' You\'re using Gemini. Try switching to Ollama (100% free, no limits) or OpenRouter.'));
3976
+ } else if (baseUrl.includes('localhost:11434') || baseUrl.includes('127.0.0.1:11434')) {
3977
+ console.log(chalk.gray(' You\'re using Ollama. Check that ollama serve is running.'));
3978
+ } else if (baseUrl.includes('openrouter.ai') || apiProvider === 'openrouter') {
3979
+ console.log(chalk.gray(' You\'re using OpenRouter. Try switching to Ollama (100% free, no limits) or Gemini.'));
3980
+ } else if (apiProvider === 'anthropic') {
3981
+ console.log(chalk.gray(' You\'re using Anthropic. Try switching to Ollama (100% free, no limits) or Gemini.'));
3982
+ } else {
3983
+ console.log(chalk.gray(' Try switching to Ollama (100% free, no limits) or another provider.'));
3984
+ }
3985
+ } else {
3986
+ console.log(chalk.gray(' Try configuring an AI provider (Ollama, Gemini, OpenRouter, or Anthropic).'));
3987
+ }
3988
+ } catch (e) {
3989
+ console.log(chalk.gray(' Try switching AI providers or checking your configuration.'));
3990
+ }
3991
+ } else {
3992
+ console.log(chalk.green('āœ“ Cline CLI completed successfully'));
3993
+ }
3994
+ logger.info(chalk.green('Cline CLI completed successfully'));
3995
+ } else {
3996
+ console.log(chalk.red(`\nāœ— Cline CLI exited with code ${code}`));
3997
+
3998
+ // Show full error output for debugging
3999
+ if (errorOutputBuffer.trim()) {
4000
+ console.log(chalk.yellow('\nšŸ“‹ Full Error Output:'));
4001
+ console.log(chalk.gray(errorOutputBuffer));
4002
+ }
4003
+
4004
+ logger.error(chalk.red(`Cline CLI exited with code ${code}`));
4005
+ if (errorOutputBuffer.trim()) {
4006
+ logger.error('[Cline Full Error]', errorOutputBuffer);
4007
+ }
4008
+
4009
+ // Detect authentication/API key errors
4010
+ const lowerErrorOutput = errorOutputBuffer.toLowerCase();
4011
+
4012
+ // Check which provider is configured
4013
+ const fs2 = require('fs');
4014
+ const path2 = require('path');
4015
+ const os2 = require('os');
4016
+ const configPath2 = path2.join(os2.homedir(), '.cline_cli', 'cline_cli_settings.json');
4017
+ let apiProvider = null;
4018
+ let isOllama = false;
4019
+ let isOpenRouter = false;
4020
+
4021
+ let configuredModelId = null;
4022
+ if (fs2.existsSync(configPath2)) {
4023
+ const config = JSON.parse(fs2.readFileSync(configPath2, 'utf8'));
4024
+ apiProvider = config.globalState?.apiProvider;
4025
+ configuredModelId = config.globalState?.apiModelId;
4026
+ isOllama = apiProvider === 'openai-native' && config.globalState?.openAiBaseUrl === 'http://localhost:11434/v1';
4027
+ isOpenRouter = apiProvider === 'openrouter';
4028
+ }
4029
+
4030
+ // "Maximum retries reached" with OpenRouter is almost always a rate limit issue
4031
+ const isRateLimited = lowerErrorOutput.includes('rate limit') ||
4032
+ lowerErrorOutput.includes('limit exceeded') ||
4033
+ lowerErrorOutput.includes('too many requests') ||
4034
+ lowerErrorOutput.includes('429') ||
4035
+ (isOpenRouter && lowerErrorOutput.includes('maximum retries reached')) ||
4036
+ (lowerErrorOutput.includes('maximum retries reached') &&
4037
+ (lowerErrorOutput.includes('openrouter') || lowerErrorOutput.includes('limit')));
4038
+
4039
+ // For Ollama, "Maximum retries reached" is NOT an auth error - it's a connectivity/service issue
4040
+ const isAuthError = !isOllama && (
4041
+ code === 255 ||
4042
+ code === 1 ||
4043
+ lowerErrorOutput.includes('maximum retries reached') ||
4044
+ lowerErrorOutput.includes('authentication') ||
4045
+ lowerErrorOutput.includes('api key') ||
4046
+ lowerErrorOutput.includes('unauthorized') ||
4047
+ lowerErrorOutput.includes('401') ||
4048
+ lowerErrorOutput.includes('403') ||
4049
+ lowerErrorOutput.includes('invalid api') ||
4050
+ lowerErrorOutput.includes('expired') ||
4051
+ isRateLimited
4052
+ );
4053
+
4054
+ // Ollama-specific errors
4055
+ const isOllamaError = isOllama && (
4056
+ lowerErrorOutput.includes('maximum retries reached') ||
4057
+ lowerErrorOutput.includes('connection refused') ||
4058
+ lowerErrorOutput.includes('connect econnrefused') ||
4059
+ lowerErrorOutput.includes('server not responding') ||
4060
+ code === 255 ||
4061
+ code === 1
4062
+ );
4063
+
4064
+ if (isOllamaError) {
4065
+ // Attempt automatic fix for Ollama
4066
+ try {
4067
+ const fixSpinner = ora('Attempting to fix Ollama automatically...').start();
4068
+
4069
+ // 1) Ensure service is running
4070
+ fixSpinner.text = 'Starting Ollama service...';
4071
+ const started = await clineManager.startOllamaService();
4072
+ if (started) {
4073
+ fixSpinner.succeed('Ollama service is running');
4074
+ } else {
4075
+ fixSpinner.warn('Could not confirm Ollama service is running (it may still be starting)');
4076
+ }
4077
+
4078
+ // 2) Verify API endpoint is accessible (wait a bit if service just started)
4079
+ if (started) {
4080
+ fixSpinner.start('Verifying Ollama API is accessible...');
4081
+ // Wait a moment for service to fully initialize
4082
+ await new Promise(resolve => setTimeout(resolve, 2000));
4083
+ const apiCheck = await clineManager.verifyOllamaAPI();
4084
+ if (apiCheck.success) {
4085
+ fixSpinner.succeed('Ollama API is accessible');
4086
+ } else {
4087
+ fixSpinner.warn('Ollama API may not be fully ready (will retry)');
4088
+ }
4089
+
4090
+ // Re-verify configuration is correct for Cline CLI
4091
+ if (configuredModelId) {
4092
+ fixSpinner.start('Re-verifying Cline CLI configuration...');
4093
+ const configResult = await clineManager.configureWithOllama(configuredModelId);
4094
+ if (configResult.success) {
4095
+ fixSpinner.succeed('Cline CLI configuration verified');
4096
+ } else {
4097
+ fixSpinner.warn('Could not re-verify configuration (will retry)');
4098
+ }
4099
+ }
4100
+ }
4101
+
4102
+ // 3) Ensure configured model is installed and accessible (if known)
4103
+ if (configuredModelId) {
4104
+ fixSpinner.start(`Ensuring model ${configuredModelId} is installed...`);
4105
+ const pullResult = await clineManager.pullOllamaModel(configuredModelId, (p) => {
4106
+ const percent = typeof p === 'object' && p.percentage !== undefined ? p.percentage : p;
4107
+ fixSpinner.text = `Downloading ${configuredModelId}... ${percent}%`;
4108
+ });
4109
+ if (pullResult.success) {
4110
+ // Verify model is accessible via API
4111
+ fixSpinner.start(`Verifying model ${configuredModelId} is accessible...`);
4112
+ await new Promise(resolve => setTimeout(resolve, 1000));
4113
+ const modelAccessible = await clineManager.verifyModelAccessible(configuredModelId);
4114
+ if (modelAccessible) {
4115
+ fixSpinner.succeed(`Model ${configuredModelId} is ready and accessible`);
4116
+ } else {
4117
+ fixSpinner.succeed(`Model ${configuredModelId} is installed (verifying accessibility...)`);
4118
+ }
4119
+ } else {
4120
+ fixSpinner.warn(`Could not verify/download model ${configuredModelId}. Continuing...`);
4121
+ }
4122
+ }
4123
+
4124
+ // 3) Retry the task once (only retry once to avoid infinite loops)
4125
+ console.log(chalk.cyan('\n↻ Retrying task now that Ollama is ready...\n'));
4126
+ console.log(chalk.gray('─────────────────────────────────────────────────────────'));
4127
+ console.log(chalk.bold.cyan('Cline CLI Output (Retry):'));
4128
+ console.log(chalk.gray('─────────────────────────────────────────────────────────\n'));
4129
+
4130
+ // Track that we've already attempted auto-fix
4131
+ let retryErrorBuffer = '';
4132
+ let retryHasSeenOutput = false;
4133
+ let retryOutputCheckInterval;
4134
+
4135
+ retryOutputCheckInterval = setInterval(() => {
4136
+ if (!retryHasSeenOutput) {
4137
+ console.log(chalk.yellow('\n⚠ No output from Cline CLI after 10 seconds.'));
4138
+ console.log(chalk.gray(' This might indicate a configuration issue.'));
4139
+ }
4140
+ }, 10000);
4141
+
4142
+ const retryProc = clineManager.runInBackground(
4143
+ textToSend,
4144
+ repoPath,
4145
+ (output) => {
4146
+ retryHasSeenOutput = true;
4147
+ if (retryOutputCheckInterval) clearInterval(retryOutputCheckInterval);
4148
+ process.stdout.write(chalk.gray(output));
4149
+ if (output.trim()) {
4150
+ logger.info('[Cline Retry]', output.trim());
4151
+ }
4152
+ },
4153
+ (error) => {
4154
+ retryHasSeenOutput = true;
4155
+ if (retryOutputCheckInterval) clearInterval(retryOutputCheckInterval);
4156
+ retryErrorBuffer += error;
4157
+ process.stderr.write(chalk.red(error));
4158
+ if (error.trim()) {
4159
+ logger.error('[Cline Retry Error]', error.trim());
4160
+ }
4161
+ }
4162
+ );
4163
+
4164
+ // Handle retry process exit (don't auto-fix again to avoid loops)
4165
+ retryProc.on('close', async (retryCode) => {
4166
+ if (retryOutputCheckInterval) clearInterval(retryOutputCheckInterval);
4167
+ console.log(chalk.gray('\n─────────────────────────────────────────────────────────'));
4168
+ if (retryCode === 0) {
4169
+ if (!retryHasSeenOutput && retryErrorBuffer.trim().length === 0) {
4170
+ console.log(chalk.yellow('⚠ Cline CLI completed but with no output'));
4171
+ } else {
4172
+ console.log(chalk.green('āœ“ Cline CLI completed successfully after retry'));
4173
+ }
4174
+ logger.info('Cline CLI completed successfully after retry');
4175
+ } else {
4176
+ console.log(chalk.red(`āœ— Cline CLI failed again with code ${retryCode}`));
4177
+ logger.error(`Cline CLI failed again with code ${retryCode}`);
4178
+
4179
+ // Show error message but don't auto-fix again
4180
+ const lowerRetryError = retryErrorBuffer.toLowerCase();
4181
+ if (isOllama && (
4182
+ lowerRetryError.includes('maximum retries reached') ||
4183
+ lowerRetryError.includes('connection refused') ||
4184
+ lowerRetryError.includes('server not responding') ||
4185
+ lowerRetryError.includes('connect econnrefused')
4186
+ )) {
4187
+ console.log(chalk.yellow('\n⚠ Ollama is still having connection issues after automatic fix.'));
4188
+ console.log(chalk.gray(' Please manually verify:'));
4189
+ console.log(chalk.cyan(' 1. ollama serve (in a separate terminal)'));
4190
+ console.log(chalk.cyan(' 2. curl http://localhost:11434/api/tags'));
4191
+ console.log(chalk.cyan(' 3. ollama list (verify models)'));
4192
+ if (configuredModelId) {
4193
+ console.log(chalk.cyan(` 4. ollama pull ${configuredModelId}`));
4194
+ }
4195
+ console.log(chalk.gray('\n If Ollama is running but still failing, there may be a deeper issue.'));
4196
+ console.log(chalk.gray(' Consider switching to a different AI provider temporarily.'));
4197
+ } else {
4198
+ // Other error - show generic help
4199
+ console.log(chalk.yellow('\n⚠ Cline CLI failed after automatic retry.'));
4200
+ console.log(chalk.gray(' Please check the error messages above for details.'));
4201
+ }
4202
+ }
4203
+ console.log(chalk.gray('─────────────────────────────────────────────────────────\n'));
4204
+
4205
+ // Clean up Auto Mode status when retry process exits
4206
+ await stopAutoMode('completed');
4207
+ });
4208
+
4209
+ // Don't continue with original error handling - retry will handle it
4210
+ return;
4211
+ } catch (autoFixErr) {
4212
+ console.log(chalk.yellow(' ⚠ Automatic fix failed:'), autoFixErr.message);
4213
+ }
4214
+
4215
+ // Ollama-specific error handling
4216
+ console.log(chalk.yellow('\n⚠ Cline CLI failed - Ollama connection error detected.'));
4217
+ console.log(chalk.yellow(' This usually means the Ollama service is not responding or not running.'));
4218
+ console.log(chalk.gray('\nšŸ’” How to fix:'));
4219
+ console.log(chalk.gray(' 1. Check if Ollama service is running:'));
4220
+ console.log(chalk.cyan(' ollama serve'));
4221
+ console.log(chalk.gray(' 2. Or launch Ollama.app from /Applications'));
4222
+ console.log(chalk.gray(' 3. Verify the service is accessible:'));
4223
+ console.log(chalk.cyan(' curl http://localhost:11434/api/tags'));
4224
+ console.log(chalk.gray(' 4. Make sure the model you\'re using is installed:'));
4225
+ console.log(chalk.cyan(' ollama list'));
4226
+ if (configuredModelId) {
4227
+ console.log(chalk.gray(' 5. Configured model:'), chalk.cyan(configuredModelId));
4228
+ console.log(chalk.gray(' 6. If the model is missing, download it:'));
4229
+ console.log(chalk.cyan(` ollama pull ${configuredModelId}`));
4230
+ console.log(chalk.gray(' 7. Then restart auto mode and try again\n'));
4231
+ } else {
4232
+ console.log(chalk.gray(' 5. If the model is missing, download it:'));
4233
+ console.log(chalk.cyan(' ollama pull <model-name>'));
4234
+ console.log(chalk.gray(' 6. Then restart auto mode and try again\n'));
4235
+ }
4236
+ // Don't clear config for Ollama errors - it's a service issue, not a config issue
4237
+ } else if (isAuthError) {
4238
+ if (isRateLimited) {
4239
+ console.log(chalk.yellow('\n⚠ Cline CLI failed - rate limit error detected.'));
4240
+ console.log(chalk.yellow(' Your OpenRouter free API key has exceeded its usage limit.'));
4241
+ console.log(chalk.cyan('\nšŸ“ Step-by-step instructions to create a new OpenRouter API key:'));
4242
+ console.log(chalk.gray(''));
4243
+ console.log(chalk.white(' 1. Open your browser and visit:'), chalk.cyan('https://openrouter.ai/keys'));
4244
+ console.log(chalk.white(' 2. Sign in to your OpenRouter account (or create a free account if needed)'));
4245
+ console.log(chalk.white(' 3. Click the'), chalk.cyan('"Create API Key"'), chalk.white('button'));
4246
+ console.log(chalk.white(' 4. Give it a name (e.g.,'), chalk.cyan('"Vibe Coding Machine 2"'), chalk.white('or'), chalk.cyan('"Vibe Coding Machine Backup"'), chalk.white(')'));
4247
+ console.log(chalk.white(' 5. Leave'), chalk.cyan('"Reset limit every..."'), chalk.white('at'), chalk.cyan('"N/A"'));
4248
+ console.log(chalk.white(' 6. Click'), chalk.cyan('"Create Key"'), chalk.white('(or similar button)'));
4249
+ console.log(chalk.white(' 7. Copy the API key that appears (starts with'), chalk.cyan('"sk-or-"'), chalk.white(')'));
4250
+ console.log(chalk.white(' 8. Save it somewhere safe - it will only be shown once!'));
4251
+ console.log(chalk.gray(''));
4252
+ console.log(chalk.cyan(' 9. In Vibe Coding Machine:'));
4253
+ console.log(chalk.white(' • Stop auto mode (if running)'));
4254
+ console.log(chalk.white(' • Start auto mode again'));
4255
+ console.log(chalk.white(' • Select'), chalk.cyan('"OpenRouter"'), chalk.white('when prompted'));
4256
+ console.log(chalk.white(' • Paste your new API key'));
4257
+ console.log(chalk.gray(''));
4258
+ console.log(chalk.cyan(' šŸ’” Important:'));
4259
+ console.log(chalk.gray(' • Free OpenRouter keys have usage limits (typically 20 requests/minute)'));
4260
+ console.log(chalk.gray(' • You can create unlimited new keys for free'));
4261
+ console.log(chalk.gray(' • When one key hits its limit, just create a new one!'));
4262
+ console.log(chalk.gray(''));
4263
+ } else {
4264
+ console.log(chalk.yellow('\n⚠ Cline CLI failed - authentication error detected.'));
4265
+ console.log(chalk.yellow(' This usually means your API key is missing, invalid, or expired.'));
4266
+ }
4267
+
4268
+ // Clear invalid API key from BOTH config file AND saved file to force reconfiguration
4269
+ try {
4270
+ const fs = require('fs');
4271
+ const path = require('path');
4272
+ const os = require('os');
4273
+ const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
4274
+ const allnightDir = path.join(os.homedir(), '.vibecodingmachine');
4275
+
4276
+ if (fs.existsSync(configPath)) {
4277
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
4278
+ const apiProvider = config.globalState?.apiProvider;
4279
+
4280
+ if (apiProvider === 'anthropic') {
4281
+ // Clear the invalid key AND provider from config to force reconfiguration
4282
+ delete config.globalState.anthropicApiKey;
4283
+ delete config.globalState.apiProvider;
4284
+ delete config.globalState.apiModelId;
4285
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
4286
+
4287
+ // Also clear saved key file and create invalid marker
4288
+ const savedKeyFile = path.join(allnightDir, 'anthropic-api-key.txt');
4289
+ if (fs.existsSync(savedKeyFile)) {
4290
+ fs.unlinkSync(savedKeyFile);
4291
+ }
4292
+
4293
+ // Create marker file to prevent re-syncing invalid key
4294
+ const invalidMarker = path.join(allnightDir, '.anthropic-key-invalid');
4295
+ fs.writeFileSync(invalidMarker, new Date().toISOString());
4296
+
4297
+ console.log(chalk.gray('\n āœ“ Cleared invalid API key from config and saved files'));
4298
+ console.log(chalk.gray(' Next time you start auto mode, you will be prompted to set up a new API key.'));
4299
+ } else if (apiProvider === 'openrouter') {
4300
+ // For rate-limited keys, don't delete the key file (user can create new one)
4301
+ // Just clear from config so they can reconfigure
4302
+ if (isRateLimited) {
4303
+ delete config.globalState.openRouterApiKey;
4304
+ delete config.globalState.apiProvider;
4305
+ delete config.globalState.apiModelId;
4306
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
4307
+ console.log(chalk.gray('\n āœ“ Cleared rate-limited API key from config'));
4308
+ console.log(chalk.gray(' Your saved key file was kept - you can create a new key and reconfigure.'));
4309
+ } else {
4310
+ // Invalid key - clear everything
4311
+ delete config.globalState.openRouterApiKey;
4312
+ delete config.globalState.apiProvider;
4313
+ delete config.globalState.apiModelId;
4314
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
4315
+
4316
+ // Also clear saved key file and create invalid marker
4317
+ const savedKeyFile = path.join(allnightDir, 'openrouter-api-key.txt');
4318
+ if (fs.existsSync(savedKeyFile)) {
4319
+ fs.unlinkSync(savedKeyFile);
4320
+ }
4321
+
4322
+ // Create marker file to prevent re-syncing invalid key
4323
+ const invalidMarker = path.join(allnightDir, '.openrouter-key-invalid');
4324
+ fs.writeFileSync(invalidMarker, new Date().toISOString());
4325
+
4326
+ console.log(chalk.gray('\n āœ“ Cleared invalid API key from config and saved files'));
4327
+ }
4328
+ console.log(chalk.gray(' Next time you start auto mode, you will be prompted to set up a new API key.'));
4329
+ } else if (apiProvider === 'gemini') {
4330
+ // Clear the invalid key AND provider from config to force reconfiguration
4331
+ delete config.globalState.geminiApiKey;
4332
+ delete config.globalState.apiProvider;
4333
+ delete config.globalState.apiModelId;
4334
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
4335
+
4336
+ // Also clear saved key file and create invalid marker
4337
+ const savedKeyFile = path.join(allnightDir, 'gemini-api-key.txt');
4338
+ if (fs.existsSync(savedKeyFile)) {
4339
+ fs.unlinkSync(savedKeyFile);
4340
+ }
4341
+
4342
+ // Create marker file to prevent re-syncing invalid key
4343
+ const invalidMarker = path.join(allnightDir, '.gemini-key-invalid');
4344
+ fs.writeFileSync(invalidMarker, new Date().toISOString());
4345
+
4346
+ console.log(chalk.gray('\n āœ“ Cleared invalid API key from config and saved files'));
4347
+ console.log(chalk.gray(' Next time you start auto mode, you will be prompted to set up a new API key.'));
4348
+ }
4349
+ }
4350
+ } catch (clearError) {
4351
+ // Non-fatal
4352
+ console.log(chalk.yellow(' ⚠ Could not clear invalid config:', clearError.message));
4353
+ }
4354
+
4355
+ console.log(chalk.gray('\n To fix this:'));
4356
+ console.log(chalk.gray(' 1. Stop auto mode (press Ctrl+C or select "Auto Mode: Stop")'));
4357
+ console.log(chalk.gray(' 2. Start auto mode again'));
4358
+ console.log(chalk.gray(' 3. You will be prompted to set up a new API key'));
4359
+ console.log(chalk.gray(' 4. Choose Google Gemini (free, no credit card), OpenRouter (free), or Anthropic'));
4360
+ console.log(chalk.gray(' 5. Enter a valid API key'));
4361
+ }
4362
+ }
4363
+ console.log(chalk.gray('─────────────────────────────────────────────────────────\n'));
4364
+
4365
+ // Clean up Auto Mode status when Cline CLI process exits
4366
+ await stopAutoMode('completed');
4367
+ });
4368
+ } catch (error) {
4369
+ spinner.fail('Failed to start Cline CLI');
4370
+ console.log(chalk.red('\nāœ— Error:'), error.message);
4371
+
4372
+ // Clean up Auto Mode status if Cline CLI fails to start
4373
+ await stopAutoMode('error');
4374
+ }
4375
+ } else {
4376
+ // Use AppleScript for GUI IDEs
4377
+
4378
+ // Check if IDE is installed, download if needed
4379
+ spinner.text = `Checking ${config.ide} installation...`;
4380
+ const fs = require('fs');
4381
+ const { spawn } = require('child_process');
4382
+ const https = require('https');
4383
+
4384
+ const ideAppPaths = {
4385
+ 'cursor': '/Applications/Cursor.app',
4386
+ 'windsurf': '/Applications/Windsurf.app',
4387
+ 'antigravity': '/Applications/Antigravity.app',
4388
+ 'vscode': '/Applications/Visual Studio Code.app'
4389
+ };
4390
+
4391
+ const ideDownloadUrls = {
4392
+ 'antigravity': os.arch() === 'arm64'
4393
+ ? 'https://edgedl.me.gvt1.com/edgedl/release2/j0qc3/antigravity/stable/1.11.2-6251250307170304/darwin-arm/Antigravity.dmg'
4394
+ : 'https://edgedl.me.gvt1.com/edgedl/release2/j0qc3/antigravity/stable/1.11.2-6251250307170304/darwin-x64/Antigravity.dmg'
4395
+ };
4396
+
4397
+ const ideAppPath = ideAppPaths[config.ide];
4398
+
4399
+ if (ideAppPath && !fs.existsSync(ideAppPath)) {
4400
+ spinner.warn(chalk.yellow(`${config.ide} not found at ${ideAppPath}`));
4401
+
4402
+ const downloadUrl = ideDownloadUrls[config.ide];
4403
+ if (downloadUrl) {
4404
+ console.log(chalk.cyan(`šŸ“„ Downloading ${config.ide}...`));
4405
+ spinner.text = `Downloading ${config.ide}...`;
4406
+
4407
+ const downloadPath = path.join(os.tmpdir(), `${config.ide}.dmg`);
4408
+
4409
+ try {
4410
+ // Download the DMG
4411
+ await new Promise((resolve, reject) => {
4412
+ const file = fs.createWriteStream(downloadPath);
4413
+
4414
+ const downloadFile = (url) => {
4415
+ https.get(url, (response) => {
4416
+ // Handle redirects
4417
+ if (response.statusCode === 301 || response.statusCode === 302) {
4418
+ file.close();
4419
+ fs.unlinkSync(downloadPath);
4420
+ downloadFile(response.headers.location);
4421
+ return;
4422
+ }
4423
+
4424
+ if (response.statusCode !== 200) {
4425
+ file.close();
4426
+ fs.unlinkSync(downloadPath);
4427
+ reject(new Error(`Download failed with status ${response.statusCode}`));
4428
+ return;
4429
+ }
4430
+
4431
+ response.pipe(file);
4432
+ file.on('finish', () => {
4433
+ file.close();
4434
+ resolve();
4435
+ });
4436
+ }).on('error', (err) => {
4437
+ try {
4438
+ file.close();
4439
+ fs.unlinkSync(downloadPath);
4440
+ } catch (e) {
4441
+ // Ignore cleanup errors
4442
+ }
4443
+ reject(err);
4444
+ });
4445
+ };
4446
+
4447
+ downloadFile(downloadUrl);
4448
+ });
4449
+
4450
+ console.log(chalk.green(`āœ“ Downloaded ${config.ide}`));
4451
+ spinner.text = `Installing ${config.ide}...`;
4452
+
4453
+ // Mount the DMG (remove -quiet to get mount point output)
4454
+ const { execSync } = require('child_process');
4455
+ const mountOutput = execSync(`hdiutil attach "${downloadPath}" -nobrowse`, { encoding: 'utf8' });
4456
+
4457
+ // Parse mount point from hdiutil output (last line contains mount point)
4458
+ const lines = mountOutput.trim().split('\n');
4459
+ const lastLine = lines[lines.length - 1];
4460
+ const mountPoint = lastLine.match(/\/Volumes\/.+$/)?.[0]?.trim();
4461
+
4462
+ if (mountPoint) {
4463
+ console.log(chalk.gray(` Mounted at: ${mountPoint}`));
4464
+
4465
+ // Determine app name inside DMG
4466
+ const appName = config.ide === 'antigravity' ? 'Antigravity' :
4467
+ config.ide === 'cursor' ? 'Cursor' :
4468
+ config.ide === 'windsurf' ? 'Windsurf' :
4469
+ config.ide === 'vscode' ? 'Visual Studio Code' : config.ide;
4470
+
4471
+ // Copy app to Applications
4472
+ execSync(`cp -R "${mountPoint}/${appName}.app" /Applications/`, { encoding: 'utf8' });
4473
+
4474
+ // Unmount
4475
+ execSync(`hdiutil detach "${mountPoint}"`, { encoding: 'utf8' });
4476
+
4477
+ // Clean up DMG
4478
+ fs.unlinkSync(downloadPath);
4479
+
4480
+ console.log(chalk.green(`āœ“ Installed ${config.ide} to /Applications`));
4481
+ spinner.succeed(`${config.ide} installed successfully`);
4482
+ } else {
4483
+ throw new Error(`Could not parse mount point from: ${mountOutput}`);
4484
+ }
4485
+ } catch (downloadError) {
4486
+ spinner.fail(`Failed to install ${config.ide}`);
4487
+ console.log(chalk.red('Error:'), downloadError.message);
4488
+ console.log(chalk.gray(`Please install ${config.ide} manually from its website`));
4489
+ await stopAutoMode('error');
4490
+ return;
4491
+ }
4492
+ } else {
4493
+ spinner.fail(`${config.ide} not installed and auto-installation not available`);
4494
+ console.log(chalk.yellow(`\nPlease install ${config.ide} manually before using it with Vibe Coding Machine`));
4495
+ await stopAutoMode('error');
4496
+ return;
4497
+ }
4498
+ } else if (ideAppPath) {
4499
+ spinner.succeed(`${config.ide} is installed`);
4500
+ }
4501
+
4502
+ // Launch the IDE if it's not already running
4503
+ spinner.text = `Launching ${config.ide}...`;
4504
+ const { execSync } = require('child_process');
4505
+
4506
+ try {
4507
+ // Check if IDE is already running
4508
+ const isRunning = execSync(`pgrep -x "${config.ide === 'antigravity' ? 'Antigravity' :
4509
+ config.ide === 'cursor' ? 'Cursor' :
4510
+ config.ide === 'windsurf' ? 'Windsurf' :
4511
+ config.ide === 'vscode' ? 'Code' : config.ide}"`,
4512
+ { encoding: 'utf8', stdio: 'pipe' }).trim();
4513
+ if (isRunning) {
4514
+ console.log(chalk.gray(` ${config.ide} is already running (PID: ${isRunning.split('\n')[0]})`));
4515
+ }
4516
+ } catch (err) {
4517
+ // Not running, launch it
4518
+ console.log(chalk.gray(` Launching ${config.ide}...`));
4519
+ const appName = config.ide === 'antigravity' ? 'Antigravity' :
4520
+ config.ide === 'cursor' ? 'Cursor' :
4521
+ config.ide === 'windsurf' ? 'Windsurf' :
4522
+ config.ide === 'vscode' ? 'Visual Studio Code' : config.ide;
4523
+
4524
+ execSync(`open -a "${appName}" "${repoPath}"`, { encoding: 'utf8' });
4525
+
4526
+ // Wait for the app to launch (Electron apps need more time)
4527
+ const waitTime = (config.ide === 'antigravity' || config.ide === 'cursor' || config.ide === 'windsurf') ? 5000 : 3000;
4528
+ console.log(chalk.gray(` Waiting for ${config.ide} to start...`));
4529
+ await new Promise(resolve => setTimeout(resolve, waitTime));
4530
+
4531
+ // Verify it's now running
4532
+ try {
4533
+ const processName = config.ide === 'antigravity' ? 'Antigravity' :
4534
+ config.ide === 'cursor' ? 'Cursor' :
4535
+ config.ide === 'windsurf' ? 'Windsurf' :
4536
+ config.ide === 'vscode' ? 'Code' : config.ide;
4537
+ const pid = execSync(`pgrep -x "${processName}"`, { encoding: 'utf8', stdio: 'pipe' }).trim();
4538
+ console.log(chalk.green(` āœ“ ${config.ide} started successfully (PID: ${pid.split('\n')[0]})`));
4539
+ } catch (checkErr) {
4540
+ console.log(chalk.yellow(` ⚠ ${config.ide} may still be starting up...`));
4541
+ // Continue anyway - the AppleScript will retry
4542
+ }
4543
+ }
4544
+
4545
+ spinner.text = 'Sending initial message to IDE...';
4546
+ const asManager = new AppleScriptManager();
4547
+
4548
+ try {
4549
+ const result = await asManager.sendText(textToSend, config.ide);
4550
+
4551
+ if (!result.success) {
4552
+ logIDEMessage(config.ide, `[FAILED] ${textToSend}`);
4553
+ spinner.warn('Auto mode started but failed to send initial message to IDE');
4554
+ console.log(chalk.yellow('\n⚠ Warning:'), result.error || 'Failed to send message');
4555
+ console.log(chalk.gray(' Consider using'), chalk.cyan('Cline IDE'), chalk.gray('instead (set with'), chalk.cyan('allnightai'), chalk.gray('menu)'));
4556
+ } else {
4557
+ logIDEMessage(config.ide, textToSend);
4558
+ spinner.succeed('Autonomous mode started and initial message sent');
4559
+ console.log(chalk.green('\nāœ“ Vibe Coding Machine is now coding autonomously'));
4560
+ }
4561
+ } catch (sendError) {
4562
+ logIDEMessage(config.ide, `[ERROR] ${textToSend}`);
4563
+ spinner.warn('Auto mode started but failed to send initial message');
4564
+ console.log(chalk.yellow('\n⚠ Warning:'), sendError.message);
4565
+ console.log(chalk.gray(' Consider using'), chalk.cyan('Cline IDE'), chalk.gray('instead'));
4566
+ }
4567
+ }
4568
+
4569
+ // Format IDE name for display
4570
+ const formatIDEName = (ide) => {
4571
+ const ideNames = {
4572
+ 'aider': 'Aider CLI',
4573
+ 'cline': 'Cline IDE',
4574
+ 'continue': 'Continue CLI',
4575
+ 'cursor': 'Cursor',
4576
+ 'vscode': 'VS Code',
4577
+ 'windsurf': 'Windsurf',
4578
+ 'antigravity': 'Google Antigravity'
4579
+ };
4580
+ return ideNames[ide] || ide;
4581
+ };
4582
+ console.log(chalk.gray(' IDE:'), chalk.cyan(formatIDEName(config.ide)));
4583
+
4584
+ if (config.neverStop) {
4585
+ console.log(chalk.gray(' Mode:'), chalk.cyan('Never stop'));
4586
+ } else if (config.maxChats) {
4587
+ console.log(chalk.gray(' Max chats:'), chalk.cyan(config.maxChats));
4588
+ }
4589
+ } catch (error) {
4590
+ spinner.stop();
4591
+ spinner.fail('Failed to start autonomous mode');
4592
+ console.error(chalk.red('Error:'), error.message);
4593
+ if (error.stack) {
4594
+ console.log(chalk.gray(error.stack));
4595
+ }
4596
+ throw error; // Re-throw so interactive.js can handle it
4597
+ }
4598
+ }
4599
+
4600
+ async function stop() {
4601
+ const spinner = ora('Stopping autonomous mode...').start();
4602
+
4603
+ try {
4604
+ // Create stop file to signal running auto:start process
4605
+ const fs = require('fs-extra');
4606
+ const stopFilePath = path.join(os.homedir(), '.config', 'allnightai', '.stop');
4607
+
4608
+ await fs.ensureDir(path.dirname(stopFilePath));
4609
+ await fs.writeFile(stopFilePath, `Stop requested at ${new Date().toISOString()}`);
4610
+
4611
+ spinner.text = 'Stop signal sent, waiting for process to exit...';
4612
+
4613
+ // Wait a moment for the process to detect the stop file
4614
+ await new Promise(resolve => setTimeout(resolve, 2000));
4615
+
4616
+ // Clean up auto mode status
4617
+ await stopAutoMode();
4618
+
4619
+ spinner.succeed('Autonomous mode stopped');
4620
+ console.log(chalk.gray('\n The running process should exit within a few seconds.'));
4621
+ } catch (error) {
4622
+ spinner.fail('Failed to stop autonomous mode');
4623
+ console.error(chalk.red('Error:'), error.message);
4624
+ process.exit(1);
4625
+ }
4626
+ }
4627
+
4628
+ async function status() {
4629
+ try {
4630
+ const autoStatus = await checkAutoModeStatus();
4631
+ const config = await getAutoConfig();
4632
+
4633
+ console.log(chalk.bold('\nAuto Mode Status\n'));
4634
+
4635
+ if (autoStatus.running) {
4636
+ console.log(chalk.green('ā—'), 'Running');
4637
+ console.log(chalk.gray(' IDE:'), chalk.cyan(config.ide || 'cursor'));
4638
+ console.log(chalk.gray(' Chat count:'), chalk.cyan(autoStatus.chatCount || 0));
4639
+
4640
+ if (config.neverStop) {
4641
+ console.log(chalk.gray(' Mode:'), chalk.cyan('Never stop'));
4642
+ } else if (config.maxChats) {
4643
+ console.log(chalk.gray(' Max chats:'), chalk.cyan(config.maxChats));
4644
+ const remaining = config.maxChats - (autoStatus.chatCount || 0);
4645
+ console.log(chalk.gray(' Remaining:'), chalk.cyan(remaining));
4646
+ }
4647
+ } else {
4648
+ console.log(chalk.gray('ā—‹'), 'Not running');
4649
+ }
4650
+
4651
+ console.log();
4652
+ } catch (error) {
4653
+ console.error(chalk.red('Error checking status:'), error.message);
4654
+ process.exit(1);
4655
+ }
4656
+ }
4657
+
4658
+ async function config(options) {
4659
+ try {
4660
+ const currentConfig = await getAutoConfig();
4661
+ const newConfig = { ...currentConfig };
4662
+
4663
+ if (options.maxChats !== undefined) {
4664
+ newConfig.maxChats = options.maxChats;
4665
+ }
4666
+
4667
+ if (options.neverStop !== undefined) {
4668
+ newConfig.neverStop = options.neverStop;
4669
+ }
4670
+
4671
+ await setAutoConfig(newConfig);
4672
+
4673
+ console.log(chalk.green('āœ“'), 'Auto mode configuration updated');
4674
+ console.log(chalk.gray('\nCurrent settings:'));
4675
+
4676
+ if (newConfig.neverStop) {
4677
+ console.log(chalk.gray(' Mode:'), chalk.cyan('Never stop'));
4678
+ } else if (newConfig.maxChats) {
4679
+ console.log(chalk.gray(' Max chats:'), chalk.cyan(newConfig.maxChats));
4680
+ }
4681
+ } catch (error) {
4682
+ console.error(chalk.red('Error updating configuration:'), error.message);
4683
+ process.exit(1);
4684
+ }
4685
+ }
4686
+
4687
+ module.exports = {
4688
+ start,
4689
+ stop,
4690
+ status,
4691
+ config
4692
+ };