vibecodingmachine-cli 1.0.5 → 1.0.7

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 (37) hide show
  1. package/.allnightai/REQUIREMENTS.md +11 -11
  2. package/.allnightai/temp/auto-status.json +6 -0
  3. package/.env +7 -0
  4. package/.eslintrc.js +16 -16
  5. package/README.md +85 -85
  6. package/bin/vibecodingmachine.js +274 -274
  7. package/jest.config.js +8 -8
  8. package/logs/audit/2025-11-07.jsonl +2 -2
  9. package/package.json +62 -62
  10. package/scripts/README.md +128 -128
  11. package/scripts/auto-start-wrapper.sh +92 -92
  12. package/scripts/postinstall.js +81 -81
  13. package/src/commands/auth.js +96 -96
  14. package/src/commands/auto-direct.js +1748 -1748
  15. package/src/commands/auto.js +4692 -4692
  16. package/src/commands/auto.js.bak +710 -710
  17. package/src/commands/ide.js +70 -70
  18. package/src/commands/repo.js +159 -159
  19. package/src/commands/requirements.js +161 -161
  20. package/src/commands/setup.js +91 -91
  21. package/src/commands/status.js +88 -88
  22. package/src/index.js +5 -5
  23. package/src/utils/auth.js +572 -577
  24. package/src/utils/auto-mode-ansi-ui.js +238 -238
  25. package/src/utils/auto-mode-simple-ui.js +161 -161
  26. package/src/utils/auto-mode-ui.js.bak.blessed +207 -207
  27. package/src/utils/auto-mode.js +65 -65
  28. package/src/utils/config.js +64 -64
  29. package/src/utils/interactive.js +3616 -3616
  30. package/src/utils/keyboard-handler.js +153 -152
  31. package/src/utils/logger.js +4 -4
  32. package/src/utils/persistent-header.js +116 -116
  33. package/src/utils/provider-registry.js +128 -128
  34. package/src/utils/status-card.js +120 -120
  35. package/src/utils/stdout-interceptor.js +127 -127
  36. package/tests/auto-mode.test.js +37 -37
  37. package/tests/config.test.js +34 -34
