@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.
Files changed (53) hide show
  1. package/.vibecode/learning/fixes.json +1 -0
  2. package/.vibecode/learning/preferences.json +1 -0
  3. package/README.md +310 -49
  4. package/SESSION_NOTES.md +154 -0
  5. package/bin/vibecode.js +235 -2
  6. package/package.json +5 -2
  7. package/src/agent/decomposition.js +476 -0
  8. package/src/agent/index.js +391 -0
  9. package/src/agent/memory.js +542 -0
  10. package/src/agent/orchestrator.js +917 -0
  11. package/src/agent/self-healing.js +516 -0
  12. package/src/commands/agent.js +349 -0
  13. package/src/commands/ask.js +230 -0
  14. package/src/commands/assist.js +413 -0
  15. package/src/commands/build.js +345 -4
  16. package/src/commands/debug.js +565 -0
  17. package/src/commands/docs.js +167 -0
  18. package/src/commands/git.js +1024 -0
  19. package/src/commands/go.js +635 -0
  20. package/src/commands/learn.js +294 -0
  21. package/src/commands/migrate.js +341 -0
  22. package/src/commands/plan.js +8 -2
  23. package/src/commands/refactor.js +205 -0
  24. package/src/commands/review.js +126 -1
  25. package/src/commands/security.js +229 -0
  26. package/src/commands/shell.js +486 -0
  27. package/src/commands/templates.js +397 -0
  28. package/src/commands/test.js +194 -0
  29. package/src/commands/undo.js +281 -0
  30. package/src/commands/watch.js +556 -0
  31. package/src/commands/wizard.js +322 -0
  32. package/src/config/constants.js +5 -1
  33. package/src/config/templates.js +146 -15
  34. package/src/core/backup.js +325 -0
  35. package/src/core/error-analyzer.js +237 -0
  36. package/src/core/fix-generator.js +195 -0
  37. package/src/core/iteration.js +226 -0
  38. package/src/core/learning.js +295 -0
  39. package/src/core/session.js +18 -2
  40. package/src/core/test-runner.js +281 -0
  41. package/src/debug/analyzer.js +329 -0
  42. package/src/debug/evidence.js +228 -0
  43. package/src/debug/fixer.js +348 -0
  44. package/src/debug/image-analyzer.js +304 -0
  45. package/src/debug/index.js +378 -0
  46. package/src/debug/verifier.js +346 -0
  47. package/src/index.js +102 -0
  48. package/src/providers/claude-code.js +12 -7
  49. package/src/templates/index.js +724 -0
  50. package/src/ui/__tests__/error-translator.test.js +390 -0
  51. package/src/ui/dashboard.js +364 -0
  52. package/src/ui/error-translator.js +775 -0
  53. 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
+ }