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