@@ -1,1748 +1,1748 @@
1
- /**
2
- * Direct LLM Auto Mode - Full implementation using DirectLLMManager
3
- * No IDE CLI tools - direct API calls to Ollama, Anthropic, Groq, or Bedrock
4
- * Includes proper status management, file changes, and requirement tracking
5
- */
6
-
7
- const chalk = require('chalk');
8
- const { DirectLLMManager } = require('vibecodingmachine-core');
9
- const { getRepoPath, getAutoConfig, setAutoConfig } = require('../utils/config');
10
- const { getRequirementsPath, readRequirements } = require('vibecodingmachine-core');
11
- const fs = require('fs-extra');
12
- const path = require('path');
13
- const { spawn } = require('child_process');
14
- const chokidar = require('chokidar');
15
- // Status management will use in-process tracking instead of external file
16
- const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
17
- const { getProviderPreferences, getProviderDefinition } = require('../utils/provider-registry');
18
-
19
- // CRITICAL: Shared ProviderManager instance to track rate limits across all function calls
20
- const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
21
- const sharedProviderManager = new ProviderManager();
22
-
23
- /**
24
- * Get timestamp for logging
25
- */
26
- function getTimestamp() {
27
- const now = new Date();
28
- return now.toLocaleTimeString('en-US', {
29
- hour: '2-digit',
30
- minute: '2-digit',
31
- hour12: false,
32
- timeZone: 'America/Denver'
33
- }) + ' MST';
34
- }
35
-
36
- /**
37
- * Strip ANSI escape codes from a string
38
- */
39
- function stripAnsi(str) {
40
- // eslint-disable-next-line no-control-regex
41
- return str.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
42
- }
43
-
44
- /**
45
- * Count visual width of emojis in a string (emojis take 2 terminal columns)
46
- * Uses string iterator to properly handle multi-code-point emojis
47
- */
48
- function countEmojiWidth(str) {
49
- // Remove ANSI codes first
50
- const cleaned = stripAnsi(str);
51
-
52
- let emojiCount = 0;
53
- // Use spread operator to properly split multi-code-point characters
54
- for (const char of cleaned) {
55
- const code = char.codePointAt(0);
56
- // Check if it's an emoji (takes 2 columns in terminal)
57
- if (
58
- (code >= 0x1F300 && code <= 0x1F9FF) || // Misc Symbols and Pictographs
59
- (code >= 0x2600 && code <= 0x26FF) || // Misc Symbols
60
- (code >= 0x2700 && code <= 0x27BF) || // Dingbats
61
- (code >= 0x1F000 && code <= 0x1F02F) || // Mahjong Tiles
62
- (code >= 0x1F0A0 && code <= 0x1F0FF) || // Playing Cards
63
- (code >= 0x1F100 && code <= 0x1F64F) || // Enclosed Characters
64
- (code >= 0x1F680 && code <= 0x1F6FF) || // Transport and Map
65
- (code >= 0x1F910 && code <= 0x1F96B) || // Supplemental Symbols
66
- (code >= 0x1F980 && code <= 0x1F9E0) // Supplemental Symbols
67
- ) {
68
- emojiCount++;
69
- }
70
- }
71
- return emojiCount;
72
- }
73
-
74
- /**
75
- * Get visual width of a string accounting for ANSI codes and emojis
76
- */
77
- function getVisualWidth(str) {
78
- const stripped = stripAnsi(str);
79
- const emojiCount = countEmojiWidth(str);
80
- // Count actual characters (using spread to handle multi-code-point correctly)
81
- const charCount = [...stripped].length;
82
- // Each emoji takes 2 visual columns, regular chars take 1
83
- return charCount + emojiCount;
84
- }
85
-
86
- /**
87
- * Pad string to visual width accounting for emojis and ANSI codes
88
- */
89
- function padToVisualWidth(str, targetWidth) {
90
- const visualWidth = getVisualWidth(str);
91
- const paddingNeeded = targetWidth - visualWidth;
92
- if (paddingNeeded <= 0) {
93
- return str;
94
- }
95
- return str + ' '.repeat(paddingNeeded);
96
- }
97
-
98
- function isRateLimitMessage(text) {
99
- if (!text) return false;
100
- const lower = text.toLowerCase();
101
- return lower.includes('rate limit') ||
102
- lower.includes('too many requests') ||
103
- lower.includes('429') ||
104
- lower.includes('weekly limit') ||
105
- lower.includes('daily limit') ||
106
- lower.includes('limit reached');
107
- }
108
-
109
- function sleep(ms) {
110
- return new Promise(resolve => setTimeout(resolve, ms));
111
- }
112
-
113
- /**
114
- * Print purple status card with progress indicators
115
- */
116
- /**
117
- * Update status section in requirements file
118
- * @param {string} repoPath - Repository path
119
- * @param {string} status - Status to set (PREPARE, ACT, CLEAN UP, VERIFY, DONE)
120
- */
121
- async function updateRequirementsStatus(repoPath, status) {
122
- try {
123
- const reqPath = await getRequirementsPath(repoPath);
124
- if (!reqPath || !await fs.pathExists(reqPath)) {
125
- return;
126
- }
127
-
128
- const content = await fs.readFile(reqPath, 'utf-8');
129
- const lines = content.split('\n');
130
- let inStatusSection = false;
131
- let statusLineIndex = -1;
132
-
133
- // Find the status section and the line with the status
134
- for (let i = 0; i < lines.length; i++) {
135
- const line = lines[i];
136
-
137
- if (line.includes('🚦 Current Status')) {
138
- inStatusSection = true;
139
- continue;
140
- }
141
-
142
- if (inStatusSection && line.startsWith('##') && !line.startsWith('###')) {
143
- // End of status section, status line not found
144
- break;
145
- }
146
-
147
- if (inStatusSection && line.trim().match(/^(PREPARE|CREATE|ACT|CLEAN UP|VERIFY|DONE)$/)) {
148
- statusLineIndex = i;
149
- break;
150
- }
151
- }
152
-
153
- // Update or add the status line
154
- if (statusLineIndex >= 0) {
155
- // Replace existing status
156
- lines[statusLineIndex] = status;
157
- } else if (inStatusSection) {
158
- // Add status after the section header
159
- for (let i = 0; i < lines.length; i++) {
160
- if (lines[i].includes('🚦 Current Status')) {
161
- lines.splice(i + 1, 0, status);
162
- break;
163
- }
164
- }
165
- } else {
166
- // Status section doesn't exist - find the requirement and add it
167
- for (let i = 0; i < lines.length; i++) {
168
- if (lines[i].startsWith('### ') && !lines[i].includes('🚦 Current Status')) {
169
- // Found a requirement header, add status section after it
170
- lines.splice(i + 1, 0, '', '#### 🚦 Current Status', status);
171
- break;
172
- }
173
- }
174
- }
175
-
176
- await fs.writeFile(reqPath, lines.join('\n'));
177
- } catch (error) {
178
- // Silently fail - don't break execution if status update fails
179
- console.error(chalk.gray(` Warning: Could not update status in requirements file: ${error.message}`));
180
- }
181
- }
182
-
183
- /**
184
- * Print purple status card with progress indicators
185
- * Makes current stage very prominent with bright white text
186
- * Uses full terminal width
187
- */
188
- function printStatusCard(currentTitle, currentStatus) {
189
- const stages = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
190
- const stageMap = {
191
- 'PREPARE': 0,
192
- 'ACT': 1,
193
- 'CLEAN UP': 2,
194
- 'VERIFY': 3,
195
- 'DONE': 4
196
- };
197
-
198
- const currentIndex = stageMap[currentStatus] || 0;
199
-
200
- // Build workflow line with visual prominence for current stage
201
- const stageParts = stages.map((stage, idx) => {
202
- if (idx < currentIndex) {
203
- // Completed stages - grey with checkmark
204
- return chalk.grey(`āœ… ${stage}`);
205
- } else if (idx === currentIndex) {
206
- // CURRENT stage - BRIGHT WHITE with hammer
207
- return chalk.bold.white(`šŸ”Ø ${stage}`);
208
- } else {
209
- // Future stages - grey with hourglass
210
- return chalk.grey(`ā³ ${stage}`);
211
- }
212
- });
213
-
214
- const workflowLine = stageParts.join(chalk.grey(' → '));
215
-
216
- // Get terminal width, default to 100 if not available
217
- const terminalWidth = process.stdout.columns || 100;
218
- const boxWidth = Math.max(terminalWidth - 4, 80); // Leave 4 chars margin, minimum 80
219
-
220
- // Truncate title if needed to fit in box
221
- const maxTitleWidth = boxWidth - 20; // Leave room for "šŸŽÆ Working on: "
222
- const titleShort = currentTitle?.substring(0, maxTitleWidth) + (currentTitle?.length > maxTitleWidth ? '...' : '');
223
- const titleLine = chalk.cyan(`šŸŽÆ Working on: `) + chalk.white(titleShort);
224
-
225
- console.log(chalk.magenta('\nā•­' + '─'.repeat(boxWidth) + 'ā•®'));
226
- console.log(chalk.magenta('│') + padToVisualWidth(' ' + workflowLine, boxWidth) + chalk.magenta('│'));
227
- console.log(chalk.magenta('│') + ' '.repeat(boxWidth) + chalk.magenta('│'));
228
- console.log(chalk.magenta('│') + padToVisualWidth(' ' + titleLine, boxWidth) + chalk.magenta('│'));
229
- console.log(chalk.magenta('ā•°' + '─'.repeat(boxWidth) + '╯\n'));
230
- }
231
-
232
- /**
233
- * Get current requirement from REQUIREMENTS file
234
- */
235
- async function getCurrentRequirement(repoPath) {
236
- try {
237
- const reqPath = await getRequirementsPath(repoPath);
238
- if (!reqPath || !await fs.pathExists(reqPath)) {
239
- return null;
240
- }
241
-
242
- const content = await fs.readFile(reqPath, 'utf8');
243
-
244
- // Extract first TODO requirement (new header format)
245
- const lines = content.split('\n');
246
- let inTodoSection = false;
247
-
248
- for (let i = 0; i < lines.length; i++) {
249
- const line = lines[i].trim();
250
-
251
- // Check if we're in the TODO section
252
- if (line.includes('## ā³ Requirements not yet completed') ||
253
- line.includes('Requirements not yet completed')) {
254
- inTodoSection = true;
255
- continue;
256
- }
257
-
258
- // If we hit another section header, stop looking
259
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
260
- break;
261
- }
262
-
263
- // If we're in TODO section and find a requirement header (###)
264
- if (inTodoSection && line.startsWith('###')) {
265
- const title = line.replace(/^###\s*/, '').trim();
266
- // Skip empty titles
267
- if (title && title.length > 0) {
268
- // Read package and description (optional)
269
- let package = null;
270
- let description = '';
271
- let j = i + 1;
272
-
273
- // Read next few lines for package and description
274
- while (j < lines.length && j < i + 20) {
275
- const nextLine = lines[j].trim();
276
- // Stop if we hit another requirement or section
277
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
278
- break;
279
- }
280
- // Check for PACKAGE line
281
- if (nextLine.startsWith('PACKAGE:')) {
282
- package = nextLine.replace(/^PACKAGE:\s*/, '').trim();
283
- } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
284
- // Description line (not empty, not package)
285
- if (description) {
286
- description += '\n' + nextLine;
287
- } else {
288
- description = nextLine;
289
- }
290
- }
291
- j++;
292
- }
293
-
294
- return {
295
- text: title,
296
- fullLine: lines[i],
297
- package: package,
298
- description: description
299
- };
300
- }
301
- }
302
- }
303
-
304
- return null;
305
- } catch (err) {
306
- console.error('Error reading requirement:', err.message);
307
- return null;
308
- }
309
- }
310
-
311
- /**
312
- * Move requirement from TODO to TO VERIFY BY HUMAN
313
- */
314
- async function moveRequirementToVerify(repoPath, requirementText) {
315
- try {
316
- const reqPath = await getRequirementsPath(repoPath);
317
- if (!reqPath || !await fs.pathExists(reqPath)) {
318
- return false;
319
- }
320
-
321
- const content = await fs.readFile(reqPath, 'utf8');
322
- const lines = content.split('\n');
323
-
324
- // Find the requirement by its title (in ### header format)
325
- const snippet = requirementText.substring(0, 80);
326
- let requirementStartIndex = -1;
327
- let requirementEndIndex = -1;
328
-
329
- for (let i = 0; i < lines.length; i++) {
330
- const line = lines[i].trim();
331
- // Check if this is the requirement header
332
- if (line.startsWith('###')) {
333
- const title = line.replace(/^###\s*/, '').trim();
334
- if (title && title.includes(snippet)) {
335
- requirementStartIndex = i;
336
- // Find the end of this requirement (next ### or ## header)
337
- for (let j = i + 1; j < lines.length; j++) {
338
- const nextLine = lines[j].trim();
339
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
340
- requirementEndIndex = j;
341
- break;
342
- }
343
- }
344
- if (requirementEndIndex === -1) {
345
- requirementEndIndex = lines.length;
346
- }
347
- break;
348
- }
349
- }
350
- }
351
-
352
- if (requirementStartIndex === -1) {
353
- console.log(chalk.yellow('āš ļø Could not find requirement in requirements file'));
354
- return false;
355
- }
356
-
357
- // Extract the entire requirement block
358
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
359
-
360
- // Remove the requirement from its current location
361
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
362
-
363
- // Check if the requirement is in the Recycled section
364
- const recycledIndex = lines.findIndex(line => line.trim() === '## šŸ“¦ RECYCLED');
365
- if (recycledIndex !== -1) {
366
- const recycledBlock = lines.slice(recycledIndex + 1);
367
- const requirementIndexInRecycled = recycledBlock.findIndex(line => line.trim().startsWith('###') && line.trim().includes(snippet));
368
- if (requirementIndexInRecycled !== -1) {
369
- lines.splice(recycledIndex + 1 + requirementIndexInRecycled, 1);
370
- }
371
- }
372
-
373
- // Find or create VERIFIED section
374
- const headingVariants = [
375
- '## šŸ“ VERIFIED',
376
- '## VERIFIED',
377
- '## āœ… VERIFIED',
378
- '## āœ… VERIFIED BY HUMAN'
379
- ];
380
-
381
- let verifyIndex = lines.findIndex(line => headingVariants.includes(line.trim()));
382
- if (verifyIndex === -1) {
383
- const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ā“'));
384
- const headingLine = '## šŸ“ VERIFIED';
385
- const insertionIndex = manualFeedbackIndex === -1 ? lines.length : manualFeedbackIndex;
386
- const block = [];
387
- if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
388
- block.push('');
389
- }
390
- block.push(headingLine, '');
391
- lines.splice(insertionIndex, 0, ...block);
392
- verifyIndex = lines.findIndex(line => line.trim() === headingLine);
393
- }
394
-
395
- // Insert the requirement block at the top of the VERIFIED section
396
- lines.splice(verifyIndex + 1, 0, ...requirementBlock);
397
- // Add blank line after if needed
398
- if (verifyIndex + 1 + requirementBlock.length < lines.length && lines[verifyIndex + 1 + requirementBlock.length].trim() !== '') {
399
- lines.splice(verifyIndex + 1 + requirementBlock.length, 0, '');
400
- }
401
-
402
- await fs.writeFile(reqPath, lines.join('\n'));
403
- console.log(`Moved requirement to VERIFIED: ${requirementText}`);
404
- return true;
405
- } catch (err) {
406
- console.error('Error moving requirement:', err.message);
407
- return false;
408
- }
409
- }
410
-
411
-
412
- /**
413
- * Move requirement to recycled section
414
- */
415
- async function moveRequirementToRecycle(repoPath, requirementText) {
416
- try {
417
- const reqPath = await getRequirementsPath(repoPath);
418
- if (!reqPath || !await fs.pathExists(reqPath)) {
419
- return false;
420
- }
421
-
422
- let content = await fs.readFile(reqPath, 'utf8');
423
-
424
- // Find and remove from any section
425
- const lines = content.split('\n');
426
- let requirementIndex = -1;
427
-
428
- for (let i = 0; i < lines.length; i++) {
429
- if (lines[i].includes(requirementText.substring(0, 50))) {
430
- requirementIndex = i;
431
- break;
432
- }
433
- }
434
-
435
- if (requirementIndex === -1) {
436
- console.log(chalk.yellow('āš ļø Could not find requirement'));
437
- return false;
438
- }
439
-
440
- // Remove from any section
441
- const requirementLine = lines[requirementIndex];
442
- lines.splice(requirementIndex, 1);
443
-
444
- // Add to Recycled section (after "## ā™»ļø Recycled")
445
- let recycledIndex = -1;
446
- for (let i = 0; i < lines.length; i++) {
447
- if (lines[i].includes('## ā™»ļø Recycled')) {
448
- recycledIndex = i; // Move to the line of the header
449
- break;
450
- }
451
- }
452
-
453
- if (recycledIndex === -1) {
454
- lines.push('## ā™»ļø Recycled');
455
- recycledIndex = lines.length - 1;
456
- }
457
-
458
- // Add timestamp and insert at TOP of Recycled list
459
- const timestamp = new Date().toISOString().split('T')[0];
460
- lines.splice(recycledIndex + 1, 0, `- ${timestamp}: ${requirementLine.replace(/^- /, '')}`);
461
-
462
- // Save
463
- await fs.writeFile(reqPath, lines.join('\n'));
464
- return true;
465
- } catch (err) {
466
- console.error('Error moving requirement:', err.message);
467
- return false;
468
- }
469
- }
470
-
471
- /**
472
- * Get all available provider configurations
473
- */
474
- async function getAllAvailableProviders() {
475
- const config = await getAutoConfig();
476
- const prefs = await getProviderPreferences();
477
- const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
478
- const providers = [];
479
-
480
- const groqKey = process.env.GROQ_API_KEY || config.groqApiKey;
481
- const anthropicKey = process.env.ANTHROPIC_API_KEY || config.anthropicApiKey;
482
- const awsRegion = process.env.AWS_REGION || config.awsRegion;
483
- const awsAccessKey = process.env.AWS_ACCESS_KEY_ID || config.awsAccessKeyId;
484
- const awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY || config.awsSecretAccessKey;
485
- const claudeCodeAvailable = await llm.isClaudeCodeAvailable();
486
- const ollamaAvailable = await llm.isOllamaAvailable();
487
- let ollamaModels = [];
488
- if (ollamaAvailable) {
489
- ollamaModels = await llm.getOllamaModels();
490
- }
491
-
492
- for (const providerId of prefs.order) {
493
- const def = getProviderDefinition(providerId);
494
- if (!def) continue;
495
-
496
- const enabled = prefs.enabled[providerId] !== false;
497
- const base = {
498
- provider: providerId,
499
- displayName: def.name,
500
- type: def.type,
501
- category: def.category,
502
- enabled,
503
- estimatedSpeed: def.estimatedSpeed,
504
- ide: def.ide,
505
- maxChats: def.type === 'ide' ? 1 : undefined
506
- };
507
-
508
- switch (providerId) {
509
- case 'groq': {
510
- if (!groqKey) continue;
511
- const model = config.groqModel || def.defaultModel;
512
- providers.push({
513
- ...base,
514
- model,
515
- apiKey: groqKey,
516
- displayName: `${def.name} (${model})`
517
- });
518
- break;
519
- }
520
- case 'anthropic': {
521
- if (!anthropicKey) continue;
522
- const model = config.anthropicModel || def.defaultModel;
523
- providers.push({
524
- ...base,
525
- model,
526
- apiKey: anthropicKey,
527
- displayName: `${def.name} (${model})`
528
- });
529
- break;
530
- }
531
- case 'bedrock': {
532
- if (!awsRegion || !awsAccessKey || !awsSecretKey) continue;
533
- providers.push({
534
- ...base,
535
- model: def.defaultModel,
536
- region: awsRegion,
537
- accessKeyId: awsAccessKey,
538
- secretAccessKey: awsSecretKey
539
- });
540
- break;
541
- }
542
- case 'claude-code': {
543
- if (!claudeCodeAvailable) continue;
544
- providers.push({
545
- ...base,
546
- model: def.defaultModel
547
- });
548
- break;
549
- }
550
- case 'cursor':
551
- case 'windsurf':
552
- case 'antigravity': {
553
- providers.push(base);
554
- break;
555
- }
556
- case 'ollama': {
557
- if (!ollamaAvailable || ollamaModels.length === 0) continue;
558
- const preferredModel = config.llmModel && config.llmModel.includes('ollama/')
559
- ? config.llmModel.split('/')[1]
560
- : config.llmModel || config.aiderModel;
561
- let bestModel = preferredModel && ollamaModels.includes(preferredModel)
562
- ? preferredModel
563
- : ollamaModels.includes(def.defaultModel)
564
- ? def.defaultModel
565
- : ollamaModels[0];
566
- providers.push({
567
- ...base,
568
- model: bestModel,
569
- displayName: `${def.name} (${bestModel})`
570
- });
571
- break;
572
- }
573
- default:
574
- break;
575
- }
576
- }
577
-
578
- return providers;
579
- }
580
-
581
- /**
582
- * Get provider configuration with automatic failover
583
- */
584
- async function getProviderConfig() {
585
- const config = await getAutoConfig();
586
- const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
587
- const providers = await getAllAvailableProviders();
588
- const savedAgent = config.agent || config.ide;
589
-
590
- if (providers.length === 0) {
591
- return { status: 'no_providers', providers: [] };
592
- }
593
-
594
- const enabledProviders = providers.filter(p => p.enabled);
595
- const disabledProviders = providers.filter(p => !p.enabled);
596
-
597
- if (enabledProviders.length === 0) {
598
- return { status: 'no_enabled', disabledProviders };
599
- }
600
-
601
- const availableProviders = enabledProviders.filter(p => {
602
- return !providerManager.isRateLimited(p.provider, p.model);
603
- });
604
-
605
- let selection = null;
606
- if (savedAgent) {
607
- selection = availableProviders.find(p => p.provider === savedAgent);
608
- }
609
- if (!selection) {
610
- selection = availableProviders[0] || null;
611
- }
612
-
613
- if (selection) {
614
- const perfKey = `${selection.provider}:${selection.model || ''}`;
615
- const avgSpeed = providerManager.performance[perfKey]?.avgSpeed;
616
- const speedInfo = avgSpeed ? ` (avg: ${(avgSpeed / 1000).toFixed(1)}s)` : '';
617
- console.log(chalk.green(`āœ“ Selected: ${selection.displayName}${speedInfo}`));
618
- return { status: 'ok', provider: selection, disabledProviders };
619
- }
620
-
621
- const waits = enabledProviders
622
- .map(p => providerManager.getTimeUntilReset(p.provider, p.model))
623
- .filter(Boolean);
624
- const nextResetMs = waits.length ? Math.min(...waits) : null;
625
-
626
- return {
627
- status: 'all_rate_limited',
628
- enabledProviders,
629
- disabledProviders,
630
- nextResetMs
631
- };
632
- }
633
-
634
- async function acquireProviderConfig() {
635
- while (true) {
636
- const selection = await getProviderConfig();
637
- if (selection.status === 'ok') {
638
- return selection.provider;
639
- }
640
-
641
- if (selection.status === 'no_providers') {
642
- console.log(chalk.red('\nāœ— No providers available. Configure API keys or IDE agents first.\n'));
643
- return null;
644
- }
645
-
646
- if (selection.status === 'no_enabled') {
647
- console.log(chalk.red('\nāœ— All providers are disabled. Enable at least one provider in the Agent menu.\n'));
648
- return null;
649
- }
650
-
651
- if (selection.status === 'all_rate_limited') {
652
- console.log(chalk.yellow('\nāš ļø All enabled providers are currently rate limited.'));
653
- if (selection.disabledProviders && selection.disabledProviders.length > 0) {
654
- console.log(chalk.gray(' Tip: Enable additional providers in the Agent menu for more fallbacks.'));
655
- }
656
- const waitMs = selection.nextResetMs || 60000;
657
- const waitMinutes = Math.max(1, Math.ceil(waitMs / 60000));
658
- console.log(chalk.gray(` Waiting for rate limits to reset (~${waitMinutes}m)...\n`));
659
- await sleep(Math.min(waitMs, 60000));
660
- continue;
661
- }
662
-
663
- return null;
664
- }
665
- }
666
-
667
- /**
668
- * Parse search/replace blocks from LLM response
669
- */
670
- function parseSearchReplaceBlocks(response) {
671
- const changes = [];
672
-
673
- // Match FILE: path SEARCH: ``` old ``` REPLACE: ``` new ``` format
674
- const blockRegex = /FILE:\s*(.+?)\nSEARCH:\s*```(?:[a-z]*)\n([\s\S]+?)```\s*REPLACE:\s*```(?:[a-z]*)\n([\s\S]+?)```/g;
675
-
676
- let match;
677
- while ((match = blockRegex.exec(response)) !== null) {
678
- let filePath = match[1].trim();
679
- const searchText = match[2];
680
- const replaceText = match[3];
681
-
682
- // Clean up file path - remove "---" prefix if present
683
- filePath = filePath.replace(/^---\s*/, '').trim();
684
-
685
- changes.push({
686
- file: filePath,
687
- search: searchText,
688
- replace: replaceText
689
- });
690
- }
691
-
692
- return changes;
693
- }
694
-
695
- /**
696
- * Normalize whitespace for comparison (convert all whitespace to single spaces)
697
- */
698
- function normalizeWhitespace(str) {
699
- return str.replace(/\s+/g, ' ').trim();
700
- }
701
-
702
- /**
703
- * Extract key identifiers from code (variable names, function names, strings)
704
- */
705
- function extractIdentifiers(code) {
706
- const identifiers = new Set();
707
-
708
- // Extract quoted strings
709
- const stringMatches = code.match(/'([^']+)'|"([^"]+)"/g);
710
- if (stringMatches) {
711
- stringMatches.forEach(match => {
712
- const str = match.slice(1, -1); // Remove quotes
713
- if (str.length > 3) { // Only meaningful strings
714
- identifiers.add(str);
715
- }
716
- });
717
- }
718
-
719
- // Extract variable/function names (words followed by : or =)
720
- const nameMatches = code.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[:=]/g);
721
- if (nameMatches) {
722
- nameMatches.forEach(match => {
723
- const name = match.replace(/[:=].*$/, '').trim();
724
- if (name.length > 2) {
725
- identifiers.add(name);
726
- }
727
- });
728
- }
729
-
730
- // Extract common patterns like 'type:', 'name:', 'value:'
731
- const patternMatches = code.match(/(type|name|value|file|path|function|const|let|var)\s*:/gi);
732
- if (patternMatches) {
733
- patternMatches.forEach(match => {
734
- identifiers.add(match.toLowerCase().replace(/\s*:/, ''));
735
- });
736
- }
737
-
738
- return Array.from(identifiers);
739
- }
740
-
741
- /**
742
- * Extract structural pattern from code (ignoring values)
743
- */
744
- function extractPattern(code) {
745
- // Replace strings with placeholders
746
- let pattern = code.replace(/'[^']+'|"[^"]+"/g, '"..."');
747
- // Replace numbers with placeholders
748
- pattern = pattern.replace(/\b\d+\b/g, 'N');
749
- // Normalize whitespace
750
- pattern = normalizeWhitespace(pattern);
751
- return pattern;
752
- }
753
-
754
- /**
755
- * Apply a search/replace change to a file with fuzzy matching fallback
756
- */
757
- async function applyFileChange(change, repoPath) {
758
- try {
759
- const fullPath = path.join(repoPath, change.file);
760
-
761
- // Check if file exists
762
- if (!await fs.pathExists(fullPath)) {
763
- return { success: false, error: `File not found: ${change.file}` };
764
- }
765
-
766
- // Read file
767
- let content = await fs.readFile(fullPath, 'utf8');
768
-
769
- // Try exact match first
770
- console.log(chalk.gray(` šŸ” Trying exact match...`));
771
- if (content.includes(change.search)) {
772
- const newContent = content.replace(change.search, change.replace);
773
- await fs.writeFile(fullPath, newContent, 'utf8');
774
- console.log(chalk.green(` āœ“ Exact match found`));
775
- return { success: true, method: 'exact' };
776
- }
777
- console.log(chalk.gray(` āœ— Exact match failed`));
778
-
779
- // Try with normalized whitespace (fuzzy match)
780
- console.log(chalk.gray(` šŸ” Trying fuzzy match (normalized whitespace)...`));
781
- const normalizedSearch = normalizeWhitespace(change.search);
782
- const lines = content.split('\n');
783
- const searchLines = change.search.split('\n');
784
-
785
- console.log(chalk.gray(` - Search block: ${searchLines.length} lines`));
786
- console.log(chalk.gray(` - File total: ${lines.length} lines`));
787
-
788
- // Extract key identifiers from search text (function names, variable names, strings)
789
- const searchIdentifiers = extractIdentifiers(change.search);
790
-
791
- // Try multiple window sizes (±5 lines) to account for LLM not including enough context
792
- for (let sizeOffset = 0; sizeOffset <= 10; sizeOffset++) {
793
- const windowSize = searchLines.length + sizeOffset;
794
- if (sizeOffset > 0) {
795
- console.log(chalk.gray(` šŸ” Trying window size +${sizeOffset} (${windowSize} lines)...`));
796
- }
797
-
798
- // Try to find a sequence of lines that matches when normalized
799
- for (let i = 0; i < lines.length; i++) {
800
- if (i + windowSize > lines.length) break;
801
-
802
- const window = lines.slice(i, i + windowSize).join('\n');
803
- const normalizedWindow = normalizeWhitespace(window);
804
-
805
- // Check if normalized versions match (or if normalized window contains normalized search)
806
- if (normalizedWindow === normalizedSearch || normalizedWindow.includes(normalizedSearch)) {
807
- // Found a match! Replace this section
808
- const beforeLines = lines.slice(0, i);
809
- const afterLines = lines.slice(i + windowSize);
810
- const replaceLines = change.replace.split('\n');
811
-
812
- const newLines = [...beforeLines, ...replaceLines, ...afterLines];
813
- const newContent = newLines.join('\n');
814
-
815
- await fs.writeFile(fullPath, newContent, 'utf8');
816
- console.log(chalk.green(` āœ“ Fuzzy match found at line ${i + 1} (window size: ${windowSize})`));
817
- return { success: true, method: 'fuzzy', matchedAt: i + 1, windowSize };
818
- }
819
-
820
- // Also try semantic matching - check if key identifiers match even if some values differ
821
- if (searchIdentifiers.length > 0) {
822
- const windowIdentifiers = extractIdentifiers(window);
823
- const matchingIdentifiers = searchIdentifiers.filter(id => windowIdentifiers.includes(id));
824
- // If 80% of identifiers match, consider it a potential match
825
- if (matchingIdentifiers.length >= searchIdentifiers.length * 0.8) {
826
- // Check if the structure is similar (same number of lines, similar patterns)
827
- const searchPattern = extractPattern(change.search);
828
- const windowPattern = extractPattern(window);
829
- if (searchPattern === windowPattern) {
830
- // Found a semantic match! Replace this section
831
- const beforeLines = lines.slice(0, i);
832
- const afterLines = lines.slice(i + windowSize);
833
- const replaceLines = change.replace.split('\n');
834
-
835
- const newLines = [...beforeLines, ...replaceLines, ...afterLines];
836
- const newContent = newLines.join('\n');
837
-
838
- await fs.writeFile(fullPath, newContent, 'utf8');
839
- console.log(chalk.green(` āœ“ Semantic match found at line ${i + 1} (window size: ${windowSize}, ${matchingIdentifiers.length}/${searchIdentifiers.length} identifiers)`));
840
- return { success: true, method: 'semantic', matchedAt: i + 1, windowSize };
841
- }
842
- }
843
- }
844
- }
845
- }
846
- console.log(chalk.red(` āœ— No match found (tried exact + fuzzy with multiple window sizes)`));
847
-
848
- return {
849
- success: false,
850
- error: `Search text not found in ${change.file} (tried exact, fuzzy, and semantic matching with windows ${searchLines.length}-${searchLines.length + 10} lines)`
851
- };
852
-
853
- } catch (error) {
854
- return { success: false, error: error.message };
855
- }
856
- }
857
-
858
- /**
859
- * Find relevant files based on requirement
860
- */
861
- async function findRelevantFiles(requirement, repoPath) {
862
- const relevantFiles = [];
863
-
864
- try {
865
- const reqLower = requirement.toLowerCase();
866
-
867
- // Map keywords to specific files
868
- if (reqLower.includes('completed') && reqLower.includes('verify')) {
869
- // This is about the auto mode moving requirements
870
- relevantFiles.push('packages/cli/src/commands/auto-direct.js');
871
- } else if (reqLower.includes('requirements page') || reqLower.includes('requirements:')) {
872
- // This is about the requirements menu/page
873
- relevantFiles.push('packages/cli/src/utils/interactive.js');
874
- } else if (reqLower.includes('main screen') || reqLower.includes('menu')) {
875
- // This is about the main menu
876
- relevantFiles.push('packages/cli/src/utils/interactive.js');
877
- } else {
878
- // Default: check both files
879
- relevantFiles.push('packages/cli/src/commands/auto-direct.js');
880
- relevantFiles.push('packages/cli/src/utils/interactive.js');
881
- }
882
- } catch (error) {
883
- console.log(chalk.yellow(`āš ļø Error finding files: ${error.message}`));
884
- }
885
-
886
- return relevantFiles;
887
- }
888
-
889
- /**
890
- * Read file snippets to give LLM context
891
- */
892
- async function readFileSnippets(files, repoPath, requirement) {
893
- const snippets = [];
894
-
895
- for (const file of files) {
896
- try {
897
- const fullPath = path.join(repoPath, file);
898
- if (await fs.pathExists(fullPath)) {
899
- const content = await fs.readFile(fullPath, 'utf8');
900
- const lines = content.split('\n');
901
-
902
- let startLine = -1;
903
- let endLine = -1;
904
-
905
- // For auto-direct.js, find the moveRequirementToVerify function
906
- if (file.includes('auto-direct.js')) {
907
- for (let i = 0; i < lines.length; i++) {
908
- const line = lines[i];
909
- if (line.includes('async function moveRequirementToVerify')) {
910
- startLine = i;
911
- // Find the end of the function
912
- let braceCount = 0;
913
- let foundStart = false;
914
- for (let j = i; j < lines.length; j++) {
915
- const l = lines[j];
916
- // Count braces to find function end
917
- for (const char of l) {
918
- if (char === '{') {
919
- braceCount++;
920
- foundStart = true;
921
- } else if (char === '}') {
922
- braceCount--;
923
- if (foundStart && braceCount === 0) {
924
- endLine = j + 1;
925
- break;
926
- }
927
- }
928
- }
929
- if (endLine > 0) break;
930
- }
931
- break;
932
- }
933
- }
934
- }
935
-
936
- // For interactive.js, search based on requirement keywords
937
- if (file.includes('interactive.js')) {
938
- const reqLower = requirement.toLowerCase();
939
-
940
- // Search for specific sections based on requirement
941
- if (reqLower.includes('current agent') || (reqLower.includes('└─') && reqLower.includes('current agent'))) {
942
- // Find the Current Agent display code (with or without rate limit)
943
- // First, try to find the exact "└─ Current Agent" pattern
944
- for (let i = 0; i < lines.length; i++) {
945
- // Look for the exact pattern with tree character
946
- if (lines[i].includes('└─') && lines[i].includes('Current Agent')) {
947
- startLine = Math.max(0, i - 15);
948
- endLine = Math.min(lines.length, i + 20);
949
- break;
950
- }
951
- }
952
- // If not found, look for "Current Agent" in menu items
953
- if (startLine === -1) {
954
- for (let i = 0; i < lines.length; i++) {
955
- if (lines[i].includes('Current Agent') ||
956
- (lines[i].includes('currentAgent') && lines[i].includes('items.push'))) {
957
- startLine = Math.max(0, i - 15);
958
- endLine = Math.min(lines.length, i + 20);
959
- break;
960
- }
961
- }
962
- }
963
- // If still not found, look for the setting item with Current Agent
964
- if (startLine === -1) {
965
- for (let i = 0; i < lines.length; i++) {
966
- if (lines[i].includes('setting:current-agent') ||
967
- (lines[i].includes('Current Agent') && lines[i].includes('type:') && lines[i].includes('setting'))) {
968
- startLine = Math.max(0, i - 10);
969
- endLine = Math.min(lines.length, i + 15);
970
- break;
971
- }
972
- }
973
- }
974
- } else if (reqLower.includes('remove') || reqLower.includes('delete')) {
975
- // Find the delete/remove confirmation code
976
- for (let i = 0; i < lines.length; i++) {
977
- // Look for confirmAction with 'Delete' or 'Are you sure'
978
- if ((lines[i].includes('confirmAction') && lines[i].includes('Delete')) ||
979
- (lines[i].includes('confirmDelete') && (i > 0 && lines[i - 5] && lines[i - 5].includes("'delete'")))) {
980
- startLine = Math.max(0, i - 10);
981
- endLine = Math.min(lines.length, i + 20);
982
- break;
983
- }
984
- }
985
- } else if (reqLower.includes('submenu') || reqLower.includes('menu')) {
986
- // Find the showRequirementActions function
987
- for (let i = 0; i < lines.length; i++) {
988
- if (lines[i].includes('async function showRequirementActions')) {
989
- startLine = i;
990
- endLine = Math.min(lines.length, i + 80);
991
- break;
992
- }
993
- }
994
- } else if (reqLower.includes('next todo requirement') || reqLower.includes('next requirement') || (reqLower.includes('requirement') && reqLower.includes('indent'))) {
995
- // Find the "Next TODO Requirement" section
996
- for (let i = 0; i < lines.length; i++) {
997
- const line = lines[i];
998
- if (line.includes('Next TODO Requirement') || line.includes('Next Requirement') || (line.includes('nextReqText') && line.includes('items.push'))) {
999
- // Get more context - look backwards for requirementsText and forwards for the items.push
1000
- startLine = Math.max(0, i - 30);
1001
- // Look for the items.push that contains Next TODO Requirement
1002
- for (let j = i; j < Math.min(lines.length, i + 20); j++) {
1003
- if (lines[j].includes('items.push') && (lines[j].includes('Next TODO Requirement') || lines[j].includes('Next Requirement') || (j > 0 && (lines[j - 1].includes('Next TODO Requirement') || lines[j - 1].includes('Next Requirement'))))) {
1004
- endLine = Math.min(lines.length, j + 10);
1005
- break;
1006
- }
1007
- }
1008
- if (endLine === -1) {
1009
- endLine = Math.min(lines.length, i + 60);
1010
- }
1011
- break;
1012
- }
1013
- }
1014
- // If not found, fall back to requirements section
1015
- if (startLine === -1) {
1016
- for (let i = 0; i < lines.length; i++) {
1017
- const line = lines[i];
1018
- if (line.includes('let requirementsText') || line.includes('requirementsText')) {
1019
- startLine = Math.max(0, i - 10);
1020
- endLine = Math.min(lines.length, i + 80);
1021
- break;
1022
- }
1023
- }
1024
- }
1025
- } else {
1026
- // Default: find menu/requirements section
1027
- for (let i = 0; i < lines.length; i++) {
1028
- const line = lines[i];
1029
- if (line.includes('let requirementsText') || line.includes('requirementsText')) {
1030
- startLine = Math.max(0, i - 10);
1031
- endLine = Math.min(lines.length, i + 80);
1032
- break;
1033
- }
1034
- }
1035
- }
1036
- }
1037
-
1038
- if (startLine >= 0 && endLine > startLine) {
1039
- const snippet = lines.slice(startLine, endLine).join('\n');
1040
- console.log(chalk.gray(` Found snippet at lines ${startLine + 1}-${endLine + 1}`));
1041
- snippets.push({ file, snippet, startLine: startLine + 1, endLine: endLine + 1 });
1042
- } else {
1043
- console.log(chalk.yellow(` āš ļø Could not find relevant section in ${file}`));
1044
- }
1045
- }
1046
- } catch (error) {
1047
- console.log(chalk.yellow(`āš ļø Could not read ${file}: ${error.message}`));
1048
- }
1049
- }
1050
-
1051
- return snippets;
1052
- }
1053
-
1054
- async function runIdeProviderIteration(providerConfig, repoPath) {
1055
- return new Promise((resolve) => {
1056
- console.log(chalk.cyan(`āš™ļø Launching ${providerConfig.displayName} fallback (auto:start)...\n`));
1057
-
1058
- const args = [CLI_ENTRY_POINT, 'auto:start', '--ide', providerConfig.ide || providerConfig.provider, '--max-chats', String(providerConfig.maxChats || 1)];
1059
- const child = spawn(process.execPath, args, {
1060
- cwd: repoPath,
1061
- env: process.env,
1062
- stdio: ['inherit', 'pipe', 'pipe']
1063
- });
1064
-
1065
- let combinedOutput = '';
1066
-
1067
- child.stdout.on('data', (data) => {
1068
- const text = data.toString();
1069
- combinedOutput += text;
1070
- process.stdout.write(text);
1071
- });
1072
-
1073
- child.stderr.on('data', (data) => {
1074
- const text = data.toString();
1075
- combinedOutput += text;
1076
- process.stderr.write(text);
1077
- });
1078
-
1079
- child.on('error', (error) => {
1080
- resolve({
1081
- success: false,
1082
- error: `Failed to start ${providerConfig.displayName}: ${error.message}`,
1083
- output: combinedOutput
1084
- });
1085
- });
1086
-
1087
- child.on('exit', (code) => {
1088
- if (code === 0) {
1089
- resolve({ success: true, output: combinedOutput });
1090
- } else {
1091
- const message = `${providerConfig.displayName} exited with code ${code}`;
1092
- resolve({
1093
- success: false,
1094
- error: combinedOutput ? `${message}\n${combinedOutput}` : message,
1095
- output: combinedOutput,
1096
- rateLimited: isRateLimitMessage(combinedOutput)
1097
- });
1098
- }
1099
- });
1100
- });
1101
- }
1102
-
1103
- /**
1104
- * Wait for IDE agent to complete work by monitoring requirements file
1105
- * @param {string} repoPath - Repository path
1106
- * @param {string} requirementText - Requirement text to watch for
1107
- * @param {string} ideType - IDE type (e.g., 'antigravity') for quota limit handling
1108
- * @param {number} timeoutMs - Timeout in milliseconds (default: 30 minutes)
1109
- * @returns {Promise<{success: boolean, reason?: string}>}
1110
- */
1111
- async function waitForIdeCompletion(repoPath, requirementText, ideType = '', timeoutMs = 30 * 60 * 1000) {
1112
- const reqPath = await getRequirementsPath(repoPath);
1113
-
1114
- return new Promise(async (resolve) => {
1115
- let startTime = Date.now();
1116
- let lastCheckTime = Date.now();
1117
- let quotaHandled = false;
1118
- const checkIntervalMs = 2000; // Check every 2 seconds
1119
-
1120
- console.log(chalk.gray('\nā³ Waiting for IDE agent to complete...'));
1121
- console.log(chalk.gray(` Monitoring: ${path.basename(reqPath)}`));
1122
- console.log(chalk.gray(` Timeout: ${Math.floor(timeoutMs / 60000)} minutes\n`));
1123
-
1124
- const watcher = chokidar.watch(reqPath, {
1125
- persistent: true,
1126
- ignoreInitial: false
1127
- });
1128
-
1129
- const checkCompletion = async () => {
1130
- try {
1131
- const content = await fs.readFile(reqPath, 'utf-8');
1132
- const lines = content.split('\n');
1133
-
1134
- // Check 1: Is requirement in "Verified by AI" section?
1135
- let inVerifiedSection = false;
1136
- let foundInVerified = false;
1137
-
1138
- for (const line of lines) {
1139
- if (line.includes('## āœ… Verified by AI screenshot')) {
1140
- inVerifiedSection = true;
1141
- continue;
1142
- }
1143
-
1144
- if (inVerifiedSection && line.startsWith('##') && !line.startsWith('###')) {
1145
- inVerifiedSection = false;
1146
- break;
1147
- }
1148
-
1149
- if (inVerifiedSection && line.includes(requirementText)) {
1150
- foundInVerified = true;
1151
- break;
1152
- }
1153
- }
1154
-
1155
- if (foundInVerified) {
1156
- watcher.close();
1157
- console.log(chalk.green('āœ“ IDE agent completed - requirement moved to Verified section\n'));
1158
- resolve({ success: true });
1159
- return;
1160
- }
1161
-
1162
- // Check 2: Does status section contain "DONE"?
1163
- let inStatusSection = false;
1164
- let statusContainsDone = false;
1165
-
1166
- for (const line of lines) {
1167
- if (line.includes('🚦 Current Status')) {
1168
- inStatusSection = true;
1169
- continue;
1170
- }
1171
-
1172
- if (inStatusSection && line.startsWith('##') && !line.startsWith('###')) {
1173
- inStatusSection = false;
1174
- break;
1175
- }
1176
-
1177
- if (inStatusSection && line.trim() === 'DONE') {
1178
- statusContainsDone = true;
1179
- break;
1180
- }
1181
- }
1182
-
1183
- if (statusContainsDone) {
1184
- watcher.close();
1185
- console.log(chalk.green('āœ“ IDE agent completed - status marked as DONE\n'));
1186
- resolve({ success: true });
1187
- return;
1188
- }
1189
-
1190
- // Check 3: Quota limit detection for Antigravity (after 2 minutes of waiting)
1191
- const elapsed = Date.now() - startTime;
1192
- if (ideType === 'antigravity' && !quotaHandled && elapsed >= 120000) {
1193
- console.log(chalk.yellow('\nāš ļø No progress detected after 2 minutes - checking for quota limit...\n'));
1194
- try {
1195
- const { AppleScriptManager } = require('vibecodingmachine-core');
1196
- const manager = new AppleScriptManager();
1197
- const result = await manager.handleAntigravityQuotaLimit();
1198
-
1199
- if (result.success) {
1200
- console.log(chalk.green(`āœ“ Switched to model: ${result.model || 'alternative'}`));
1201
- console.log(chalk.cyan(' Resuming work with new model...\n'));
1202
- quotaHandled = true;
1203
- // Reset start time to give new model time to work
1204
- startTime = Date.now();
1205
- } else {
1206
- console.log(chalk.yellow(`āš ļø Could not switch models: ${result.error}\n`));
1207
- quotaHandled = true; // Don't try again
1208
- }
1209
- } catch (error) {
1210
- console.error(chalk.red(`Error handling quota limit: ${error.message}\n`));
1211
- quotaHandled = true; // Don't try again
1212
- }
1213
- }
1214
-
1215
- // Check 4: Timeout
1216
- if (elapsed >= timeoutMs) {
1217
- watcher.close();
1218
- console.log(chalk.yellow(`\nāš ļø Timeout after ${Math.floor(elapsed / 60000)} minutes\n`));
1219
- resolve({ success: false, reason: 'timeout' });
1220
- return;
1221
- }
1222
-
1223
- // Log progress every 30 seconds
1224
- if (Date.now() - lastCheckTime >= 30000) {
1225
- const elapsedMin = Math.floor(elapsed / 60000);
1226
- const remainingMin = Math.floor((timeoutMs - elapsed) / 60000);
1227
- console.log(chalk.gray(` Still waiting... (${elapsedMin}m elapsed, ${remainingMin}m remaining)`));
1228
- lastCheckTime = Date.now();
1229
- }
1230
- } catch (error) {
1231
- console.error(chalk.red(`Error checking completion: ${error.message}`));
1232
- }
1233
- };
1234
-
1235
- // Check on file changes
1236
- watcher.on('change', () => {
1237
- checkCompletion();
1238
- });
1239
-
1240
- // Also check periodically in case file watcher misses changes
1241
- const interval = setInterval(() => {
1242
- checkCompletion();
1243
- }, checkIntervalMs);
1244
-
1245
- // Clean up interval when promise resolves
1246
- const originalResolve = resolve;
1247
- resolve = (result) => {
1248
- clearInterval(interval);
1249
- originalResolve(result);
1250
- };
1251
-
1252
- // Initial check
1253
- checkCompletion();
1254
- });
1255
- }
1256
-
1257
- async function runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime) {
1258
- // Update console and requirements file with PREPARE status
1259
- printStatusCard(requirement.text, 'PREPARE');
1260
- await updateRequirementsStatus(repoPath, 'PREPARE');
1261
- console.log(chalk.gray('Skipping direct file context - delegating to IDE agent.\n'));
1262
-
1263
- // Update console and requirements file with ACT status
1264
- printStatusCard(requirement.text, 'ACT');
1265
- await updateRequirementsStatus(repoPath, 'ACT');
1266
- const ideResult = await runIdeProviderIteration(providerConfig, repoPath);
1267
-
1268
- if (!ideResult.success) {
1269
- // CRITICAL: Mark provider as unavailable for ANY error so acquireProviderConfig() will skip it
1270
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, ideResult.output || ideResult.error || 'IDE provider failed');
1271
- return { success: false, error: ideResult.error || 'IDE provider failed' };
1272
- }
1273
-
1274
- console.log(chalk.green('āœ“ Prompt sent to IDE agent successfully'));
1275
-
1276
- // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
1277
- const completionResult = await waitForIdeCompletion(repoPath, requirement.text, providerConfig.ide || providerConfig.provider);
1278
-
1279
- if (!completionResult.success) {
1280
- const errorMsg = completionResult.reason === 'timeout'
1281
- ? 'IDE agent timed out'
1282
- : 'IDE agent failed to complete';
1283
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, errorMsg);
1284
- return { success: false, error: errorMsg };
1285
- }
1286
-
1287
- printStatusCard(requirement.text, 'VERIFY');
1288
- console.log(chalk.green('āœ… IDE provider completed iteration\n'));
1289
-
1290
- printStatusCard(requirement.text, 'DONE');
1291
- const duration = Date.now() - startTime;
1292
- providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
1293
-
1294
- const moved = await moveRequirementToVerify(repoPath, requirement.text);
1295
- if (moved) {
1296
- console.log(chalk.green('āœ“ Requirement moved to TO VERIFY BY HUMAN section'));
1297
- } else {
1298
- console.log(chalk.yellow('āš ļø Requirement still pending verification in REQUIREMENTS file'));
1299
- }
1300
-
1301
- console.log();
1302
- console.log(chalk.gray('─'.repeat(80)));
1303
- console.log();
1304
-
1305
- return { success: true, changes: [] };
1306
- }
1307
-
1308
- /**
1309
- * Run one iteration of autonomous mode with full workflow
1310
- */
1311
- async function runIteration(requirement, providerConfig, repoPath) {
1312
- const startTime = Date.now();
1313
- const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
1314
- const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
1315
-
1316
- if (providerConfig.type === 'ide') {
1317
- return runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime);
1318
- }
1319
-
1320
- // ═══════════════════════════════════════════════════════════
1321
- // PREPARE PHASE - SEARCH AND READ ACTUAL FILES
1322
- // ═══════════════════════════════════════════════════════════
1323
- printStatusCard(requirement.text, 'PREPARE');
1324
-
1325
- console.log(chalk.bold.white('šŸ“‹ REQUIREMENT:'));
1326
- console.log(chalk.cyan(` ${requirement.text}\n`));
1327
- console.log(chalk.gray('Provider:'), chalk.white(providerConfig.displayName));
1328
- console.log(chalk.gray('Repository:'), chalk.white(repoPath));
1329
- console.log();
1330
-
1331
- console.log(chalk.cyan('šŸ” Searching for relevant files...\n'));
1332
- const relevantFiles = await findRelevantFiles(requirement.text, repoPath);
1333
-
1334
- if (relevantFiles.length > 0) {
1335
- console.log(chalk.white('Found relevant files:'));
1336
- relevantFiles.forEach((file, i) => {
1337
- console.log(chalk.gray(` ${i + 1}. ${file}`));
1338
- });
1339
- console.log();
1340
- }
1341
-
1342
- console.log(chalk.cyan('šŸ“– Reading file context...\n'));
1343
- const fileSnippets = await readFileSnippets(relevantFiles, repoPath, requirement.text);
1344
-
1345
- await new Promise(resolve => setTimeout(resolve, 500));
1346
-
1347
- // ═══════════════════════════════════════════════════════════
1348
- // ACT PHASE - GET STRUCTURED CHANGES FROM LLM
1349
- // ═══════════════════════════════════════════════════════════
1350
- printStatusCard(requirement.text, 'ACT');
1351
-
1352
- console.log(chalk.cyan('šŸ¤– Asking LLM for implementation...\n'));
1353
-
1354
- // Build context with actual file snippets
1355
- let contextSection = '';
1356
- if (fileSnippets.length > 0) {
1357
- contextSection = '\n\nCURRENT CODE CONTEXT:\n';
1358
- fileSnippets.forEach(({ file, snippet, startLine }) => {
1359
- contextSection += `\n--- ${file} (around line ${startLine}) ---\n${snippet}\n`;
1360
- });
1361
- }
1362
-
1363
- const prompt = `You are implementing a code change. Your task is to provide a SEARCH/REPLACE block that will modify the code.
1364
-
1365
- REQUIREMENT TO IMPLEMENT:
1366
- ${requirement.text}
1367
- ${contextSection}
1368
-
1369
- YOUR TASK:
1370
- 1. Read the CURRENT CODE CONTEXT carefully
1371
- 2. Find the EXACT location where changes are needed
1372
- 3. COPY AT LEAST 10 LINES EXACTLY as they appear (including indentation, spacing, comments)
1373
- 4. Show what the code should look like after your changes
1374
-
1375
- OUTPUT FORMAT:
1376
-
1377
- FILE: <exact path from the "---" line>
1378
- SEARCH: \`\`\`javascript
1379
- <COPY 10+ lines EXACTLY - preserve indentation, spacing, comments>
1380
- \`\`\`
1381
- REPLACE: \`\`\`javascript
1382
- <SAME lines but with necessary modifications>
1383
- \`\`\`
1384
-
1385
- CRITICAL RULES - READ CAREFULLY:
1386
- 1. SEARCH block must be COPIED CHARACTER-BY-CHARACTER from CURRENT CODE CONTEXT above
1387
- 2. Use the EXACT code as it appears NOW - do NOT use old/outdated code from memory
1388
- 3. Include ALL property values EXACTLY as shown (e.g., if context shows 'type: "info"', use 'type: "info"', NOT 'type: "setting"')
1389
- 4. Include indentation EXACTLY as shown (count the spaces!)
1390
- 5. Include AT LEAST 20-30 LINES of context:
1391
- - Start 10-15 lines BEFORE the code you need to change
1392
- - End 10-15 lines AFTER the code you need to change
1393
- - MORE context is BETTER than less - include extra lines if unsure
1394
- 6. Do NOT paraphrase or rewrite - COPY EXACTLY from CURRENT CODE CONTEXT
1395
- 7. FILE path must match exactly what's after "---" in CURRENT CODE CONTEXT (DO NOT include the "---" itself)
1396
- 8. Output ONLY the FILE/SEARCH/REPLACE block
1397
- 9. NO explanations, NO markdown outside the blocks, NO additional text
1398
- 10. If you cannot find the exact code to modify, output: ERROR: CANNOT LOCATE CODE IN CONTEXT
1399
- 11. IMPORTANT: Include ALL intermediate code between the before/after context - do NOT skip lines
1400
- 12. DOUBLE-CHECK: Compare your SEARCH block line-by-line with CURRENT CODE CONTEXT to ensure they match
1401
-
1402
- EXAMPLE (notice EXACT copying of indentation and spacing):
1403
-
1404
- FILE: packages/cli/src/utils/interactive.js
1405
- SEARCH: \`\`\`javascript
1406
- // Add warning if no TODO requirements
1407
- if (counts.todoCount === 0) {
1408
- requirementsText += \` \${chalk.red('āš ļø No requirements to work on')}\`;
1409
- }
1410
- } else {
1411
- requirementsText += \`\${chalk.yellow('0 TODO')}, \${chalk.cyan('0 TO VERIFY')}, \${chalk.green('0 VERIFIED')} \${chalk.red('āš ļø No requirements')}\`;
1412
- }
1413
- \`\`\`
1414
- REPLACE: \`\`\`javascript
1415
- // Add warning if no TODO requirements
1416
- if (counts.todoCount === 0) {
1417
- requirementsText += \` \${chalk.red('āš ļø No requirements')}\`;
1418
- }
1419
- } else {
1420
- requirementsText += \`\${chalk.yellow('0 TODO')}, \${chalk.cyan('0 TO VERIFY')}, \${chalk.green('0 VERIFIED')} \${chalk.red('āš ļø No requirements')}\`;
1421
- }
1422
- \`\`\`
1423
-
1424
- Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
1425
-
1426
- let fullResponse = '';
1427
-
1428
- const result = await llm.call(providerConfig, prompt, {
1429
- temperature: 0.1,
1430
- maxTokens: 4096,
1431
- onChunk: (chunk) => {
1432
- process.stdout.write(chalk.gray(chunk));
1433
- fullResponse += chunk;
1434
- },
1435
- onComplete: () => {
1436
- console.log('\n');
1437
- },
1438
- onError: (error) => {
1439
- console.error(chalk.red(`\nāœ— Error: ${error}`));
1440
- }
1441
- });
1442
-
1443
- if (!result.success) {
1444
- const combinedError = [result.error, fullResponse].filter(Boolean).join('\n').trim() || 'Unknown error';
1445
- console.log(chalk.red(`\nāœ— LLM call failed: ${combinedError}`));
1446
- // CRITICAL: Mark provider as rate-limited for ANY error so acquireProviderConfig() will skip it
1447
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, combinedError);
1448
- return { success: false, error: combinedError };
1449
- }
1450
-
1451
- // ═══════════════════════════════════════════════════════════
1452
- // CLEAN UP PHASE - APPLY CHANGES TO ACTUAL FILES
1453
- // ═══════════════════════════════════════════════════════════
1454
- printStatusCard(requirement.text, 'CLEAN UP');
1455
-
1456
- console.log(chalk.cyan('🧹 Parsing and applying changes...\n'));
1457
-
1458
- // Check if LLM said it cannot locate code
1459
- if (fullResponse.includes('ERROR: CANNOT LOCATE CODE') || fullResponse.includes('CANNOT LOCATE CODE')) {
1460
- console.log(chalk.red('\nāœ— LLM could not locate the code to modify'));
1461
- console.log(chalk.yellow('The code context provided may not contain the relevant section\n'));
1462
- return { success: false, error: 'LLM could not locate code in context', changes: [] };
1463
- }
1464
-
1465
- const changes = parseSearchReplaceBlocks(fullResponse);
1466
-
1467
- if (changes.length === 0) {
1468
- if (fullResponse.includes('NO CHANGES NEEDED')) {
1469
- console.log(chalk.yellow('āš ļø LLM determined no code changes needed\n'));
1470
- return { success: false, error: 'No changes needed', changes: [] };
1471
- } else {
1472
- console.log(chalk.yellow('āš ļø Could not parse any search/replace blocks from LLM response\n'));
1473
- console.log(chalk.gray('This might be a documentation-only requirement or LLM formatting issue\n'));
1474
- return { success: false, error: 'No search/replace blocks found', changes: [] };
1475
- }
1476
- } else {
1477
- console.log(chalk.white(`Applying ${changes.length} change(s):\n`));
1478
-
1479
- let appliedCount = 0;
1480
- let failedCount = 0;
1481
-
1482
- for (let i = 0; i < changes.length; i++) {
1483
- const change = changes[i];
1484
- console.log(chalk.cyan(` ${i + 1}. ${change.file}...`));
1485
-
1486
- const applyResult = await applyFileChange(change, repoPath);
1487
-
1488
- if (applyResult.success) {
1489
- const methodInfo = applyResult.method === 'fuzzy'
1490
- ? ` (fuzzy match at line ${applyResult.matchedAt})`
1491
- : '';
1492
- console.log(chalk.green(` āœ“ Applied successfully${methodInfo}`));
1493
- appliedCount++;
1494
- } else {
1495
- console.log(chalk.red(` āœ— Failed: ${applyResult.error}`));
1496
- failedCount++;
1497
- }
1498
- }
1499
-
1500
- console.log();
1501
- console.log(chalk.white(`Applied: ${chalk.green(appliedCount)}, Failed: ${chalk.red(failedCount)}`));
1502
- console.log();
1503
-
1504
- // CRITICAL: Fail if no changes were applied successfully
1505
- if (appliedCount === 0 && failedCount > 0) {
1506
- console.log(chalk.bold.red('\nāŒ ITERATION FAILED\n'));
1507
- console.log(chalk.red('No changes were successfully applied'));
1508
- console.log(chalk.yellow('Common causes:'));
1509
- console.log(chalk.gray(' - LLM provided incorrect search text'));
1510
- console.log(chalk.gray(' - Code has changed since context was provided'));
1511
- console.log(chalk.gray(' - File path is incorrect'));
1512
- console.log();
1513
- console.log(chalk.cyan('šŸ’” Tip: Check the search block matches the actual code in the file'));
1514
- console.log();
1515
- return { success: false, error: 'No changes applied', changes: [] };
1516
- }
1517
- }
1518
-
1519
- await new Promise(resolve => setTimeout(resolve, 500));
1520
-
1521
- // ═══════════════════════════════════════════════════════════
1522
- // VERIFY PHASE - CONFIRM CHANGES WERE APPLIED
1523
- // ═══════════════════════════════════════════════════════════
1524
- printStatusCard(requirement.text, 'VERIFY');
1525
-
1526
- console.log(chalk.cyan('āœ“ Verifying changes...\n'));
1527
-
1528
- if (changes.length > 0) {
1529
- console.log(chalk.white('Modified files:'));
1530
- for (const change of changes) {
1531
- const fullPath = path.join(repoPath, change.file);
1532
- if (await fs.pathExists(fullPath)) {
1533
- const content = await fs.readFile(fullPath, 'utf8');
1534
- const hasChange = content.includes(change.replace.trim());
1535
-
1536
- if (hasChange) {
1537
- console.log(chalk.green(` āœ“ ${change.file}`));
1538
- } else {
1539
- console.log(chalk.yellow(` āš ļø ${change.file} (change may not have applied)`));
1540
- }
1541
- }
1542
- }
1543
- console.log();
1544
- }
1545
-
1546
- await new Promise(resolve => setTimeout(resolve, 500));
1547
-
1548
- // ═══════════════════════════════════════════════════════════
1549
- // DONE PHASE
1550
- // ═══════════════════════════════════════════════════════════
1551
- printStatusCard(requirement.text, 'DONE');
1552
-
1553
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1554
-
1555
- console.log(chalk.bold.green('āœ… REQUIREMENT COMPLETED!\n'));
1556
- console.log(chalk.white('Requirement:'), chalk.cyan(requirement.text));
1557
- console.log(chalk.white('Files modified:'), chalk.cyan(changes.length));
1558
- console.log(chalk.white('Status:'), chalk.green('Moving to TO VERIFY BY HUMAN'));
1559
- console.log(chalk.white('Time:'), chalk.gray(`${elapsed}s`));
1560
- console.log();
1561
-
1562
- // Move requirement to TO VERIFY section
1563
- const moved = await moveRequirementToVerify(repoPath, requirement.text);
1564
- if (moved) {
1565
- console.log(chalk.green('āœ“ Requirement moved to TO VERIFY BY HUMAN section'));
1566
- } else {
1567
- console.log(chalk.yellow('āš ļø Could not automatically move requirement'));
1568
- }
1569
-
1570
- // Record performance metric
1571
- const duration = Date.now() - startTime;
1572
- providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
1573
-
1574
- console.log();
1575
- console.log(chalk.gray('─'.repeat(80)));
1576
- console.log();
1577
-
1578
- return { success: true, changes };
1579
- }
1580
-
1581
- /**
1582
- * Main auto mode command handler
1583
- */
1584
- async function handleAutoStart(options) {
1585
- try {
1586
- // STRICT AUTH CHECK
1587
- const auth = require('../utils/auth');
1588
- const isAuth = await auth.isAuthenticated();
1589
- if (!isAuth) {
1590
- console.log(chalk.cyan('\nšŸ” Opening browser for authentication...\n'));
1591
- try {
1592
- await auth.login();
1593
- console.log(chalk.green('\nāœ“ Authentication successful!\n'));
1594
- } catch (error) {
1595
- console.log(chalk.red('\nāœ— Authentication failed:'), error.message);
1596
- process.exit(1);
1597
- }
1598
- }
1599
-
1600
- console.log(chalk.bold.cyan('\nšŸ¤– Vibe Coding Machine - Direct API Auto Mode\n'));
1601
- console.log(chalk.gray('═'.repeat(80)));
1602
- console.log();
1603
-
1604
- // Get repo path
1605
- const repoPath = await getRepoPath();
1606
- if (!repoPath) {
1607
- console.log(chalk.red('āœ— No repository configured'));
1608
- console.log(chalk.gray('Run:'), chalk.cyan('ana repo:set <path>'));
1609
- return;
1610
- }
1611
-
1612
- console.log(chalk.white('Repository:'), chalk.cyan(repoPath));
1613
-
1614
- // Get provider configuration
1615
- let providerConfig = await acquireProviderConfig();
1616
- if (!providerConfig) {
1617
- return;
1618
- }
1619
-
1620
- console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
1621
-
1622
- // Get max chats
1623
- const config = await getAutoConfig();
1624
- const unlimited = !options.maxChats && !config.maxChats && config.neverStop;
1625
- const maxChats = unlimited ? Number.MAX_SAFE_INTEGER : (options.maxChats || config.maxChats || 1);
1626
- console.log(chalk.white('Max iterations:'), unlimited ? chalk.cyan('āˆž (never stop)') : chalk.cyan(maxChats));
1627
- console.log();
1628
- console.log(chalk.gray('═'.repeat(80)));
1629
-
1630
- // Main loop
1631
- let completedCount = 0;
1632
- let failedCount = 0;
1633
-
1634
- for (let i = 0; i < maxChats; i++) {
1635
- console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
1636
- console.log(chalk.bold.magenta(` ITERATION ${i + 1} of ${maxChats}`));
1637
- console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
1638
-
1639
- // Get current requirement
1640
- const requirement = await getCurrentRequirement(repoPath);
1641
- if (!requirement) {
1642
- console.log(chalk.bold.yellow('\nšŸŽ‰ All requirements completed!'));
1643
- console.log(chalk.gray('No more TODO items found in REQUIREMENTS file\n'));
1644
- break;
1645
- }
1646
-
1647
- // Run iteration with full workflow
1648
- const result = await runIteration(requirement, providerConfig, repoPath);
1649
-
1650
- if (result.success) {
1651
- completedCount++;
1652
- console.log(chalk.bold.green(`āœ… Iteration ${i + 1}/${maxChats} COMPLETE`));
1653
- console.log(chalk.gray('Moving to next requirement...\n'));
1654
-
1655
- // Check if restart CLI is enabled and there are more iterations
1656
- if (config.restartCLI && i < maxChats - 1) {
1657
- console.log(chalk.cyan('šŸ”„ Restarting CLI to pick up latest changes...\n'));
1658
-
1659
- // Calculate remaining iterations
1660
- const remainingIterations = maxChats - (i + 1);
1661
-
1662
- // Spawn new CLI process
1663
- const { spawn } = require('child_process');
1664
- const cliScriptPath = path.join(__dirname, '../../bin/vibecodingmachine.js');
1665
- const args = ['auto:direct', '--max-chats', remainingIterations.toString()];
1666
-
1667
- // Spawn without detached mode - child will inherit terminal
1668
- // We'll exit after a brief delay to let child establish itself
1669
- const child = spawn(process.execPath, [cliScriptPath, ...args], {
1670
- stdio: 'inherit',
1671
- cwd: process.cwd(),
1672
- env: process.env
1673
- });
1674
-
1675
- // Handle child errors (but don't wait for completion)
1676
- child.on('error', (err) => {
1677
- console.error(chalk.red('Error restarting CLI:'), err.message);
1678
- });
1679
-
1680
- // Don't wait for child - unref so parent can exit
1681
- child.unref();
1682
-
1683
- // Give child a moment to start before exiting parent
1684
- await new Promise(resolve => setTimeout(resolve, 300));
1685
-
1686
- // Exit this process - child continues with terminal
1687
- process.exit(0);
1688
- } else {
1689
- // Small delay before next iteration (if not restarting)
1690
- if (i < maxChats - 1) {
1691
- await new Promise(resolve => setTimeout(resolve, 2000));
1692
- }
1693
- }
1694
- } else {
1695
- // Check if it's a rate limit error (for logging purposes)
1696
- const isRateLimitError = isRateLimitMessage(result.error);
1697
- const errorType = isRateLimitError ? 'Rate limit' : 'Error';
1698
-
1699
- console.log(chalk.yellow(`āš ļø ${errorType} detected, switching to next provider in your list...\n`));
1700
-
1701
- const newProviderConfig = await acquireProviderConfig();
1702
- if (newProviderConfig) {
1703
- providerConfig = newProviderConfig;
1704
- console.log(chalk.green(`āœ“ Switched to: ${providerConfig.displayName}\n`));
1705
-
1706
- // Retry this iteration with new provider (don't increment i)
1707
- i--;
1708
- continue;
1709
- } else {
1710
- console.log(chalk.red('āœ— No alternative providers available\n'));
1711
- }
1712
-
1713
- failedCount++;
1714
- console.log(chalk.bold.red(`āŒ Iteration ${i + 1}/${maxChats} FAILED`));
1715
- console.log(chalk.red(`Error: ${result.error}\n`));
1716
- console.log(chalk.yellow('Continuing to next requirement...\n'));
1717
- }
1718
- }
1719
-
1720
- // Final Summary
1721
- console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
1722
- console.log(chalk.bold.magenta(` FINAL SUMMARY`));
1723
- console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
1724
-
1725
- console.log(chalk.white('Total iterations:'), unlimited ? chalk.cyan('āˆž (never stop)') : chalk.cyan(maxChats));
1726
- console.log(chalk.white('Completed:'), chalk.green(`${completedCount} āœ“`));
1727
- if (failedCount > 0) {
1728
- console.log(chalk.white('Failed:'), chalk.red(`${failedCount} āœ—`));
1729
- }
1730
- console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
1731
- console.log();
1732
-
1733
- if (completedCount > 0) {
1734
- console.log(chalk.bold.green(`šŸŽ‰ ${completedCount} requirement${completedCount > 1 ? 's' : ''} moved to TO VERIFY BY HUMAN!`));
1735
- console.log(chalk.gray('Check the REQUIREMENTS file to verify and approve changes.\n'));
1736
- }
1737
-
1738
- } catch (error) {
1739
- console.error(chalk.red('\nāœ— Fatal Error:'), error.message);
1740
- if (error.stack) {
1741
- console.log(chalk.gray(error.stack));
1742
- }
1743
- process.exit(1);
1744
- }
1745
- }
1746
-
1747
- module.exports = { handleAutoStart };
1748
-
1
+ /**
2
+ * Direct LLM Auto Mode - Full implementation using DirectLLMManager
3
+ * No IDE CLI tools - direct API calls to Ollama, Anthropic, Groq, or Bedrock
4
+ * Includes proper status management, file changes, and requirement tracking
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const { DirectLLMManager } = require('vibecodingmachine-core');
9
+ const { getRepoPath, getAutoConfig, setAutoConfig } = require('../utils/config');
10
+ const { getRequirementsPath, readRequirements } = require('vibecodingmachine-core');
11
+ const fs = require('fs-extra');
12
+ const path = require('path');
13
+ const { spawn } = require('child_process');
14
+ const chokidar = require('chokidar');
15
+ // Status management will use in-process tracking instead of external file
16
+ const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
17
+ const { getProviderPreferences, getProviderDefinition } = require('../utils/provider-registry');
18
+
19
+ // CRITICAL: Shared ProviderManager instance to track rate limits across all function calls
20
+ const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
21
+ const sharedProviderManager = new ProviderManager();
22
+
23
+ /**
24
+ * Get timestamp for logging
25
+ */
26
+ function getTimestamp() {
27
+ const now = new Date();
28
+ return now.toLocaleTimeString('en-US', {
29
+ hour: '2-digit',
30
+ minute: '2-digit',
31
+ hour12: false,
32
+ timeZone: 'America/Denver'
33
+ }) + ' MST';
34
+ }
35
+
36
+ /**
37
+ * Strip ANSI escape codes from a string
38
+ */
39
+ function stripAnsi(str) {
40
+ // eslint-disable-next-line no-control-regex
41
+ return str.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
42
+ }
43
+
44
+ /**
45
+ * Count visual width of emojis in a string (emojis take 2 terminal columns)
46
+ * Uses string iterator to properly handle multi-code-point emojis
47
+ */
48
+ function countEmojiWidth(str) {
49
+ // Remove ANSI codes first
50
+ const cleaned = stripAnsi(str);
51
+
52
+ let emojiCount = 0;
53
+ // Use spread operator to properly split multi-code-point characters
54
+ for (const char of cleaned) {
55
+ const code = char.codePointAt(0);
56
+ // Check if it's an emoji (takes 2 columns in terminal)
57
+ if (
58
+ (code >= 0x1F300 && code <= 0x1F9FF) || // Misc Symbols and Pictographs
59
+ (code >= 0x2600 && code <= 0x26FF) || // Misc Symbols
60
+ (code >= 0x2700 && code <= 0x27BF) || // Dingbats
61
+ (code >= 0x1F000 && code <= 0x1F02F) || // Mahjong Tiles
62
+ (code >= 0x1F0A0 && code <= 0x1F0FF) || // Playing Cards
63
+ (code >= 0x1F100 && code <= 0x1F64F) || // Enclosed Characters
64
+ (code >= 0x1F680 && code <= 0x1F6FF) || // Transport and Map
65
+ (code >= 0x1F910 && code <= 0x1F96B) || // Supplemental Symbols
66
+ (code >= 0x1F980 && code <= 0x1F9E0) // Supplemental Symbols
67
+ ) {
68
+ emojiCount++;
69
+ }
70
+ }
71
+ return emojiCount;
72
+ }
73
+
74
+ /**
75
+ * Get visual width of a string accounting for ANSI codes and emojis
76
+ */
77
+ function getVisualWidth(str) {
78
+ const stripped = stripAnsi(str);
79
+ const emojiCount = countEmojiWidth(str);
80
+ // Count actual characters (using spread to handle multi-code-point correctly)
81
+ const charCount = [...stripped].length;
82
+ // Each emoji takes 2 visual columns, regular chars take 1
83
+ return charCount + emojiCount;
84
+ }
85
+
86
+ /**
87
+ * Pad string to visual width accounting for emojis and ANSI codes
88
+ */
89
+ function padToVisualWidth(str, targetWidth) {
90
+ const visualWidth = getVisualWidth(str);
91
+ const paddingNeeded = targetWidth - visualWidth;
92
+ if (paddingNeeded <= 0) {
93
+ return str;
94
+ }
95
+ return str + ' '.repeat(paddingNeeded);
96
+ }
97
+
98
+ function isRateLimitMessage(text) {
99
+ if (!text) return false;
100
+ const lower = text.toLowerCase();
101
+ return lower.includes('rate limit') ||
102
+ lower.includes('too many requests') ||
103
+ lower.includes('429') ||
104
+ lower.includes('weekly limit') ||
105
+ lower.includes('daily limit') ||
106
+ lower.includes('limit reached');
107
+ }
108
+
109
+ function sleep(ms) {
110
+ return new Promise(resolve => setTimeout(resolve, ms));
111
+ }
112
+
113
+ /**
114
+ * Print purple status card with progress indicators
115
+ */
116
+ /**
117
+ * Update status section in requirements file
118
+ * @param {string} repoPath - Repository path
119
+ * @param {string} status - Status to set (PREPARE, ACT, CLEAN UP, VERIFY, DONE)
120
+ */
121
+ async function updateRequirementsStatus(repoPath, status) {
122
+ try {
123
+ const reqPath = await getRequirementsPath(repoPath);
124
+ if (!reqPath || !await fs.pathExists(reqPath)) {
125
+ return;
126
+ }
127
+
128
+ const content = await fs.readFile(reqPath, 'utf-8');
129
+ const lines = content.split('\n');
130
+ let inStatusSection = false;
131
+ let statusLineIndex = -1;
132
+
133
+ // Find the status section and the line with the status
134
+ for (let i = 0; i < lines.length; i++) {
135
+ const line = lines[i];
136
+
137
+ if (line.includes('🚦 Current Status')) {
138
+ inStatusSection = true;
139
+ continue;
140
+ }
141
+
142
+ if (inStatusSection && line.startsWith('##') && !line.startsWith('###')) {
143
+ // End of status section, status line not found
144
+ break;
145
+ }
146
+
147
+ if (inStatusSection && line.trim().match(/^(PREPARE|CREATE|ACT|CLEAN UP|VERIFY|DONE)$/)) {
148
+ statusLineIndex = i;
149
+ break;
150
+ }
151
+ }
152
+
153
+ // Update or add the status line
154
+ if (statusLineIndex >= 0) {
155
+ // Replace existing status
156
+ lines[statusLineIndex] = status;
157
+ } else if (inStatusSection) {
158
+ // Add status after the section header
159
+ for (let i = 0; i < lines.length; i++) {
160
+ if (lines[i].includes('🚦 Current Status')) {
161
+ lines.splice(i + 1, 0, status);
162
+ break;
163
+ }
164
+ }
165
+ } else {
166
+ // Status section doesn't exist - find the requirement and add it
167
+ for (let i = 0; i < lines.length; i++) {
168
+ if (lines[i].startsWith('### ') && !lines[i].includes('🚦 Current Status')) {
169
+ // Found a requirement header, add status section after it
170
+ lines.splice(i + 1, 0, '', '#### 🚦 Current Status', status);
171
+ break;
172
+ }
173
+ }
174
+ }
175
+
176
+ await fs.writeFile(reqPath, lines.join('\n'));
177
+ } catch (error) {
178
+ // Silently fail - don't break execution if status update fails
179
+ console.error(chalk.gray(` Warning: Could not update status in requirements file: ${error.message}`));
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Print purple status card with progress indicators
185
+ * Makes current stage very prominent with bright white text
186
+ * Uses full terminal width
187
+ */
188
+ function printStatusCard(currentTitle, currentStatus) {
189
+ const stages = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
190
+ const stageMap = {
191
+ 'PREPARE': 0,
192
+ 'ACT': 1,
193
+ 'CLEAN UP': 2,
194
+ 'VERIFY': 3,
195
+ 'DONE': 4
196
+ };
197
+
198
+ const currentIndex = stageMap[currentStatus] || 0;
199
+
200
+ // Build workflow line with visual prominence for current stage
201
+ const stageParts = stages.map((stage, idx) => {
202
+ if (idx < currentIndex) {
203
+ // Completed stages - grey with checkmark
204
+ return chalk.grey(`āœ… ${stage}`);
205
+ } else if (idx === currentIndex) {
206
+ // CURRENT stage - BRIGHT WHITE with hammer
207
+ return chalk.bold.white(`šŸ”Ø ${stage}`);
208
+ } else {
209
+ // Future stages - grey with hourglass
210
+ return chalk.grey(`ā³ ${stage}`);
211
+ }
212
+ });
213
+
214
+ const workflowLine = stageParts.join(chalk.grey(' → '));
215
+
216
+ // Get terminal width, default to 100 if not available
217
+ const terminalWidth = process.stdout.columns || 100;
218
+ const boxWidth = Math.max(terminalWidth - 4, 80); // Leave 4 chars margin, minimum 80
219
+
220
+ // Truncate title if needed to fit in box
221
+ const maxTitleWidth = boxWidth - 20; // Leave room for "šŸŽÆ Working on: "
222
+ const titleShort = currentTitle?.substring(0, maxTitleWidth) + (currentTitle?.length > maxTitleWidth ? '...' : '');
223
+ const titleLine = chalk.cyan(`šŸŽÆ Working on: `) + chalk.white(titleShort);
224
+
225
+ console.log(chalk.magenta('\nā•­' + '─'.repeat(boxWidth) + 'ā•®'));
226
+ console.log(chalk.magenta('│') + padToVisualWidth(' ' + workflowLine, boxWidth) + chalk.magenta('│'));
227
+ console.log(chalk.magenta('│') + ' '.repeat(boxWidth) + chalk.magenta('│'));
228
+ console.log(chalk.magenta('│') + padToVisualWidth(' ' + titleLine, boxWidth) + chalk.magenta('│'));
229
+ console.log(chalk.magenta('ā•°' + '─'.repeat(boxWidth) + '╯\n'));
230
+ }
231
+
232
+ /**
233
+ * Get current requirement from REQUIREMENTS file
234
+ */
235
+ async function getCurrentRequirement(repoPath) {
236
+ try {
237
+ const reqPath = await getRequirementsPath(repoPath);
238
+ if (!reqPath || !await fs.pathExists(reqPath)) {
239
+ return null;
240
+ }
241
+
242
+ const content = await fs.readFile(reqPath, 'utf8');
243
+
244
+ // Extract first TODO requirement (new header format)
245
+ const lines = content.split('\n');
246
+ let inTodoSection = false;
247
+
248
+ for (let i = 0; i < lines.length; i++) {
249
+ const line = lines[i].trim();
250
+
251
+ // Check if we're in the TODO section
252
+ if (line.includes('## ā³ Requirements not yet completed') ||
253
+ line.includes('Requirements not yet completed')) {
254
+ inTodoSection = true;
255
+ continue;
256
+ }
257
+
258
+ // If we hit another section header, stop looking
259
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
260
+ break;
261
+ }
262
+
263
+ // If we're in TODO section and find a requirement header (###)
264
+ if (inTodoSection && line.startsWith('###')) {
265
+ const title = line.replace(/^###\s*/, '').trim();
266
+ // Skip empty titles
267
+ if (title && title.length > 0) {
268
+ // Read package and description (optional)
269
+ let package = null;
270
+ let description = '';
271
+ let j = i + 1;
272
+
273
+ // Read next few lines for package and description
274
+ while (j < lines.length && j < i + 20) {
275
+ const nextLine = lines[j].trim();
276
+ // Stop if we hit another requirement or section
277
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
278
+ break;
279
+ }
280
+ // Check for PACKAGE line
281
+ if (nextLine.startsWith('PACKAGE:')) {
282
+ package = nextLine.replace(/^PACKAGE:\s*/, '').trim();
283
+ } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
284
+ // Description line (not empty, not package)
285
+ if (description) {
286
+ description += '\n' + nextLine;
287
+ } else {
288
+ description = nextLine;
289
+ }
290
+ }
291
+ j++;
292
+ }
293
+
294
+ return {
295
+ text: title,
296
+ fullLine: lines[i],
297
+ package: package,
298
+ description: description
299
+ };
300
+ }
301
+ }
302
+ }
303
+
304
+ return null;
305
+ } catch (err) {
306
+ console.error('Error reading requirement:', err.message);
307
+ return null;
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Move requirement from TODO to TO VERIFY BY HUMAN
313
+ */
314
+ async function moveRequirementToVerify(repoPath, requirementText) {
315
+ try {
316
+ const reqPath = await getRequirementsPath(repoPath);
317
+ if (!reqPath || !await fs.pathExists(reqPath)) {
318
+ return false;
319
+ }
320
+
321
+ const content = await fs.readFile(reqPath, 'utf8');
322
+ const lines = content.split('\n');
323
+
324
+ // Find the requirement by its title (in ### header format)
325
+ const snippet = requirementText.substring(0, 80);
326
+ let requirementStartIndex = -1;
327
+ let requirementEndIndex = -1;
328
+
329
+ for (let i = 0; i < lines.length; i++) {
330
+ const line = lines[i].trim();
331
+ // Check if this is the requirement header
332
+ if (line.startsWith('###')) {
333
+ const title = line.replace(/^###\s*/, '').trim();
334
+ if (title && title.includes(snippet)) {
335
+ requirementStartIndex = i;
336
+ // Find the end of this requirement (next ### or ## header)
337
+ for (let j = i + 1; j < lines.length; j++) {
338
+ const nextLine = lines[j].trim();
339
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
340
+ requirementEndIndex = j;
341
+ break;
342
+ }
343
+ }
344
+ if (requirementEndIndex === -1) {
345
+ requirementEndIndex = lines.length;
346
+ }
347
+ break;
348
+ }
349
+ }
350
+ }
351
+
352
+ if (requirementStartIndex === -1) {
353
+ console.log(chalk.yellow('āš ļø Could not find requirement in requirements file'));
354
+ return false;
355
+ }
356
+
357
+ // Extract the entire requirement block
358
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
359
+
360
+ // Remove the requirement from its current location
361
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
362
+
363
+ // Check if the requirement is in the Recycled section
364
+ const recycledIndex = lines.findIndex(line => line.trim() === '## šŸ“¦ RECYCLED');
365
+ if (recycledIndex !== -1) {
366
+ const recycledBlock = lines.slice(recycledIndex + 1);
367
+ const requirementIndexInRecycled = recycledBlock.findIndex(line => line.trim().startsWith('###') && line.trim().includes(snippet));
368
+ if (requirementIndexInRecycled !== -1) {
369
+ lines.splice(recycledIndex + 1 + requirementIndexInRecycled, 1);
370
+ }
371
+ }
372
+
373
+ // Find or create VERIFIED section
374
+ const headingVariants = [
375
+ '## šŸ“ VERIFIED',
376
+ '## VERIFIED',
377
+ '## āœ… VERIFIED',
378
+ '## āœ… VERIFIED BY HUMAN'
379
+ ];
380
+
381
+ let verifyIndex = lines.findIndex(line => headingVariants.includes(line.trim()));
382
+ if (verifyIndex === -1) {
383
+ const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ā“'));
384
+ const headingLine = '## šŸ“ VERIFIED';
385
+ const insertionIndex = manualFeedbackIndex === -1 ? lines.length : manualFeedbackIndex;
386
+ const block = [];
387
+ if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
388
+ block.push('');
389
+ }
390
+ block.push(headingLine, '');
391
+ lines.splice(insertionIndex, 0, ...block);
392
+ verifyIndex = lines.findIndex(line => line.trim() === headingLine);
393
+ }
394
+
395
+ // Insert the requirement block at the top of the VERIFIED section
396
+ lines.splice(verifyIndex + 1, 0, ...requirementBlock);
397
+ // Add blank line after if needed
398
+ if (verifyIndex + 1 + requirementBlock.length < lines.length && lines[verifyIndex + 1 + requirementBlock.length].trim() !== '') {
399
+ lines.splice(verifyIndex + 1 + requirementBlock.length, 0, '');
400
+ }
401
+
402
+ await fs.writeFile(reqPath, lines.join('\n'));
403
+ console.log(`Moved requirement to VERIFIED: ${requirementText}`);
404
+ return true;
405
+ } catch (err) {
406
+ console.error('Error moving requirement:', err.message);
407
+ return false;
408
+ }
409
+ }
410
+
411
+
412
+ /**
413
+ * Move requirement to recycled section
414
+ */
415
+ async function moveRequirementToRecycle(repoPath, requirementText) {
416
+ try {
417
+ const reqPath = await getRequirementsPath(repoPath);
418
+ if (!reqPath || !await fs.pathExists(reqPath)) {
419
+ return false;
420
+ }
421
+
422
+ let content = await fs.readFile(reqPath, 'utf8');
423
+
424
+ // Find and remove from any section
425
+ const lines = content.split('\n');
426
+ let requirementIndex = -1;
427
+
428
+ for (let i = 0; i < lines.length; i++) {
429
+ if (lines[i].includes(requirementText.substring(0, 50))) {
430
+ requirementIndex = i;
431
+ break;
432
+ }
433
+ }
434
+
435
+ if (requirementIndex === -1) {
436
+ console.log(chalk.yellow('āš ļø Could not find requirement'));
437
+ return false;
438
+ }
439
+
440
+ // Remove from any section
441
+ const requirementLine = lines[requirementIndex];
442
+ lines.splice(requirementIndex, 1);
443
+
444
+ // Add to Recycled section (after "## ā™»ļø Recycled")
445
+ let recycledIndex = -1;
446
+ for (let i = 0; i < lines.length; i++) {
447
+ if (lines[i].includes('## ā™»ļø Recycled')) {
448
+ recycledIndex = i; // Move to the line of the header
449
+ break;
450
+ }
451
+ }
452
+
453
+ if (recycledIndex === -1) {
454
+ lines.push('## ā™»ļø Recycled');
455
+ recycledIndex = lines.length - 1;
456
+ }
457
+
458
+ // Add timestamp and insert at TOP of Recycled list
459
+ const timestamp = new Date().toISOString().split('T')[0];
460
+ lines.splice(recycledIndex + 1, 0, `- ${timestamp}: ${requirementLine.replace(/^- /, '')}`);
461
+
462
+ // Save
463
+ await fs.writeFile(reqPath, lines.join('\n'));
464
+ return true;
465
+ } catch (err) {
466
+ console.error('Error moving requirement:', err.message);
467
+ return false;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Get all available provider configurations
473
+ */
474
+ async function getAllAvailableProviders() {
475
+ const config = await getAutoConfig();
476
+ const prefs = await getProviderPreferences();
477
+ const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
478
+ const providers = [];
479
+
480
+ const groqKey = process.env.GROQ_API_KEY || config.groqApiKey;
481
+ const anthropicKey = process.env.ANTHROPIC_API_KEY || config.anthropicApiKey;
482
+ const awsRegion = process.env.AWS_REGION || config.awsRegion;
483
+ const awsAccessKey = process.env.AWS_ACCESS_KEY_ID || config.awsAccessKeyId;
484
+ const awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY || config.awsSecretAccessKey;
485
+ const claudeCodeAvailable = await llm.isClaudeCodeAvailable();
486
+ const ollamaAvailable = await llm.isOllamaAvailable();
487
+ let ollamaModels = [];
488
+ if (ollamaAvailable) {
489
+ ollamaModels = await llm.getOllamaModels();
490
+ }
491
+
492
+ for (const providerId of prefs.order) {
493
+ const def = getProviderDefinition(providerId);
494
+ if (!def) continue;
495
+
496
+ const enabled = prefs.enabled[providerId] !== false;
497
+ const base = {
498
+ provider: providerId,
499
+ displayName: def.name,
500
+ type: def.type,
501
+ category: def.category,
502
+ enabled,
503
+ estimatedSpeed: def.estimatedSpeed,
504
+ ide: def.ide,
505
+ maxChats: def.type === 'ide' ? 1 : undefined
506
+ };
507
+
508
+ switch (providerId) {
509
+ case 'groq': {
510
+ if (!groqKey) continue;
511
+ const model = config.groqModel || def.defaultModel;
512
+ providers.push({
513
+ ...base,
514
+ model,
515
+ apiKey: groqKey,
516
+ displayName: `${def.name} (${model})`
517
+ });
518
+ break;
519
+ }
520
+ case 'anthropic': {
521
+ if (!anthropicKey) continue;
522
+ const model = config.anthropicModel || def.defaultModel;
523
+ providers.push({
524
+ ...base,
525
+ model,
526
+ apiKey: anthropicKey,
527
+ displayName: `${def.name} (${model})`
528
+ });
529
+ break;
530
+ }
531
+ case 'bedrock': {
532
+ if (!awsRegion || !awsAccessKey || !awsSecretKey) continue;
533
+ providers.push({
534
+ ...base,
535
+ model: def.defaultModel,
536
+ region: awsRegion,
537
+ accessKeyId: awsAccessKey,
538
+ secretAccessKey: awsSecretKey
539
+ });
540
+ break;
541
+ }
542
+ case 'claude-code': {
543
+ if (!claudeCodeAvailable) continue;
544
+ providers.push({
545
+ ...base,
546
+ model: def.defaultModel
547
+ });
548
+ break;
549
+ }
550
+ case 'cursor':
551
+ case 'windsurf':
552
+ case 'antigravity': {
553
+ providers.push(base);
554
+ break;
555
+ }
556
+ case 'ollama': {
557
+ if (!ollamaAvailable || ollamaModels.length === 0) continue;
558
+ const preferredModel = config.llmModel && config.llmModel.includes('ollama/')
559
+ ? config.llmModel.split('/')[1]
560
+ : config.llmModel || config.aiderModel;
561
+ let bestModel = preferredModel && ollamaModels.includes(preferredModel)
562
+ ? preferredModel
563
+ : ollamaModels.includes(def.defaultModel)
564
+ ? def.defaultModel
565
+ : ollamaModels[0];
566
+ providers.push({
567
+ ...base,
568
+ model: bestModel,
569
+ displayName: `${def.name} (${bestModel})`
570
+ });
571
+ break;
572
+ }
573
+ default:
574
+ break;
575
+ }
576
+ }
577
+
578
+ return providers;
579
+ }
580
+
581
+ /**
582
+ * Get provider configuration with automatic failover
583
+ */
584
+ async function getProviderConfig() {
585
+ const config = await getAutoConfig();
586
+ const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
587
+ const providers = await getAllAvailableProviders();
588
+ const savedAgent = config.agent || config.ide;
589
+
590
+ if (providers.length === 0) {
591
+ return { status: 'no_providers', providers: [] };
592
+ }
593
+
594
+ const enabledProviders = providers.filter(p => p.enabled);
595
+ const disabledProviders = providers.filter(p => !p.enabled);
596
+
597
+ if (enabledProviders.length === 0) {
598
+ return { status: 'no_enabled', disabledProviders };
599
+ }
600
+
601
+ const availableProviders = enabledProviders.filter(p => {
602
+ return !providerManager.isRateLimited(p.provider, p.model);
603
+ });
604
+
605
+ let selection = null;
606
+ if (savedAgent) {
607
+ selection = availableProviders.find(p => p.provider === savedAgent);
608
+ }
609
+ if (!selection) {
610
+ selection = availableProviders[0] || null;
611
+ }
612
+
613
+ if (selection) {
614
+ const perfKey = `${selection.provider}:${selection.model || ''}`;
615
+ const avgSpeed = providerManager.performance[perfKey]?.avgSpeed;
616
+ const speedInfo = avgSpeed ? ` (avg: ${(avgSpeed / 1000).toFixed(1)}s)` : '';
617
+ console.log(chalk.green(`āœ“ Selected: ${selection.displayName}${speedInfo}`));
618
+ return { status: 'ok', provider: selection, disabledProviders };
619
+ }
620
+
621
+ const waits = enabledProviders
622
+ .map(p => providerManager.getTimeUntilReset(p.provider, p.model))
623
+ .filter(Boolean);
624
+ const nextResetMs = waits.length ? Math.min(...waits) : null;
625
+
626
+ return {
627
+ status: 'all_rate_limited',
628
+ enabledProviders,
629
+ disabledProviders,
630
+ nextResetMs
631
+ };
632
+ }
633
+
634
+ async function acquireProviderConfig() {
635
+ while (true) {
636
+ const selection = await getProviderConfig();
637
+ if (selection.status === 'ok') {
638
+ return selection.provider;
639
+ }
640
+
641
+ if (selection.status === 'no_providers') {
642
+ console.log(chalk.red('\nāœ— No providers available. Configure API keys or IDE agents first.\n'));
643
+ return null;
644
+ }
645
+
646
+ if (selection.status === 'no_enabled') {
647
+ console.log(chalk.red('\nāœ— All providers are disabled. Enable at least one provider in the Agent menu.\n'));
648
+ return null;
649
+ }
650
+
651
+ if (selection.status === 'all_rate_limited') {
652
+ console.log(chalk.yellow('\nāš ļø All enabled providers are currently rate limited.'));
653
+ if (selection.disabledProviders && selection.disabledProviders.length > 0) {
654
+ console.log(chalk.gray(' Tip: Enable additional providers in the Agent menu for more fallbacks.'));
655
+ }
656
+ const waitMs = selection.nextResetMs || 60000;
657
+ const waitMinutes = Math.max(1, Math.ceil(waitMs / 60000));
658
+ console.log(chalk.gray(` Waiting for rate limits to reset (~${waitMinutes}m)...\n`));
659
+ await sleep(Math.min(waitMs, 60000));
660
+ continue;
661
+ }
662
+
663
+ return null;
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Parse search/replace blocks from LLM response
669
+ */
670
+ function parseSearchReplaceBlocks(response) {
671
+ const changes = [];
672
+
673
+ // Match FILE: path SEARCH: ``` old ``` REPLACE: ``` new ``` format
674
+ const blockRegex = /FILE:\s*(.+?)\nSEARCH:\s*```(?:[a-z]*)\n([\s\S]+?)```\s*REPLACE:\s*```(?:[a-z]*)\n([\s\S]+?)```/g;
675
+
676
+ let match;
677
+ while ((match = blockRegex.exec(response)) !== null) {
678
+ let filePath = match[1].trim();
679
+ const searchText = match[2];
680
+ const replaceText = match[3];
681
+
682
+ // Clean up file path - remove "---" prefix if present
683
+ filePath = filePath.replace(/^---\s*/, '').trim();
684
+
685
+ changes.push({
686
+ file: filePath,
687
+ search: searchText,
688
+ replace: replaceText
689
+ });
690
+ }
691
+
692
+ return changes;
693
+ }
694
+
695
+ /**
696
+ * Normalize whitespace for comparison (convert all whitespace to single spaces)
697
+ */
698
+ function normalizeWhitespace(str) {
699
+ return str.replace(/\s+/g, ' ').trim();
700
+ }
701
+
702
+ /**
703
+ * Extract key identifiers from code (variable names, function names, strings)
704
+ */
705
+ function extractIdentifiers(code) {
706
+ const identifiers = new Set();
707
+
708
+ // Extract quoted strings
709
+ const stringMatches = code.match(/'([^']+)'|"([^"]+)"/g);
710
+ if (stringMatches) {
711
+ stringMatches.forEach(match => {
712
+ const str = match.slice(1, -1); // Remove quotes
713
+ if (str.length > 3) { // Only meaningful strings
714
+ identifiers.add(str);
715
+ }
716
+ });
717
+ }
718
+
719
+ // Extract variable/function names (words followed by : or =)
720
+ const nameMatches = code.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[:=]/g);
721
+ if (nameMatches) {
722
+ nameMatches.forEach(match => {
723
+ const name = match.replace(/[:=].*$/, '').trim();
724
+ if (name.length > 2) {
725
+ identifiers.add(name);
726
+ }
727
+ });
728
+ }
729
+
730
+ // Extract common patterns like 'type:', 'name:', 'value:'
731
+ const patternMatches = code.match(/(type|name|value|file|path|function|const|let|var)\s*:/gi);
732
+ if (patternMatches) {
733
+ patternMatches.forEach(match => {
734
+ identifiers.add(match.toLowerCase().replace(/\s*:/, ''));
735
+ });
736
+ }
737
+
738
+ return Array.from(identifiers);
739
+ }
740
+
741
+ /**
742
+ * Extract structural pattern from code (ignoring values)
743
+ */
744
+ function extractPattern(code) {
745
+ // Replace strings with placeholders
746
+ let pattern = code.replace(/'[^']+'|"[^"]+"/g, '"..."');
747
+ // Replace numbers with placeholders
748
+ pattern = pattern.replace(/\b\d+\b/g, 'N');
749
+ // Normalize whitespace
750
+ pattern = normalizeWhitespace(pattern);
751
+ return pattern;
752
+ }
753
+
754
+ /**
755
+ * Apply a search/replace change to a file with fuzzy matching fallback
756
+ */
757
+ async function applyFileChange(change, repoPath) {
758
+ try {
759
+ const fullPath = path.join(repoPath, change.file);
760
+
761
+ // Check if file exists
762
+ if (!await fs.pathExists(fullPath)) {
763
+ return { success: false, error: `File not found: ${change.file}` };
764
+ }
765
+
766
+ // Read file
767
+ let content = await fs.readFile(fullPath, 'utf8');
768
+
769
+ // Try exact match first
770
+ console.log(chalk.gray(` šŸ” Trying exact match...`));
771
+ if (content.includes(change.search)) {
772
+ const newContent = content.replace(change.search, change.replace);
773
+ await fs.writeFile(fullPath, newContent, 'utf8');
774
+ console.log(chalk.green(` āœ“ Exact match found`));
775
+ return { success: true, method: 'exact' };
776
+ }
777
+ console.log(chalk.gray(` āœ— Exact match failed`));
778
+
779
+ // Try with normalized whitespace (fuzzy match)
780
+ console.log(chalk.gray(` šŸ” Trying fuzzy match (normalized whitespace)...`));
781
+ const normalizedSearch = normalizeWhitespace(change.search);
782
+ const lines = content.split('\n');
783
+ const searchLines = change.search.split('\n');
784
+
785
+ console.log(chalk.gray(` - Search block: ${searchLines.length} lines`));
786
+ console.log(chalk.gray(` - File total: ${lines.length} lines`));
787
+
788
+ // Extract key identifiers from search text (function names, variable names, strings)
789
+ const searchIdentifiers = extractIdentifiers(change.search);
790
+
791
+ // Try multiple window sizes (±5 lines) to account for LLM not including enough context
792
+ for (let sizeOffset = 0; sizeOffset <= 10; sizeOffset++) {
793
+ const windowSize = searchLines.length + sizeOffset;
794
+ if (sizeOffset > 0) {
795
+ console.log(chalk.gray(` šŸ” Trying window size +${sizeOffset} (${windowSize} lines)...`));
796
+ }
797
+
798
+ // Try to find a sequence of lines that matches when normalized
799
+ for (let i = 0; i < lines.length; i++) {
800
+ if (i + windowSize > lines.length) break;
801
+
802
+ const window = lines.slice(i, i + windowSize).join('\n');
803
+ const normalizedWindow = normalizeWhitespace(window);
804
+
805
+ // Check if normalized versions match (or if normalized window contains normalized search)
806
+ if (normalizedWindow === normalizedSearch || normalizedWindow.includes(normalizedSearch)) {
807
+ // Found a match! Replace this section
808
+ const beforeLines = lines.slice(0, i);
809
+ const afterLines = lines.slice(i + windowSize);
810
+ const replaceLines = change.replace.split('\n');
811
+
812
+ const newLines = [...beforeLines, ...replaceLines, ...afterLines];
813
+ const newContent = newLines.join('\n');
814
+
815
+ await fs.writeFile(fullPath, newContent, 'utf8');
816
+ console.log(chalk.green(` āœ“ Fuzzy match found at line ${i + 1} (window size: ${windowSize})`));
817
+ return { success: true, method: 'fuzzy', matchedAt: i + 1, windowSize };
818
+ }
819
+
820
+ // Also try semantic matching - check if key identifiers match even if some values differ
821
+ if (searchIdentifiers.length > 0) {
822
+ const windowIdentifiers = extractIdentifiers(window);
823
+ const matchingIdentifiers = searchIdentifiers.filter(id => windowIdentifiers.includes(id));
824
+ // If 80% of identifiers match, consider it a potential match
825
+ if (matchingIdentifiers.length >= searchIdentifiers.length * 0.8) {
826
+ // Check if the structure is similar (same number of lines, similar patterns)
827
+ const searchPattern = extractPattern(change.search);
828
+ const windowPattern = extractPattern(window);
829
+ if (searchPattern === windowPattern) {
830
+ // Found a semantic match! Replace this section
831
+ const beforeLines = lines.slice(0, i);
832
+ const afterLines = lines.slice(i + windowSize);
833
+ const replaceLines = change.replace.split('\n');
834
+
835
+ const newLines = [...beforeLines, ...replaceLines, ...afterLines];
836
+ const newContent = newLines.join('\n');
837
+
838
+ await fs.writeFile(fullPath, newContent, 'utf8');
839
+ console.log(chalk.green(` āœ“ Semantic match found at line ${i + 1} (window size: ${windowSize}, ${matchingIdentifiers.length}/${searchIdentifiers.length} identifiers)`));
840
+ return { success: true, method: 'semantic', matchedAt: i + 1, windowSize };
841
+ }
842
+ }
843
+ }
844
+ }
845
+ }
846
+ console.log(chalk.red(` āœ— No match found (tried exact + fuzzy with multiple window sizes)`));
847
+
848
+ return {
849
+ success: false,
850
+ error: `Search text not found in ${change.file} (tried exact, fuzzy, and semantic matching with windows ${searchLines.length}-${searchLines.length + 10} lines)`
851
+ };
852
+
853
+ } catch (error) {
854
+ return { success: false, error: error.message };
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Find relevant files based on requirement
860
+ */
861
+ async function findRelevantFiles(requirement, repoPath) {
862
+ const relevantFiles = [];
863
+
864
+ try {
865
+ const reqLower = requirement.toLowerCase();
866
+
867
+ // Map keywords to specific files
868
+ if (reqLower.includes('completed') && reqLower.includes('verify')) {
869
+ // This is about the auto mode moving requirements
870
+ relevantFiles.push('packages/cli/src/commands/auto-direct.js');
871
+ } else if (reqLower.includes('requirements page') || reqLower.includes('requirements:')) {
872
+ // This is about the requirements menu/page
873
+ relevantFiles.push('packages/cli/src/utils/interactive.js');
874
+ } else if (reqLower.includes('main screen') || reqLower.includes('menu')) {
875
+ // This is about the main menu
876
+ relevantFiles.push('packages/cli/src/utils/interactive.js');
877
+ } else {
878
+ // Default: check both files
879
+ relevantFiles.push('packages/cli/src/commands/auto-direct.js');
880
+ relevantFiles.push('packages/cli/src/utils/interactive.js');
881
+ }
882
+ } catch (error) {
883
+ console.log(chalk.yellow(`āš ļø Error finding files: ${error.message}`));
884
+ }
885
+
886
+ return relevantFiles;
887
+ }
888
+
889
+ /**
890
+ * Read file snippets to give LLM context
891
+ */
892
+ async function readFileSnippets(files, repoPath, requirement) {
893
+ const snippets = [];
894
+
895
+ for (const file of files) {
896
+ try {
897
+ const fullPath = path.join(repoPath, file);
898
+ if (await fs.pathExists(fullPath)) {
899
+ const content = await fs.readFile(fullPath, 'utf8');
900
+ const lines = content.split('\n');
901
+
902
+ let startLine = -1;
903
+ let endLine = -1;
904
+
905
+ // For auto-direct.js, find the moveRequirementToVerify function
906
+ if (file.includes('auto-direct.js')) {
907
+ for (let i = 0; i < lines.length; i++) {
908
+ const line = lines[i];
909
+ if (line.includes('async function moveRequirementToVerify')) {
910
+ startLine = i;
911
+ // Find the end of the function
912
+ let braceCount = 0;
913
+ let foundStart = false;
914
+ for (let j = i; j < lines.length; j++) {
915
+ const l = lines[j];
916
+ // Count braces to find function end
917
+ for (const char of l) {
918
+ if (char === '{') {
919
+ braceCount++;
920
+ foundStart = true;
921
+ } else if (char === '}') {
922
+ braceCount--;
923
+ if (foundStart && braceCount === 0) {
924
+ endLine = j + 1;
925
+ break;
926
+ }
927
+ }
928
+ }
929
+ if (endLine > 0) break;
930
+ }
931
+ break;
932
+ }
933
+ }
934
+ }
935
+
936
+ // For interactive.js, search based on requirement keywords
937
+ if (file.includes('interactive.js')) {
938
+ const reqLower = requirement.toLowerCase();
939
+
940
+ // Search for specific sections based on requirement
941
+ if (reqLower.includes('current agent') || (reqLower.includes('└─') && reqLower.includes('current agent'))) {
942
+ // Find the Current Agent display code (with or without rate limit)
943
+ // First, try to find the exact "└─ Current Agent" pattern
944
+ for (let i = 0; i < lines.length; i++) {
945
+ // Look for the exact pattern with tree character
946
+ if (lines[i].includes('└─') && lines[i].includes('Current Agent')) {
947
+ startLine = Math.max(0, i - 15);
948
+ endLine = Math.min(lines.length, i + 20);
949
+ break;
950
+ }
951
+ }
952
+ // If not found, look for "Current Agent" in menu items
953
+ if (startLine === -1) {
954
+ for (let i = 0; i < lines.length; i++) {
955
+ if (lines[i].includes('Current Agent') ||
956
+ (lines[i].includes('currentAgent') && lines[i].includes('items.push'))) {
957
+ startLine = Math.max(0, i - 15);
958
+ endLine = Math.min(lines.length, i + 20);
959
+ break;
960
+ }
961
+ }
962
+ }
963
+ // If still not found, look for the setting item with Current Agent
964
+ if (startLine === -1) {
965
+ for (let i = 0; i < lines.length; i++) {
966
+ if (lines[i].includes('setting:current-agent') ||
967
+ (lines[i].includes('Current Agent') && lines[i].includes('type:') && lines[i].includes('setting'))) {
968
+ startLine = Math.max(0, i - 10);
969
+ endLine = Math.min(lines.length, i + 15);
970
+ break;
971
+ }
972
+ }
973
+ }
974
+ } else if (reqLower.includes('remove') || reqLower.includes('delete')) {
975
+ // Find the delete/remove confirmation code
976
+ for (let i = 0; i < lines.length; i++) {
977
+ // Look for confirmAction with 'Delete' or 'Are you sure'
978
+ if ((lines[i].includes('confirmAction') && lines[i].includes('Delete')) ||
979
+ (lines[i].includes('confirmDelete') && (i > 0 && lines[i - 5] && lines[i - 5].includes("'delete'")))) {
980
+ startLine = Math.max(0, i - 10);
981
+ endLine = Math.min(lines.length, i + 20);
982
+ break;
983
+ }
984
+ }
985
+ } else if (reqLower.includes('submenu') || reqLower.includes('menu')) {
986
+ // Find the showRequirementActions function
987
+ for (let i = 0; i < lines.length; i++) {
988
+ if (lines[i].includes('async function showRequirementActions')) {
989
+ startLine = i;
990
+ endLine = Math.min(lines.length, i + 80);
991
+ break;
992
+ }
993
+ }
994
+ } else if (reqLower.includes('next todo requirement') || reqLower.includes('next requirement') || (reqLower.includes('requirement') && reqLower.includes('indent'))) {
995
+ // Find the "Next TODO Requirement" section
996
+ for (let i = 0; i < lines.length; i++) {
997
+ const line = lines[i];
998
+ if (line.includes('Next TODO Requirement') || line.includes('Next Requirement') || (line.includes('nextReqText') && line.includes('items.push'))) {
999
+ // Get more context - look backwards for requirementsText and forwards for the items.push
1000
+ startLine = Math.max(0, i - 30);
1001
+ // Look for the items.push that contains Next TODO Requirement
1002
+ for (let j = i; j < Math.min(lines.length, i + 20); j++) {
1003
+ if (lines[j].includes('items.push') && (lines[j].includes('Next TODO Requirement') || lines[j].includes('Next Requirement') || (j > 0 && (lines[j - 1].includes('Next TODO Requirement') || lines[j - 1].includes('Next Requirement'))))) {
1004
+ endLine = Math.min(lines.length, j + 10);
1005
+ break;
1006
+ }
1007
+ }
1008
+ if (endLine === -1) {
1009
+ endLine = Math.min(lines.length, i + 60);
1010
+ }
1011
+ break;
1012
+ }
1013
+ }
1014
+ // If not found, fall back to requirements section
1015
+ if (startLine === -1) {
1016
+ for (let i = 0; i < lines.length; i++) {
1017
+ const line = lines[i];
1018
+ if (line.includes('let requirementsText') || line.includes('requirementsText')) {
1019
+ startLine = Math.max(0, i - 10);
1020
+ endLine = Math.min(lines.length, i + 80);
1021
+ break;
1022
+ }
1023
+ }
1024
+ }
1025
+ } else {
1026
+ // Default: find menu/requirements section
1027
+ for (let i = 0; i < lines.length; i++) {
1028
+ const line = lines[i];
1029
+ if (line.includes('let requirementsText') || line.includes('requirementsText')) {
1030
+ startLine = Math.max(0, i - 10);
1031
+ endLine = Math.min(lines.length, i + 80);
1032
+ break;
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ if (startLine >= 0 && endLine > startLine) {
1039
+ const snippet = lines.slice(startLine, endLine).join('\n');
1040
+ console.log(chalk.gray(` Found snippet at lines ${startLine + 1}-${endLine + 1}`));
1041
+ snippets.push({ file, snippet, startLine: startLine + 1, endLine: endLine + 1 });
1042
+ } else {
1043
+ console.log(chalk.yellow(` āš ļø Could not find relevant section in ${file}`));
1044
+ }
1045
+ }
1046
+ } catch (error) {
1047
+ console.log(chalk.yellow(`āš ļø Could not read ${file}: ${error.message}`));
1048
+ }
1049
+ }
1050
+
1051
+ return snippets;
1052
+ }
1053
+
1054
+ async function runIdeProviderIteration(providerConfig, repoPath) {
1055
+ return new Promise((resolve) => {
1056
+ console.log(chalk.cyan(`āš™ļø Launching ${providerConfig.displayName} fallback (auto:start)...\n`));
1057
+
1058
+ const args = [CLI_ENTRY_POINT, 'auto:start', '--ide', providerConfig.ide || providerConfig.provider, '--max-chats', String(providerConfig.maxChats || 1)];
1059
+ const child = spawn(process.execPath, args, {
1060
+ cwd: repoPath,
1061
+ env: process.env,
1062
+ stdio: ['inherit', 'pipe', 'pipe']
1063
+ });
1064
+
1065
+ let combinedOutput = '';
1066
+
1067
+ child.stdout.on('data', (data) => {
1068
+ const text = data.toString();
1069
+ combinedOutput += text;
1070
+ process.stdout.write(text);
1071
+ });
1072
+
1073
+ child.stderr.on('data', (data) => {
1074
+ const text = data.toString();
1075
+ combinedOutput += text;
1076
+ process.stderr.write(text);
1077
+ });
1078
+
1079
+ child.on('error', (error) => {
1080
+ resolve({
1081
+ success: false,
1082
+ error: `Failed to start ${providerConfig.displayName}: ${error.message}`,
1083
+ output: combinedOutput
1084
+ });
1085
+ });
1086
+
1087
+ child.on('exit', (code) => {
1088
+ if (code === 0) {
1089
+ resolve({ success: true, output: combinedOutput });
1090
+ } else {
1091
+ const message = `${providerConfig.displayName} exited with code ${code}`;
1092
+ resolve({
1093
+ success: false,
1094
+ error: combinedOutput ? `${message}\n${combinedOutput}` : message,
1095
+ output: combinedOutput,
1096
+ rateLimited: isRateLimitMessage(combinedOutput)
1097
+ });
1098
+ }
1099
+ });
1100
+ });
1101
+ }
1102
+
1103
+ /**
1104
+ * Wait for IDE agent to complete work by monitoring requirements file
1105
+ * @param {string} repoPath - Repository path
1106
+ * @param {string} requirementText - Requirement text to watch for
1107
+ * @param {string} ideType - IDE type (e.g., 'antigravity') for quota limit handling
1108
+ * @param {number} timeoutMs - Timeout in milliseconds (default: 30 minutes)
1109
+ * @returns {Promise<{success: boolean, reason?: string}>}
1110
+ */
1111
+ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', timeoutMs = 30 * 60 * 1000) {
1112
+ const reqPath = await getRequirementsPath(repoPath);
1113
+
1114
+ return new Promise(async (resolve) => {
1115
+ let startTime = Date.now();
1116
+ let lastCheckTime = Date.now();
1117
+ let quotaHandled = false;
1118
+ const checkIntervalMs = 2000; // Check every 2 seconds
1119
+
1120
+ console.log(chalk.gray('\nā³ Waiting for IDE agent to complete...'));
1121
+ console.log(chalk.gray(` Monitoring: ${path.basename(reqPath)}`));
1122
+ console.log(chalk.gray(` Timeout: ${Math.floor(timeoutMs / 60000)} minutes\n`));
1123
+
1124
+ const watcher = chokidar.watch(reqPath, {
1125
+ persistent: true,
1126
+ ignoreInitial: false
1127
+ });
1128
+
1129
+ const checkCompletion = async () => {
1130
+ try {
1131
+ const content = await fs.readFile(reqPath, 'utf-8');
1132
+ const lines = content.split('\n');
1133
+
1134
+ // Check 1: Is requirement in "Verified by AI" section?
1135
+ let inVerifiedSection = false;
1136
+ let foundInVerified = false;
1137
+
1138
+ for (const line of lines) {
1139
+ if (line.includes('## āœ… Verified by AI screenshot')) {
1140
+ inVerifiedSection = true;
1141
+ continue;
1142
+ }
1143
+
1144
+ if (inVerifiedSection && line.startsWith('##') && !line.startsWith('###')) {
1145
+ inVerifiedSection = false;
1146
+ break;
1147
+ }
1148
+
1149
+ if (inVerifiedSection && line.includes(requirementText)) {
1150
+ foundInVerified = true;
1151
+ break;
1152
+ }
1153
+ }
1154
+
1155
+ if (foundInVerified) {
1156
+ watcher.close();
1157
+ console.log(chalk.green('āœ“ IDE agent completed - requirement moved to Verified section\n'));
1158
+ resolve({ success: true });
1159
+ return;
1160
+ }
1161
+
1162
+ // Check 2: Does status section contain "DONE"?
1163
+ let inStatusSection = false;
1164
+ let statusContainsDone = false;
1165
+
1166
+ for (const line of lines) {
1167
+ if (line.includes('🚦 Current Status')) {
1168
+ inStatusSection = true;
1169
+ continue;
1170
+ }
1171
+
1172
+ if (inStatusSection && line.startsWith('##') && !line.startsWith('###')) {
1173
+ inStatusSection = false;
1174
+ break;
1175
+ }
1176
+
1177
+ if (inStatusSection && line.trim() === 'DONE') {
1178
+ statusContainsDone = true;
1179
+ break;
1180
+ }
1181
+ }
1182
+
1183
+ if (statusContainsDone) {
1184
+ watcher.close();
1185
+ console.log(chalk.green('āœ“ IDE agent completed - status marked as DONE\n'));
1186
+ resolve({ success: true });
1187
+ return;
1188
+ }
1189
+
1190
+ // Check 3: Quota limit detection for Antigravity (after 2 minutes of waiting)
1191
+ const elapsed = Date.now() - startTime;
1192
+ if (ideType === 'antigravity' && !quotaHandled && elapsed >= 120000) {
1193
+ console.log(chalk.yellow('\nāš ļø No progress detected after 2 minutes - checking for quota limit...\n'));
1194
+ try {
1195
+ const { AppleScriptManager } = require('vibecodingmachine-core');
1196
+ const manager = new AppleScriptManager();
1197
+ const result = await manager.handleAntigravityQuotaLimit();
1198
+
1199
+ if (result.success) {
1200
+ console.log(chalk.green(`āœ“ Switched to model: ${result.model || 'alternative'}`));
1201
+ console.log(chalk.cyan(' Resuming work with new model...\n'));
1202
+ quotaHandled = true;
1203
+ // Reset start time to give new model time to work
1204
+ startTime = Date.now();
1205
+ } else {
1206
+ console.log(chalk.yellow(`āš ļø Could not switch models: ${result.error}\n`));
1207
+ quotaHandled = true; // Don't try again
1208
+ }
1209
+ } catch (error) {
1210
+ console.error(chalk.red(`Error handling quota limit: ${error.message}\n`));
1211
+ quotaHandled = true; // Don't try again
1212
+ }
1213
+ }
1214
+
1215
+ // Check 4: Timeout
1216
+ if (elapsed >= timeoutMs) {
1217
+ watcher.close();
1218
+ console.log(chalk.yellow(`\nāš ļø Timeout after ${Math.floor(elapsed / 60000)} minutes\n`));
1219
+ resolve({ success: false, reason: 'timeout' });
1220
+ return;
1221
+ }
1222
+
1223
+ // Log progress every 30 seconds
1224
+ if (Date.now() - lastCheckTime >= 30000) {
1225
+ const elapsedMin = Math.floor(elapsed / 60000);
1226
+ const remainingMin = Math.floor((timeoutMs - elapsed) / 60000);
1227
+ console.log(chalk.gray(` Still waiting... (${elapsedMin}m elapsed, ${remainingMin}m remaining)`));
1228
+ lastCheckTime = Date.now();
1229
+ }
1230
+ } catch (error) {
1231
+ console.error(chalk.red(`Error checking completion: ${error.message}`));
1232
+ }
1233
+ };
1234
+
1235
+ // Check on file changes
1236
+ watcher.on('change', () => {
1237
+ checkCompletion();
1238
+ });
1239
+
1240
+ // Also check periodically in case file watcher misses changes
1241
+ const interval = setInterval(() => {
1242
+ checkCompletion();
1243
+ }, checkIntervalMs);
1244
+
1245
+ // Clean up interval when promise resolves
1246
+ const originalResolve = resolve;
1247
+ resolve = (result) => {
1248
+ clearInterval(interval);
1249
+ originalResolve(result);
1250
+ };
1251
+
1252
+ // Initial check
1253
+ checkCompletion();
1254
+ });
1255
+ }
1256
+
1257
+ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime) {
1258
+ // Update console and requirements file with PREPARE status
1259
+ printStatusCard(requirement.text, 'PREPARE');
1260
+ await updateRequirementsStatus(repoPath, 'PREPARE');
1261
+ console.log(chalk.gray('Skipping direct file context - delegating to IDE agent.\n'));
1262
+
1263
+ // Update console and requirements file with ACT status
1264
+ printStatusCard(requirement.text, 'ACT');
1265
+ await updateRequirementsStatus(repoPath, 'ACT');
1266
+ const ideResult = await runIdeProviderIteration(providerConfig, repoPath);
1267
+
1268
+ if (!ideResult.success) {
1269
+ // CRITICAL: Mark provider as unavailable for ANY error so acquireProviderConfig() will skip it
1270
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, ideResult.output || ideResult.error || 'IDE provider failed');
1271
+ return { success: false, error: ideResult.error || 'IDE provider failed' };
1272
+ }
1273
+
1274
+ console.log(chalk.green('āœ“ Prompt sent to IDE agent successfully'));
1275
+
1276
+ // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
1277
+ const completionResult = await waitForIdeCompletion(repoPath, requirement.text, providerConfig.ide || providerConfig.provider);
1278
+
1279
+ if (!completionResult.success) {
1280
+ const errorMsg = completionResult.reason === 'timeout'
1281
+ ? 'IDE agent timed out'
1282
+ : 'IDE agent failed to complete';
1283
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, errorMsg);
1284
+ return { success: false, error: errorMsg };
1285
+ }
1286
+
1287
+ printStatusCard(requirement.text, 'VERIFY');
1288
+ console.log(chalk.green('āœ… IDE provider completed iteration\n'));
1289
+
1290
+ printStatusCard(requirement.text, 'DONE');
1291
+ const duration = Date.now() - startTime;
1292
+ providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
1293
+
1294
+ const moved = await moveRequirementToVerify(repoPath, requirement.text);
1295
+ if (moved) {
1296
+ console.log(chalk.green('āœ“ Requirement moved to TO VERIFY BY HUMAN section'));
1297
+ } else {
1298
+ console.log(chalk.yellow('āš ļø Requirement still pending verification in REQUIREMENTS file'));
1299
+ }
1300
+
1301
+ console.log();
1302
+ console.log(chalk.gray('─'.repeat(80)));
1303
+ console.log();
1304
+
1305
+ return { success: true, changes: [] };
1306
+ }
1307
+
1308
+ /**
1309
+ * Run one iteration of autonomous mode with full workflow
1310
+ */
1311
+ async function runIteration(requirement, providerConfig, repoPath) {
1312
+ const startTime = Date.now();
1313
+ const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
1314
+ const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
1315
+
1316
+ if (providerConfig.type === 'ide') {
1317
+ return runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime);
1318
+ }
1319
+
1320
+ // ═══════════════════════════════════════════════════════════
1321
+ // PREPARE PHASE - SEARCH AND READ ACTUAL FILES
1322
+ // ═══════════════════════════════════════════════════════════
1323
+ printStatusCard(requirement.text, 'PREPARE');
1324
+
1325
+ console.log(chalk.bold.white('šŸ“‹ REQUIREMENT:'));
1326
+ console.log(chalk.cyan(` ${requirement.text}\n`));
1327
+ console.log(chalk.gray('Provider:'), chalk.white(providerConfig.displayName));
1328
+ console.log(chalk.gray('Repository:'), chalk.white(repoPath));
1329
+ console.log();
1330
+
1331
+ console.log(chalk.cyan('šŸ” Searching for relevant files...\n'));
1332
+ const relevantFiles = await findRelevantFiles(requirement.text, repoPath);
1333
+
1334
+ if (relevantFiles.length > 0) {
1335
+ console.log(chalk.white('Found relevant files:'));
1336
+ relevantFiles.forEach((file, i) => {
1337
+ console.log(chalk.gray(` ${i + 1}. ${file}`));
1338
+ });
1339
+ console.log();
1340
+ }
1341
+
1342
+ console.log(chalk.cyan('šŸ“– Reading file context...\n'));
1343
+ const fileSnippets = await readFileSnippets(relevantFiles, repoPath, requirement.text);
1344
+
1345
+ await new Promise(resolve => setTimeout(resolve, 500));
1346
+
1347
+ // ═══════════════════════════════════════════════════════════
1348
+ // ACT PHASE - GET STRUCTURED CHANGES FROM LLM
1349
+ // ═══════════════════════════════════════════════════════════
1350
+ printStatusCard(requirement.text, 'ACT');
1351
+
1352
+ console.log(chalk.cyan('šŸ¤– Asking LLM for implementation...\n'));
1353
+
1354
+ // Build context with actual file snippets
1355
+ let contextSection = '';
1356
+ if (fileSnippets.length > 0) {
1357
+ contextSection = '\n\nCURRENT CODE CONTEXT:\n';
1358
+ fileSnippets.forEach(({ file, snippet, startLine }) => {
1359
+ contextSection += `\n--- ${file} (around line ${startLine}) ---\n${snippet}\n`;
1360
+ });
1361
+ }
1362
+
1363
+ const prompt = `You are implementing a code change. Your task is to provide a SEARCH/REPLACE block that will modify the code.
1364
+
1365
+ REQUIREMENT TO IMPLEMENT:
1366
+ ${requirement.text}
1367
+ ${contextSection}
1368
+
1369
+ YOUR TASK:
1370
+ 1. Read the CURRENT CODE CONTEXT carefully
1371
+ 2. Find the EXACT location where changes are needed
1372
+ 3. COPY AT LEAST 10 LINES EXACTLY as they appear (including indentation, spacing, comments)
1373
+ 4. Show what the code should look like after your changes
1374
+
1375
+ OUTPUT FORMAT:
1376
+
1377
+ FILE: <exact path from the "---" line>
1378
+ SEARCH: \`\`\`javascript
1379
+ <COPY 10+ lines EXACTLY - preserve indentation, spacing, comments>
1380
+ \`\`\`
1381
+ REPLACE: \`\`\`javascript
1382
+ <SAME lines but with necessary modifications>
1383
+ \`\`\`
1384
+
1385
+ CRITICAL RULES - READ CAREFULLY:
1386
+ 1. SEARCH block must be COPIED CHARACTER-BY-CHARACTER from CURRENT CODE CONTEXT above
1387
+ 2. Use the EXACT code as it appears NOW - do NOT use old/outdated code from memory
1388
+ 3. Include ALL property values EXACTLY as shown (e.g., if context shows 'type: "info"', use 'type: "info"', NOT 'type: "setting"')
1389
+ 4. Include indentation EXACTLY as shown (count the spaces!)
1390
+ 5. Include AT LEAST 20-30 LINES of context:
1391
+ - Start 10-15 lines BEFORE the code you need to change
1392
+ - End 10-15 lines AFTER the code you need to change
1393
+ - MORE context is BETTER than less - include extra lines if unsure
1394
+ 6. Do NOT paraphrase or rewrite - COPY EXACTLY from CURRENT CODE CONTEXT
1395
+ 7. FILE path must match exactly what's after "---" in CURRENT CODE CONTEXT (DO NOT include the "---" itself)
1396
+ 8. Output ONLY the FILE/SEARCH/REPLACE block
1397
+ 9. NO explanations, NO markdown outside the blocks, NO additional text
1398
+ 10. If you cannot find the exact code to modify, output: ERROR: CANNOT LOCATE CODE IN CONTEXT
1399
+ 11. IMPORTANT: Include ALL intermediate code between the before/after context - do NOT skip lines
1400
+ 12. DOUBLE-CHECK: Compare your SEARCH block line-by-line with CURRENT CODE CONTEXT to ensure they match
1401
+
1402
+ EXAMPLE (notice EXACT copying of indentation and spacing):
1403
+
1404
+ FILE: packages/cli/src/utils/interactive.js
1405
+ SEARCH: \`\`\`javascript
1406
+ // Add warning if no TODO requirements
1407
+ if (counts.todoCount === 0) {
1408
+ requirementsText += \` \${chalk.red('āš ļø No requirements to work on')}\`;
1409
+ }
1410
+ } else {
1411
+ requirementsText += \`\${chalk.yellow('0 TODO')}, \${chalk.cyan('0 TO VERIFY')}, \${chalk.green('0 VERIFIED')} \${chalk.red('āš ļø No requirements')}\`;
1412
+ }
1413
+ \`\`\`
1414
+ REPLACE: \`\`\`javascript
1415
+ // Add warning if no TODO requirements
1416
+ if (counts.todoCount === 0) {
1417
+ requirementsText += \` \${chalk.red('āš ļø No requirements')}\`;
1418
+ }
1419
+ } else {
1420
+ requirementsText += \`\${chalk.yellow('0 TODO')}, \${chalk.cyan('0 TO VERIFY')}, \${chalk.green('0 VERIFIED')} \${chalk.red('āš ļø No requirements')}\`;
1421
+ }
1422
+ \`\`\`
1423
+
1424
+ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
1425
+
1426
+ let fullResponse = '';
1427
+
1428
+ const result = await llm.call(providerConfig, prompt, {
1429
+ temperature: 0.1,
1430
+ maxTokens: 4096,
1431
+ onChunk: (chunk) => {
1432
+ process.stdout.write(chalk.gray(chunk));
1433
+ fullResponse += chunk;
1434
+ },
1435
+ onComplete: () => {
1436
+ console.log('\n');
1437
+ },
1438
+ onError: (error) => {
1439
+ console.error(chalk.red(`\nāœ— Error: ${error}`));
1440
+ }
1441
+ });
1442
+
1443
+ if (!result.success) {
1444
+ const combinedError = [result.error, fullResponse].filter(Boolean).join('\n').trim() || 'Unknown error';
1445
+ console.log(chalk.red(`\nāœ— LLM call failed: ${combinedError}`));
1446
+ // CRITICAL: Mark provider as rate-limited for ANY error so acquireProviderConfig() will skip it
1447
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, combinedError);
1448
+ return { success: false, error: combinedError };
1449
+ }
1450
+
1451
+ // ═══════════════════════════════════════════════════════════
1452
+ // CLEAN UP PHASE - APPLY CHANGES TO ACTUAL FILES
1453
+ // ═══════════════════════════════════════════════════════════
1454
+ printStatusCard(requirement.text, 'CLEAN UP');
1455
+
1456
+ console.log(chalk.cyan('🧹 Parsing and applying changes...\n'));
1457
+
1458
+ // Check if LLM said it cannot locate code
1459
+ if (fullResponse.includes('ERROR: CANNOT LOCATE CODE') || fullResponse.includes('CANNOT LOCATE CODE')) {
1460
+ console.log(chalk.red('\nāœ— LLM could not locate the code to modify'));
1461
+ console.log(chalk.yellow('The code context provided may not contain the relevant section\n'));
1462
+ return { success: false, error: 'LLM could not locate code in context', changes: [] };
1463
+ }
1464
+
1465
+ const changes = parseSearchReplaceBlocks(fullResponse);
1466
+
1467
+ if (changes.length === 0) {
1468
+ if (fullResponse.includes('NO CHANGES NEEDED')) {
1469
+ console.log(chalk.yellow('āš ļø LLM determined no code changes needed\n'));
1470
+ return { success: false, error: 'No changes needed', changes: [] };
1471
+ } else {
1472
+ console.log(chalk.yellow('āš ļø Could not parse any search/replace blocks from LLM response\n'));
1473
+ console.log(chalk.gray('This might be a documentation-only requirement or LLM formatting issue\n'));
1474
+ return { success: false, error: 'No search/replace blocks found', changes: [] };
1475
+ }
1476
+ } else {
1477
+ console.log(chalk.white(`Applying ${changes.length} change(s):\n`));
1478
+
1479
+ let appliedCount = 0;
1480
+ let failedCount = 0;
1481
+
1482
+ for (let i = 0; i < changes.length; i++) {
1483
+ const change = changes[i];
1484
+ console.log(chalk.cyan(` ${i + 1}. ${change.file}...`));
1485
+
1486
+ const applyResult = await applyFileChange(change, repoPath);
1487
+
1488
+ if (applyResult.success) {
1489
+ const methodInfo = applyResult.method === 'fuzzy'
1490
+ ? ` (fuzzy match at line ${applyResult.matchedAt})`
1491
+ : '';
1492
+ console.log(chalk.green(` āœ“ Applied successfully${methodInfo}`));
1493
+ appliedCount++;
1494
+ } else {
1495
+ console.log(chalk.red(` āœ— Failed: ${applyResult.error}`));
1496
+ failedCount++;
1497
+ }
1498
+ }
1499
+
1500
+ console.log();
1501
+ console.log(chalk.white(`Applied: ${chalk.green(appliedCount)}, Failed: ${chalk.red(failedCount)}`));
1502
+ console.log();
1503
+
1504
+ // CRITICAL: Fail if no changes were applied successfully
1505
+ if (appliedCount === 0 && failedCount > 0) {
1506
+ console.log(chalk.bold.red('\nāŒ ITERATION FAILED\n'));
1507
+ console.log(chalk.red('No changes were successfully applied'));
1508
+ console.log(chalk.yellow('Common causes:'));
1509
+ console.log(chalk.gray(' - LLM provided incorrect search text'));
1510
+ console.log(chalk.gray(' - Code has changed since context was provided'));
1511
+ console.log(chalk.gray(' - File path is incorrect'));
1512
+ console.log();
1513
+ console.log(chalk.cyan('šŸ’” Tip: Check the search block matches the actual code in the file'));
1514
+ console.log();
1515
+ return { success: false, error: 'No changes applied', changes: [] };
1516
+ }
1517
+ }
1518
+
1519
+ await new Promise(resolve => setTimeout(resolve, 500));
1520
+
1521
+ // ═══════════════════════════════════════════════════════════
1522
+ // VERIFY PHASE - CONFIRM CHANGES WERE APPLIED
1523
+ // ═══════════════════════════════════════════════════════════
1524
+ printStatusCard(requirement.text, 'VERIFY');
1525
+
1526
+ console.log(chalk.cyan('āœ“ Verifying changes...\n'));
1527
+
1528
+ if (changes.length > 0) {
1529
+ console.log(chalk.white('Modified files:'));
1530
+ for (const change of changes) {
1531
+ const fullPath = path.join(repoPath, change.file);
1532
+ if (await fs.pathExists(fullPath)) {
1533
+ const content = await fs.readFile(fullPath, 'utf8');
1534
+ const hasChange = content.includes(change.replace.trim());
1535
+
1536
+ if (hasChange) {
1537
+ console.log(chalk.green(` āœ“ ${change.file}`));
1538
+ } else {
1539
+ console.log(chalk.yellow(` āš ļø ${change.file} (change may not have applied)`));
1540
+ }
1541
+ }
1542
+ }
1543
+ console.log();
1544
+ }
1545
+
1546
+ await new Promise(resolve => setTimeout(resolve, 500));
1547
+
1548
+ // ═══════════════════════════════════════════════════════════
1549
+ // DONE PHASE
1550
+ // ═══════════════════════════════════════════════════════════
1551
+ printStatusCard(requirement.text, 'DONE');
1552
+
1553
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1554
+
1555
+ console.log(chalk.bold.green('āœ… REQUIREMENT COMPLETED!\n'));
1556
+ console.log(chalk.white('Requirement:'), chalk.cyan(requirement.text));
1557
+ console.log(chalk.white('Files modified:'), chalk.cyan(changes.length));
1558
+ console.log(chalk.white('Status:'), chalk.green('Moving to TO VERIFY BY HUMAN'));
1559
+ console.log(chalk.white('Time:'), chalk.gray(`${elapsed}s`));
1560
+ console.log();
1561
+
1562
+ // Move requirement to TO VERIFY section
1563
+ const moved = await moveRequirementToVerify(repoPath, requirement.text);
1564
+ if (moved) {
1565
+ console.log(chalk.green('āœ“ Requirement moved to TO VERIFY BY HUMAN section'));
1566
+ } else {
1567
+ console.log(chalk.yellow('āš ļø Could not automatically move requirement'));
1568
+ }
1569
+
1570
+ // Record performance metric
1571
+ const duration = Date.now() - startTime;
1572
+ providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
1573
+
1574
+ console.log();
1575
+ console.log(chalk.gray('─'.repeat(80)));
1576
+ console.log();
1577
+
1578
+ return { success: true, changes };
1579
+ }
1580
+
1581
+ /**
1582
+ * Main auto mode command handler
1583
+ */
1584
+ async function handleAutoStart(options) {
1585
+ try {
1586
+ // STRICT AUTH CHECK
1587
+ const auth = require('../utils/auth');
1588
+ const isAuth = await auth.isAuthenticated();
1589
+ if (!isAuth) {
1590
+ console.log(chalk.cyan('\nšŸ” Opening browser for authentication...\n'));
1591
+ try {
1592
+ await auth.login();
1593
+ console.log(chalk.green('\nāœ“ Authentication successful!\n'));
1594
+ } catch (error) {
1595
+ console.log(chalk.red('\nāœ— Authentication failed:'), error.message);
1596
+ process.exit(1);
1597
+ }
1598
+ }
1599
+
1600
+ console.log(chalk.bold.cyan('\nšŸ¤– Vibe Coding Machine - Direct API Auto Mode\n'));
1601
+ console.log(chalk.gray('═'.repeat(80)));
1602
+ console.log();
1603
+
1604
+ // Get repo path
1605
+ const repoPath = await getRepoPath();
1606
+ if (!repoPath) {
1607
+ console.log(chalk.red('āœ— No repository configured'));
1608
+ console.log(chalk.gray('Run:'), chalk.cyan('ana repo:set <path>'));
1609
+ return;
1610
+ }
1611
+
1612
+ console.log(chalk.white('Repository:'), chalk.cyan(repoPath));
1613
+
1614
+ // Get provider configuration
1615
+ let providerConfig = await acquireProviderConfig();
1616
+ if (!providerConfig) {
1617
+ return;
1618
+ }
1619
+
1620
+ console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
1621
+
1622
+ // Get max chats
1623
+ const config = await getAutoConfig();
1624
+ const unlimited = !options.maxChats && !config.maxChats && config.neverStop;
1625
+ const maxChats = unlimited ? Number.MAX_SAFE_INTEGER : (options.maxChats || config.maxChats || 1);
1626
+ console.log(chalk.white('Max iterations:'), unlimited ? chalk.cyan('āˆž (never stop)') : chalk.cyan(maxChats));
1627
+ console.log();
1628
+ console.log(chalk.gray('═'.repeat(80)));
1629
+
1630
+ // Main loop
1631
+ let completedCount = 0;
1632
+ let failedCount = 0;
1633
+
1634
+ for (let i = 0; i < maxChats; i++) {
1635
+ console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
1636
+ console.log(chalk.bold.magenta(` ITERATION ${i + 1} of ${maxChats}`));
1637
+ console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
1638
+
1639
+ // Get current requirement
1640
+ const requirement = await getCurrentRequirement(repoPath);
1641
+ if (!requirement) {
1642
+ console.log(chalk.bold.yellow('\nšŸŽ‰ All requirements completed!'));
1643
+ console.log(chalk.gray('No more TODO items found in REQUIREMENTS file\n'));
1644
+ break;
1645
+ }
1646
+
1647
+ // Run iteration with full workflow
1648
+ const result = await runIteration(requirement, providerConfig, repoPath);
1649
+
1650
+ if (result.success) {
1651
+ completedCount++;
1652
+ console.log(chalk.bold.green(`āœ… Iteration ${i + 1}/${maxChats} COMPLETE`));
1653
+ console.log(chalk.gray('Moving to next requirement...\n'));
1654
+
1655
+ // Check if restart CLI is enabled and there are more iterations
1656
+ if (config.restartCLI && i < maxChats - 1) {
1657
+ console.log(chalk.cyan('šŸ”„ Restarting CLI to pick up latest changes...\n'));
1658
+
1659
+ // Calculate remaining iterations
1660
+ const remainingIterations = maxChats - (i + 1);
1661
+
1662
+ // Spawn new CLI process
1663
+ const { spawn } = require('child_process');
1664
+ const cliScriptPath = path.join(__dirname, '../../bin/vibecodingmachine.js');
1665
+ const args = ['auto:direct', '--max-chats', remainingIterations.toString()];
1666
+
1667
+ // Spawn without detached mode - child will inherit terminal
1668
+ // We'll exit after a brief delay to let child establish itself
1669
+ const child = spawn(process.execPath, [cliScriptPath, ...args], {
1670
+ stdio: 'inherit',
1671
+ cwd: process.cwd(),
1672
+ env: process.env
1673
+ });
1674
+
1675
+ // Handle child errors (but don't wait for completion)
1676
+ child.on('error', (err) => {
1677
+ console.error(chalk.red('Error restarting CLI:'), err.message);
1678
+ });
1679
+
1680
+ // Don't wait for child - unref so parent can exit
1681
+ child.unref();
1682
+
1683
+ // Give child a moment to start before exiting parent
1684
+ await new Promise(resolve => setTimeout(resolve, 300));
1685
+
1686
+ // Exit this process - child continues with terminal
1687
+ process.exit(0);
1688
+ } else {
1689
+ // Small delay before next iteration (if not restarting)
1690
+ if (i < maxChats - 1) {
1691
+ await new Promise(resolve => setTimeout(resolve, 2000));
1692
+ }
1693
+ }
1694
+ } else {
1695
+ // Check if it's a rate limit error (for logging purposes)
1696
+ const isRateLimitError = isRateLimitMessage(result.error);
1697
+ const errorType = isRateLimitError ? 'Rate limit' : 'Error';
1698
+
1699
+ console.log(chalk.yellow(`āš ļø ${errorType} detected, switching to next provider in your list...\n`));
1700
+
1701
+ const newProviderConfig = await acquireProviderConfig();
1702
+ if (newProviderConfig) {
1703
+ providerConfig = newProviderConfig;
1704
+ console.log(chalk.green(`āœ“ Switched to: ${providerConfig.displayName}\n`));
1705
+
1706
+ // Retry this iteration with new provider (don't increment i)
1707
+ i--;
1708
+ continue;
1709
+ } else {
1710
+ console.log(chalk.red('āœ— No alternative providers available\n'));
1711
+ }
1712
+
1713
+ failedCount++;
1714
+ console.log(chalk.bold.red(`āŒ Iteration ${i + 1}/${maxChats} FAILED`));
1715
+ console.log(chalk.red(`Error: ${result.error}\n`));
1716
+ console.log(chalk.yellow('Continuing to next requirement...\n'));
1717
+ }
1718
+ }
1719
+
1720
+ // Final Summary
1721
+ console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
1722
+ console.log(chalk.bold.magenta(` FINAL SUMMARY`));
1723
+ console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
1724
+
1725
+ console.log(chalk.white('Total iterations:'), unlimited ? chalk.cyan('āˆž (never stop)') : chalk.cyan(maxChats));
1726
+ console.log(chalk.white('Completed:'), chalk.green(`${completedCount} āœ“`));
1727
+ if (failedCount > 0) {
1728
+ console.log(chalk.white('Failed:'), chalk.red(`${failedCount} āœ—`));
1729
+ }
1730
+ console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
1731
+ console.log();
1732
+
1733
+ if (completedCount > 0) {
1734
+ console.log(chalk.bold.green(`šŸŽ‰ ${completedCount} requirement${completedCount > 1 ? 's' : ''} moved to TO VERIFY BY HUMAN!`));
1735
+ console.log(chalk.gray('Check the REQUIREMENTS file to verify and approve changes.\n'));
1736
+ }
1737
+
1738
+ } catch (error) {
1739
+ console.error(chalk.red('\nāœ— Fatal Error:'), error.message);
1740
+ if (error.stack) {
1741
+ console.log(chalk.gray(error.stack));
1742
+ }
1743
+ process.exit(1);
1744
+ }
1745
+ }
1746
+
1747
+ module.exports = { handleAutoStart };
1748
+