vibecodingmachine-cli 1.0.5 ā 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.allnightai/REQUIREMENTS.md +11 -11
- package/.allnightai/temp/auto-status.json +6 -0
- package/.env +7 -0
- package/.eslintrc.js +16 -16
- package/README.md +85 -85
- package/bin/vibecodingmachine.js +274 -274
- package/jest.config.js +8 -8
- package/logs/audit/2025-11-07.jsonl +2 -2
- package/package.json +62 -62
- package/scripts/README.md +128 -128
- package/scripts/auto-start-wrapper.sh +92 -92
- package/scripts/postinstall.js +81 -81
- package/src/commands/auth.js +96 -96
- package/src/commands/auto-direct.js +1748 -1748
- package/src/commands/auto.js +4692 -4692
- package/src/commands/auto.js.bak +710 -710
- package/src/commands/ide.js +70 -70
- package/src/commands/repo.js +159 -159
- package/src/commands/requirements.js +161 -161
- package/src/commands/setup.js +91 -91
- package/src/commands/status.js +88 -88
- package/src/index.js +5 -5
- package/src/utils/auth.js +571 -577
- package/src/utils/auto-mode-ansi-ui.js +238 -238
- package/src/utils/auto-mode-simple-ui.js +161 -161
- package/src/utils/auto-mode-ui.js.bak.blessed +207 -207
- package/src/utils/auto-mode.js +65 -65
- package/src/utils/config.js +64 -64
- package/src/utils/interactive.js +3616 -3616
- package/src/utils/keyboard-handler.js +153 -152
- package/src/utils/logger.js +4 -4
- package/src/utils/persistent-header.js +116 -116
- package/src/utils/provider-registry.js +128 -128
- package/src/utils/status-card.js +120 -120
- package/src/utils/stdout-interceptor.js +127 -127
- package/tests/auto-mode.test.js +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
|
+
|