@nclamvn/vibecode-cli 2.0.0 → 2.2.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/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/README.md +310 -49
- package/SESSION_NOTES.md +154 -0
- package/bin/vibecode.js +235 -2
- package/package.json +5 -2
- package/src/agent/decomposition.js +476 -0
- package/src/agent/index.js +391 -0
- package/src/agent/memory.js +542 -0
- package/src/agent/orchestrator.js +917 -0
- package/src/agent/self-healing.js +516 -0
- package/src/commands/agent.js +349 -0
- package/src/commands/ask.js +230 -0
- package/src/commands/assist.js +413 -0
- package/src/commands/build.js +345 -4
- package/src/commands/debug.js +565 -0
- package/src/commands/docs.js +167 -0
- package/src/commands/git.js +1024 -0
- package/src/commands/go.js +635 -0
- package/src/commands/learn.js +294 -0
- package/src/commands/migrate.js +341 -0
- package/src/commands/plan.js +8 -2
- package/src/commands/refactor.js +205 -0
- package/src/commands/review.js +126 -1
- package/src/commands/security.js +229 -0
- package/src/commands/shell.js +486 -0
- package/src/commands/templates.js +397 -0
- package/src/commands/test.js +194 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/watch.js +556 -0
- package/src/commands/wizard.js +322 -0
- package/src/config/constants.js +5 -1
- package/src/config/templates.js +146 -15
- package/src/core/backup.js +325 -0
- package/src/core/error-analyzer.js +237 -0
- package/src/core/fix-generator.js +195 -0
- package/src/core/iteration.js +226 -0
- package/src/core/learning.js +295 -0
- package/src/core/session.js +18 -2
- package/src/core/test-runner.js +281 -0
- package/src/debug/analyzer.js +329 -0
- package/src/debug/evidence.js +228 -0
- package/src/debug/fixer.js +348 -0
- package/src/debug/image-analyzer.js +304 -0
- package/src/debug/index.js +378 -0
- package/src/debug/verifier.js +346 -0
- package/src/index.js +102 -0
- package/src/providers/claude-code.js +12 -7
- package/src/templates/index.js +724 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
- package/src/utils/image.js +222 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Fix Generator
|
|
3
|
+
// Generate fix prompts for iterative builds
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import { formatErrors, createErrorSummary } from './error-analyzer.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a fix prompt based on errors from previous iteration
|
|
10
|
+
* @param {AnalyzedError[]} errors - Analyzed errors from test runner
|
|
11
|
+
* @param {string} originalPack - Original coder pack content
|
|
12
|
+
* @param {number} iteration - Current iteration number
|
|
13
|
+
* @returns {string} - Fix prompt for Claude Code
|
|
14
|
+
*/
|
|
15
|
+
export function generateFixPrompt(errors, originalPack, iteration = 1) {
|
|
16
|
+
const summary = createErrorSummary(errors);
|
|
17
|
+
|
|
18
|
+
const sections = [
|
|
19
|
+
'# 🔧 FIX REQUIRED - Iteration ' + iteration,
|
|
20
|
+
'',
|
|
21
|
+
`The previous build had **${errors.length} error(s)**. Please fix them.`,
|
|
22
|
+
'',
|
|
23
|
+
'---',
|
|
24
|
+
'',
|
|
25
|
+
'## 📋 Error Summary',
|
|
26
|
+
'',
|
|
27
|
+
`- **Total Errors:** ${summary.total}`,
|
|
28
|
+
`- **Critical:** ${summary.byPriority.critical}`,
|
|
29
|
+
`- **High:** ${summary.byPriority.high}`,
|
|
30
|
+
`- **Medium:** ${summary.byPriority.medium}`,
|
|
31
|
+
`- **Low:** ${summary.byPriority.low}`,
|
|
32
|
+
'',
|
|
33
|
+
`**Affected Files:** ${summary.files.length > 0 ? summary.files.join(', ') : 'Unknown'}`,
|
|
34
|
+
'',
|
|
35
|
+
'---',
|
|
36
|
+
'',
|
|
37
|
+
'## 🚨 Errors to Fix',
|
|
38
|
+
'',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Add detailed errors grouped by priority
|
|
42
|
+
const byPriority = groupByPriority(errors);
|
|
43
|
+
|
|
44
|
+
for (const [priority, priorityErrors] of Object.entries(byPriority)) {
|
|
45
|
+
if (priorityErrors.length === 0) continue;
|
|
46
|
+
|
|
47
|
+
const emoji = priority === 'critical' ? '🔴' :
|
|
48
|
+
priority === 'high' ? '🟠' :
|
|
49
|
+
priority === 'medium' ? '🟡' : '🟢';
|
|
50
|
+
|
|
51
|
+
sections.push(`### ${emoji} ${priority.toUpperCase()} Priority`);
|
|
52
|
+
sections.push('');
|
|
53
|
+
|
|
54
|
+
for (const error of priorityErrors) {
|
|
55
|
+
const location = error.file
|
|
56
|
+
? `\`${error.file}${error.line ? ':' + error.line : ''}\``
|
|
57
|
+
: 'Unknown location';
|
|
58
|
+
|
|
59
|
+
sections.push(`**${error.type}** at ${location}`);
|
|
60
|
+
sections.push(`- Message: ${error.message}`);
|
|
61
|
+
sections.push(`- Suggestion: ${error.suggestion}`);
|
|
62
|
+
if (error.raw && error.raw !== error.message) {
|
|
63
|
+
sections.push(`- Raw output: \`${truncate(error.raw, 200)}\``);
|
|
64
|
+
}
|
|
65
|
+
sections.push('');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sections.push('---');
|
|
70
|
+
sections.push('');
|
|
71
|
+
sections.push('## 📝 Original Task Reference');
|
|
72
|
+
sections.push('');
|
|
73
|
+
sections.push('<details>');
|
|
74
|
+
sections.push('<summary>Click to expand original task</summary>');
|
|
75
|
+
sections.push('');
|
|
76
|
+
sections.push(originalPack);
|
|
77
|
+
sections.push('');
|
|
78
|
+
sections.push('</details>');
|
|
79
|
+
sections.push('');
|
|
80
|
+
sections.push('---');
|
|
81
|
+
sections.push('');
|
|
82
|
+
sections.push('## ⚡ Fix Instructions');
|
|
83
|
+
sections.push('');
|
|
84
|
+
sections.push('1. **Fix ONLY the errors listed above** - Do not refactor or change working code');
|
|
85
|
+
sections.push('2. **Start with CRITICAL errors** - They likely cause cascading failures');
|
|
86
|
+
sections.push('3. **Run tests after each fix** - Verify the error is resolved');
|
|
87
|
+
sections.push('4. **Keep changes minimal** - Focus on the specific issue');
|
|
88
|
+
sections.push('');
|
|
89
|
+
sections.push('When all errors are fixed, the build will be validated again.');
|
|
90
|
+
sections.push('');
|
|
91
|
+
|
|
92
|
+
return sections.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate a minimal fix prompt for single error
|
|
97
|
+
*/
|
|
98
|
+
export function generateSingleFixPrompt(error) {
|
|
99
|
+
const location = error.file
|
|
100
|
+
? `${error.file}${error.line ? ':' + error.line : ''}`
|
|
101
|
+
: 'unknown location';
|
|
102
|
+
|
|
103
|
+
return `# Fix Required
|
|
104
|
+
|
|
105
|
+
**Error Type:** ${error.type}
|
|
106
|
+
**Location:** ${location}
|
|
107
|
+
**Message:** ${error.message}
|
|
108
|
+
|
|
109
|
+
**Suggestion:** ${error.suggestion}
|
|
110
|
+
|
|
111
|
+
Please fix this specific error. Keep the change minimal and focused.`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate iteration context for logging
|
|
116
|
+
*/
|
|
117
|
+
export function generateIterationContext(iteration, errors, duration) {
|
|
118
|
+
return {
|
|
119
|
+
iteration,
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
errorCount: errors.length,
|
|
122
|
+
errorTypes: [...new Set(errors.map(e => e.type))],
|
|
123
|
+
affectedFiles: [...new Set(errors.filter(e => e.file).map(e => e.file))],
|
|
124
|
+
duration
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Group errors by priority
|
|
130
|
+
*/
|
|
131
|
+
function groupByPriority(errors) {
|
|
132
|
+
const grouped = {
|
|
133
|
+
critical: [],
|
|
134
|
+
high: [],
|
|
135
|
+
medium: [],
|
|
136
|
+
low: []
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
for (const error of errors) {
|
|
140
|
+
const priority = error.priority || 'medium';
|
|
141
|
+
if (grouped[priority]) {
|
|
142
|
+
grouped[priority].push(error);
|
|
143
|
+
} else {
|
|
144
|
+
grouped.medium.push(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return grouped;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Truncate string with ellipsis
|
|
153
|
+
*/
|
|
154
|
+
function truncate(str, maxLen) {
|
|
155
|
+
if (!str) return '';
|
|
156
|
+
if (str.length <= maxLen) return str;
|
|
157
|
+
return str.substring(0, maxLen - 3) + '...';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if errors are fixable (not system/config errors)
|
|
162
|
+
*/
|
|
163
|
+
export function areErrorsFixable(errors) {
|
|
164
|
+
// If all errors are unknown type with no file info, might be config issue
|
|
165
|
+
const unknownWithoutFile = errors.filter(e => e.type === 'unknown' && !e.file);
|
|
166
|
+
|
|
167
|
+
if (unknownWithoutFile.length === errors.length) {
|
|
168
|
+
return {
|
|
169
|
+
fixable: false,
|
|
170
|
+
reason: 'All errors are unstructured with no file information. This may indicate a configuration or environment issue.'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { fixable: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Estimate fix complexity
|
|
179
|
+
*/
|
|
180
|
+
export function estimateFixComplexity(errors) {
|
|
181
|
+
let score = 0;
|
|
182
|
+
|
|
183
|
+
for (const error of errors) {
|
|
184
|
+
switch (error.priority) {
|
|
185
|
+
case 'critical': score += 3; break;
|
|
186
|
+
case 'high': score += 2; break;
|
|
187
|
+
case 'medium': score += 1; break;
|
|
188
|
+
case 'low': score += 0.5; break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (score <= 3) return 'simple';
|
|
193
|
+
if (score <= 8) return 'moderate';
|
|
194
|
+
return 'complex';
|
|
195
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Iteration Tracker
|
|
3
|
+
// Manage and track build-test-fix iterations
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { ensureDir, writeJson, readJson, pathExists, appendToFile } from '../utils/files.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} IterationRecord
|
|
11
|
+
* @property {number} iteration - Iteration number
|
|
12
|
+
* @property {string} timestamp - ISO timestamp
|
|
13
|
+
* @property {boolean} passed - Whether tests passed
|
|
14
|
+
* @property {number} errorCount - Number of errors
|
|
15
|
+
* @property {string[]} errorTypes - Types of errors found
|
|
16
|
+
* @property {string[]} affectedFiles - Files with errors
|
|
17
|
+
* @property {number} duration - Duration in ms
|
|
18
|
+
* @property {string} action - What action was taken (build/fix)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} IterationState
|
|
23
|
+
* @property {string} sessionId - Session identifier
|
|
24
|
+
* @property {string} startTime - When iteration started
|
|
25
|
+
* @property {number} currentIteration - Current iteration number
|
|
26
|
+
* @property {number} maxIterations - Max allowed iterations
|
|
27
|
+
* @property {IterationRecord[]} history - History of iterations
|
|
28
|
+
* @property {boolean} completed - Whether iteration loop completed
|
|
29
|
+
* @property {string} result - Final result (success/max_reached/error)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create new iteration state
|
|
34
|
+
* @param {string} sessionId - Session identifier
|
|
35
|
+
* @param {number} maxIterations - Maximum iterations allowed
|
|
36
|
+
* @returns {IterationState}
|
|
37
|
+
*/
|
|
38
|
+
export function createIterationState(sessionId, maxIterations = 3) {
|
|
39
|
+
return {
|
|
40
|
+
sessionId,
|
|
41
|
+
startTime: new Date().toISOString(),
|
|
42
|
+
currentIteration: 0,
|
|
43
|
+
maxIterations,
|
|
44
|
+
history: [],
|
|
45
|
+
completed: false,
|
|
46
|
+
result: null
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Record an iteration result
|
|
52
|
+
* @param {IterationState} state - Current iteration state
|
|
53
|
+
* @param {Object} result - Iteration result
|
|
54
|
+
* @returns {IterationState} - Updated state
|
|
55
|
+
*/
|
|
56
|
+
export function recordIteration(state, result) {
|
|
57
|
+
const record = {
|
|
58
|
+
iteration: state.currentIteration + 1,
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
passed: result.passed,
|
|
61
|
+
errorCount: result.errorCount || 0,
|
|
62
|
+
errorTypes: result.errorTypes || [],
|
|
63
|
+
affectedFiles: result.affectedFiles || [],
|
|
64
|
+
duration: result.duration || 0,
|
|
65
|
+
action: result.action || 'build'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
currentIteration: state.currentIteration + 1,
|
|
71
|
+
history: [...state.history, record]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if can continue iterating
|
|
77
|
+
* @param {IterationState} state - Current state
|
|
78
|
+
* @returns {{canContinue: boolean, reason: string}}
|
|
79
|
+
*/
|
|
80
|
+
export function canContinue(state) {
|
|
81
|
+
if (state.completed) {
|
|
82
|
+
return { canContinue: false, reason: 'Iteration already completed' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (state.currentIteration >= state.maxIterations) {
|
|
86
|
+
return { canContinue: false, reason: `Max iterations (${state.maxIterations}) reached` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if last iteration passed
|
|
90
|
+
const lastRecord = state.history[state.history.length - 1];
|
|
91
|
+
if (lastRecord && lastRecord.passed) {
|
|
92
|
+
return { canContinue: false, reason: 'Tests passed - no more iterations needed' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for stuck loop (same errors repeated 3 times)
|
|
96
|
+
if (state.history.length >= 3) {
|
|
97
|
+
const last3 = state.history.slice(-3);
|
|
98
|
+
const errorCounts = last3.map(r => r.errorCount);
|
|
99
|
+
if (errorCounts.every(c => c === errorCounts[0]) && errorCounts[0] > 0) {
|
|
100
|
+
return { canContinue: false, reason: 'Stuck in loop - same error count for 3 iterations' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { canContinue: true, reason: '' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Finalize iteration state
|
|
109
|
+
* @param {IterationState} state - Current state
|
|
110
|
+
* @param {string} result - Result type (success/max_reached/error/stuck)
|
|
111
|
+
* @returns {IterationState}
|
|
112
|
+
*/
|
|
113
|
+
export function finalizeIterationState(state, result) {
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
completed: true,
|
|
117
|
+
result,
|
|
118
|
+
endTime: new Date().toISOString(),
|
|
119
|
+
totalDuration: state.history.reduce((sum, r) => sum + r.duration, 0)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Save iteration state to session directory
|
|
125
|
+
* @param {string} sessionDir - Session directory path
|
|
126
|
+
* @param {IterationState} state - Iteration state
|
|
127
|
+
*/
|
|
128
|
+
export async function saveIterationState(sessionDir, state) {
|
|
129
|
+
const iterationDir = path.join(sessionDir, 'iterations');
|
|
130
|
+
await ensureDir(iterationDir);
|
|
131
|
+
|
|
132
|
+
const stateFile = path.join(iterationDir, 'state.json');
|
|
133
|
+
await writeJson(stateFile, state, { spaces: 2 });
|
|
134
|
+
|
|
135
|
+
// Also write individual iteration files for evidence
|
|
136
|
+
for (const record of state.history) {
|
|
137
|
+
const recordFile = path.join(iterationDir, `iteration-${record.iteration}.json`);
|
|
138
|
+
if (!await pathExists(recordFile)) {
|
|
139
|
+
await writeJson(recordFile, record, { spaces: 2 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Load iteration state from session directory
|
|
146
|
+
* @param {string} sessionDir - Session directory path
|
|
147
|
+
* @returns {Promise<IterationState|null>}
|
|
148
|
+
*/
|
|
149
|
+
export async function loadIterationState(sessionDir) {
|
|
150
|
+
const stateFile = path.join(sessionDir, 'iterations', 'state.json');
|
|
151
|
+
if (await pathExists(stateFile)) {
|
|
152
|
+
return await readJson(stateFile);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format iteration summary for display
|
|
159
|
+
* @param {IterationState} state - Iteration state
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
export function formatIterationSummary(state) {
|
|
163
|
+
const lines = [];
|
|
164
|
+
|
|
165
|
+
lines.push(`Iteration Summary (${state.sessionId})`);
|
|
166
|
+
lines.push('═'.repeat(50));
|
|
167
|
+
lines.push(`Total Iterations: ${state.currentIteration}/${state.maxIterations}`);
|
|
168
|
+
lines.push(`Result: ${state.result || 'In Progress'}`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
|
|
171
|
+
if (state.history.length > 0) {
|
|
172
|
+
lines.push('History:');
|
|
173
|
+
for (const record of state.history) {
|
|
174
|
+
const status = record.passed ? '✅' : '❌';
|
|
175
|
+
const errors = record.errorCount > 0 ? ` (${record.errorCount} errors)` : '';
|
|
176
|
+
lines.push(` ${record.iteration}. ${status} ${record.action}${errors} - ${formatDuration(record.duration)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (state.totalDuration) {
|
|
181
|
+
lines.push('');
|
|
182
|
+
lines.push(`Total Duration: ${formatDuration(state.totalDuration)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Format duration in human readable format
|
|
190
|
+
*/
|
|
191
|
+
function formatDuration(ms) {
|
|
192
|
+
if (ms < 1000) return `${ms}ms`;
|
|
193
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
194
|
+
return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Write iteration log entry
|
|
199
|
+
*/
|
|
200
|
+
export async function logIteration(logPath, iteration, message) {
|
|
201
|
+
const timestamp = new Date().toISOString();
|
|
202
|
+
const entry = `[${timestamp}] [Iteration ${iteration}] ${message}\n`;
|
|
203
|
+
await appendToFile(logPath, entry);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get progress percentage
|
|
208
|
+
*/
|
|
209
|
+
export function getProgressPercent(state) {
|
|
210
|
+
if (state.completed && state.result === 'success') {
|
|
211
|
+
return 100;
|
|
212
|
+
}
|
|
213
|
+
return Math.round((state.currentIteration / state.maxIterations) * 100);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if errors are improving (decreasing)
|
|
218
|
+
*/
|
|
219
|
+
export function isImproving(state) {
|
|
220
|
+
if (state.history.length < 2) return true;
|
|
221
|
+
|
|
222
|
+
const last = state.history[state.history.length - 1];
|
|
223
|
+
const prev = state.history[state.history.length - 2];
|
|
224
|
+
|
|
225
|
+
return last.errorCount < prev.errorCount;
|
|
226
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Learning Engine
|
|
3
|
+
// Phase H5: AI learns from user feedback
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
const LEARNING_DIR = '.vibecode/learning';
|
|
11
|
+
const GLOBAL_LEARNING_DIR = path.join(os.homedir(), '.vibecode/learning');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Learning Engine - Records and retrieves learning data
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Records fix attempts and outcomes
|
|
18
|
+
* - Stores user preferences
|
|
19
|
+
* - Provides suggestions based on past successes
|
|
20
|
+
* - Anonymizes data for global storage
|
|
21
|
+
*/
|
|
22
|
+
export class LearningEngine {
|
|
23
|
+
constructor(projectPath = process.cwd()) {
|
|
24
|
+
this.projectPath = projectPath;
|
|
25
|
+
this.localPath = path.join(projectPath, LEARNING_DIR);
|
|
26
|
+
this.globalPath = GLOBAL_LEARNING_DIR;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize learning directories
|
|
31
|
+
*/
|
|
32
|
+
async init() {
|
|
33
|
+
await fs.mkdir(this.localPath, { recursive: true });
|
|
34
|
+
await fs.mkdir(this.globalPath, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Record a fix attempt and its outcome
|
|
39
|
+
*/
|
|
40
|
+
async recordFix(fixData) {
|
|
41
|
+
await this.init();
|
|
42
|
+
|
|
43
|
+
const record = {
|
|
44
|
+
id: Date.now().toString(36),
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
errorType: fixData.errorType,
|
|
47
|
+
errorMessage: fixData.errorMessage?.substring(0, 200),
|
|
48
|
+
errorCategory: fixData.errorCategory,
|
|
49
|
+
fixApplied: fixData.fixApplied?.substring(0, 500),
|
|
50
|
+
success: fixData.success,
|
|
51
|
+
userFeedback: fixData.userFeedback,
|
|
52
|
+
userCorrection: fixData.userCorrection,
|
|
53
|
+
projectType: await this.detectProjectType(),
|
|
54
|
+
tags: fixData.tags || []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Save to local project
|
|
58
|
+
const localFile = path.join(this.localPath, 'fixes.json');
|
|
59
|
+
const localFixes = await this.loadJson(localFile, []);
|
|
60
|
+
localFixes.push(record);
|
|
61
|
+
await this.saveJson(localFile, localFixes.slice(-100)); // Keep last 100
|
|
62
|
+
|
|
63
|
+
// Save to global (anonymized)
|
|
64
|
+
const globalFile = path.join(this.globalPath, 'fixes.json');
|
|
65
|
+
const globalFixes = await this.loadJson(globalFile, []);
|
|
66
|
+
globalFixes.push({
|
|
67
|
+
...record,
|
|
68
|
+
errorMessage: this.anonymize(record.errorMessage),
|
|
69
|
+
fixApplied: this.anonymize(record.fixApplied)
|
|
70
|
+
});
|
|
71
|
+
await this.saveJson(globalFile, globalFixes.slice(-500)); // Keep last 500
|
|
72
|
+
|
|
73
|
+
return record.id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Record user preference
|
|
78
|
+
*/
|
|
79
|
+
async recordPreference(key, value, context = {}) {
|
|
80
|
+
await this.init();
|
|
81
|
+
|
|
82
|
+
const prefsFile = path.join(this.localPath, 'preferences.json');
|
|
83
|
+
const prefs = await this.loadJson(prefsFile, {});
|
|
84
|
+
|
|
85
|
+
if (!prefs[key]) {
|
|
86
|
+
prefs[key] = { values: [], contexts: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
prefs[key].values.push(value);
|
|
90
|
+
prefs[key].contexts.push(context);
|
|
91
|
+
prefs[key].lastUsed = new Date().toISOString();
|
|
92
|
+
|
|
93
|
+
// Keep only recent values
|
|
94
|
+
prefs[key].values = prefs[key].values.slice(-20);
|
|
95
|
+
prefs[key].contexts = prefs[key].contexts.slice(-20);
|
|
96
|
+
|
|
97
|
+
await this.saveJson(prefsFile, prefs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get suggestion based on learnings
|
|
102
|
+
*/
|
|
103
|
+
async getSuggestion(errorType, errorCategory) {
|
|
104
|
+
const localFixes = await this.loadJson(
|
|
105
|
+
path.join(this.localPath, 'fixes.json'),
|
|
106
|
+
[]
|
|
107
|
+
);
|
|
108
|
+
const globalFixes = await this.loadJson(
|
|
109
|
+
path.join(this.globalPath, 'fixes.json'),
|
|
110
|
+
[]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Find similar successful fixes
|
|
114
|
+
const allFixes = [...localFixes, ...globalFixes];
|
|
115
|
+
const similarFixes = allFixes.filter(f =>
|
|
116
|
+
f.success &&
|
|
117
|
+
(f.errorType === errorType || f.errorCategory === errorCategory)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (similarFixes.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Calculate confidence based on success rate
|
|
125
|
+
const totalSimilar = allFixes.filter(f =>
|
|
126
|
+
f.errorType === errorType || f.errorCategory === errorCategory
|
|
127
|
+
).length;
|
|
128
|
+
|
|
129
|
+
const successRate = similarFixes.length / totalSimilar;
|
|
130
|
+
|
|
131
|
+
// Get most recent successful fix
|
|
132
|
+
const recentFix = similarFixes.sort((a, b) =>
|
|
133
|
+
new Date(b.timestamp) - new Date(a.timestamp)
|
|
134
|
+
)[0];
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
suggestion: recentFix.fixApplied,
|
|
138
|
+
confidence: successRate,
|
|
139
|
+
basedOn: similarFixes.length,
|
|
140
|
+
lastUsed: recentFix.timestamp
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get user preference
|
|
146
|
+
*/
|
|
147
|
+
async getPreference(key, defaultValue = null) {
|
|
148
|
+
const prefsFile = path.join(this.localPath, 'preferences.json');
|
|
149
|
+
const prefs = await this.loadJson(prefsFile, {});
|
|
150
|
+
|
|
151
|
+
if (!prefs[key] || prefs[key].values.length === 0) {
|
|
152
|
+
return defaultValue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Return most common value
|
|
156
|
+
const counts = {};
|
|
157
|
+
for (const v of prefs[key].values) {
|
|
158
|
+
counts[v] = (counts[v] || 0) + 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
162
|
+
return sorted[0][0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get learning statistics
|
|
167
|
+
*/
|
|
168
|
+
async getStats() {
|
|
169
|
+
const localFixes = await this.loadJson(
|
|
170
|
+
path.join(this.localPath, 'fixes.json'),
|
|
171
|
+
[]
|
|
172
|
+
);
|
|
173
|
+
const globalFixes = await this.loadJson(
|
|
174
|
+
path.join(this.globalPath, 'fixes.json'),
|
|
175
|
+
[]
|
|
176
|
+
);
|
|
177
|
+
const prefs = await this.loadJson(
|
|
178
|
+
path.join(this.localPath, 'preferences.json'),
|
|
179
|
+
{}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const localSuccess = localFixes.filter(f => f.success).length;
|
|
183
|
+
const globalSuccess = globalFixes.filter(f => f.success).length;
|
|
184
|
+
|
|
185
|
+
// Group by error category
|
|
186
|
+
const byCategory = {};
|
|
187
|
+
for (const fix of localFixes) {
|
|
188
|
+
const cat = fix.errorCategory || 'unknown';
|
|
189
|
+
if (!byCategory[cat]) {
|
|
190
|
+
byCategory[cat] = { total: 0, success: 0 };
|
|
191
|
+
}
|
|
192
|
+
byCategory[cat].total++;
|
|
193
|
+
if (fix.success) byCategory[cat].success++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
local: {
|
|
198
|
+
total: localFixes.length,
|
|
199
|
+
success: localSuccess,
|
|
200
|
+
rate: localFixes.length > 0 ? (localSuccess / localFixes.length * 100).toFixed(1) : '0'
|
|
201
|
+
},
|
|
202
|
+
global: {
|
|
203
|
+
total: globalFixes.length,
|
|
204
|
+
success: globalSuccess,
|
|
205
|
+
rate: globalFixes.length > 0 ? (globalSuccess / globalFixes.length * 100).toFixed(1) : '0'
|
|
206
|
+
},
|
|
207
|
+
byCategory,
|
|
208
|
+
preferences: Object.keys(prefs).length,
|
|
209
|
+
lastLearning: localFixes[localFixes.length - 1]?.timestamp || null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Detect project type
|
|
215
|
+
*/
|
|
216
|
+
async detectProjectType() {
|
|
217
|
+
try {
|
|
218
|
+
const pkgPath = path.join(this.projectPath, 'package.json');
|
|
219
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
220
|
+
|
|
221
|
+
if (pkg.dependencies?.next) return 'nextjs';
|
|
222
|
+
if (pkg.dependencies?.react) return 'react';
|
|
223
|
+
if (pkg.dependencies?.vue) return 'vue';
|
|
224
|
+
if (pkg.dependencies?.express) return 'express';
|
|
225
|
+
if (pkg.dependencies?.['@prisma/client']) return 'prisma';
|
|
226
|
+
|
|
227
|
+
return 'node';
|
|
228
|
+
} catch {
|
|
229
|
+
return 'unknown';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Anonymize sensitive data for global storage
|
|
235
|
+
*/
|
|
236
|
+
anonymize(text) {
|
|
237
|
+
if (!text) return text;
|
|
238
|
+
return text
|
|
239
|
+
.replace(/\/Users\/[^\/\s]+/g, '/Users/***')
|
|
240
|
+
.replace(/\/home\/[^\/\s]+/g, '/home/***')
|
|
241
|
+
.replace(/C:\\Users\\[^\\]+/g, 'C:\\Users\\***')
|
|
242
|
+
.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '***@***.***')
|
|
243
|
+
.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '***.***.***.***')
|
|
244
|
+
.replace(/api[_-]?key[=:]\s*["']?[\w-]+["']?/gi, 'api_key=***')
|
|
245
|
+
.replace(/token[=:]\s*["']?[\w-]+["']?/gi, 'token=***')
|
|
246
|
+
.replace(/password[=:]\s*["']?[^"'\s]+["']?/gi, 'password=***');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clear all local learnings
|
|
251
|
+
*/
|
|
252
|
+
async clearLocal() {
|
|
253
|
+
await this.saveJson(path.join(this.localPath, 'fixes.json'), []);
|
|
254
|
+
await this.saveJson(path.join(this.localPath, 'preferences.json'), {});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Load JSON file
|
|
259
|
+
*/
|
|
260
|
+
async loadJson(filePath, defaultValue) {
|
|
261
|
+
try {
|
|
262
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
263
|
+
return JSON.parse(content);
|
|
264
|
+
} catch {
|
|
265
|
+
return defaultValue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save JSON file
|
|
271
|
+
*/
|
|
272
|
+
async saveJson(filePath, data) {
|
|
273
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Singleton instance
|
|
278
|
+
let learningEngine = null;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get or create LearningEngine instance
|
|
282
|
+
*/
|
|
283
|
+
export function getLearningEngine(projectPath = process.cwd()) {
|
|
284
|
+
if (!learningEngine || learningEngine.projectPath !== projectPath) {
|
|
285
|
+
learningEngine = new LearningEngine(projectPath);
|
|
286
|
+
}
|
|
287
|
+
return learningEngine;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a new LearningEngine instance
|
|
292
|
+
*/
|
|
293
|
+
export function createLearningEngine(projectPath = process.cwd()) {
|
|
294
|
+
return new LearningEngine(projectPath);
|
|
295
|
+
}
|