@nclamvn/vibecode-cli 1.5.0 → 1.6.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/bin/vibecode.js +38 -2
- package/package.json +4 -2
- package/src/agent/orchestrator.js +104 -35
- package/src/commands/build.js +13 -3
- package/src/commands/go.js +9 -2
- package/src/commands/learn.js +294 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/wizard.js +322 -0
- package/src/core/backup.js +325 -0
- package/src/core/learning.js +295 -0
- package/src/debug/index.js +30 -1
- package/src/index.js +31 -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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Backup Manager
|
|
3
|
+
// Phase H4: Undo/Rollback - Auto-backup before every action
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
const BACKUP_DIR = '.vibecode/backups';
|
|
11
|
+
const MAX_BACKUPS = 10;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BackupManager - Creates and manages backups for undo functionality
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const backup = new BackupManager();
|
|
18
|
+
* const id = await backup.createBackup('build');
|
|
19
|
+
* // ... do stuff ...
|
|
20
|
+
* await backup.restore(id); // Undo!
|
|
21
|
+
*/
|
|
22
|
+
export class BackupManager {
|
|
23
|
+
constructor(projectPath = process.cwd()) {
|
|
24
|
+
this.projectPath = projectPath;
|
|
25
|
+
this.backupPath = path.join(projectPath, BACKUP_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize backup directory
|
|
30
|
+
*/
|
|
31
|
+
async init() {
|
|
32
|
+
await fs.ensureDir(this.backupPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create backup before action
|
|
37
|
+
* @param {string} actionName - Name of action (build, agent, go, etc.)
|
|
38
|
+
* @param {string[]} files - Specific files to backup (optional)
|
|
39
|
+
* @returns {string} Backup ID
|
|
40
|
+
*/
|
|
41
|
+
async createBackup(actionName, files = null) {
|
|
42
|
+
await this.init();
|
|
43
|
+
|
|
44
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
45
|
+
const backupId = `${timestamp}_${actionName}`;
|
|
46
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
47
|
+
|
|
48
|
+
await fs.ensureDir(backupDir);
|
|
49
|
+
|
|
50
|
+
// If specific files provided, backup only those
|
|
51
|
+
// Otherwise, backup common source files
|
|
52
|
+
const filesToBackup = files || await this.getSourceFiles();
|
|
53
|
+
|
|
54
|
+
const manifest = {
|
|
55
|
+
id: backupId,
|
|
56
|
+
action: actionName,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
files: []
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const file of filesToBackup) {
|
|
62
|
+
try {
|
|
63
|
+
const sourcePath = path.join(this.projectPath, file);
|
|
64
|
+
const stats = await fs.stat(sourcePath).catch(() => null);
|
|
65
|
+
|
|
66
|
+
if (stats && stats.isFile()) {
|
|
67
|
+
const content = await fs.readFile(sourcePath);
|
|
68
|
+
const hash = createHash('md5').update(content).digest('hex');
|
|
69
|
+
|
|
70
|
+
// Replace slashes with __ for flat storage
|
|
71
|
+
const backupFileName = file.replace(/[/\\]/g, '__');
|
|
72
|
+
const backupFilePath = path.join(backupDir, backupFileName);
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(backupFilePath, content);
|
|
75
|
+
|
|
76
|
+
manifest.files.push({
|
|
77
|
+
path: file,
|
|
78
|
+
hash,
|
|
79
|
+
size: stats.size,
|
|
80
|
+
backupName: backupFileName
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Skip files that can't be backed up
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Save manifest
|
|
89
|
+
await fs.writeFile(
|
|
90
|
+
path.join(backupDir, 'manifest.json'),
|
|
91
|
+
JSON.stringify(manifest, null, 2)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Cleanup old backups
|
|
95
|
+
await this.cleanupOldBackups();
|
|
96
|
+
|
|
97
|
+
return backupId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get list of source files to backup
|
|
102
|
+
* @returns {string[]} File paths relative to project
|
|
103
|
+
*/
|
|
104
|
+
async getSourceFiles() {
|
|
105
|
+
const files = [];
|
|
106
|
+
const ignoreDirs = [
|
|
107
|
+
'node_modules', '.git', '.next', 'dist', 'build',
|
|
108
|
+
'.vibecode/backups', 'coverage', '.cache', '__pycache__'
|
|
109
|
+
];
|
|
110
|
+
const extensions = [
|
|
111
|
+
'.js', '.ts', '.tsx', '.jsx', '.json', '.css', '.scss',
|
|
112
|
+
'.html', '.md', '.vue', '.svelte', '.prisma', '.env'
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const scan = async (dir, prefix = '') => {
|
|
116
|
+
try {
|
|
117
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const fullPath = path.join(dir, entry.name);
|
|
121
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
122
|
+
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith('.')) {
|
|
125
|
+
await scan(fullPath, relativePath);
|
|
126
|
+
}
|
|
127
|
+
} else if (entry.isFile()) {
|
|
128
|
+
const ext = path.extname(entry.name);
|
|
129
|
+
if (extensions.includes(ext)) {
|
|
130
|
+
files.push(relativePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Skip directories that can't be read
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await scan(this.projectPath);
|
|
140
|
+
return files.slice(0, 100); // Limit to 100 files
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* List available backups
|
|
145
|
+
* @returns {Object[]} Array of backup manifests
|
|
146
|
+
*/
|
|
147
|
+
async listBackups() {
|
|
148
|
+
await this.init();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const entries = await fs.readdir(this.backupPath, { withFileTypes: true });
|
|
152
|
+
const backups = [];
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
const manifestPath = path.join(this.backupPath, entry.name, 'manifest.json');
|
|
157
|
+
try {
|
|
158
|
+
const manifest = await fs.readJson(manifestPath);
|
|
159
|
+
backups.push(manifest);
|
|
160
|
+
} catch {
|
|
161
|
+
// Skip invalid backups
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by timestamp, newest first
|
|
167
|
+
return backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Restore from backup
|
|
175
|
+
* @param {string} backupId - Backup ID to restore
|
|
176
|
+
* @returns {Object} Restore result
|
|
177
|
+
*/
|
|
178
|
+
async restore(backupId) {
|
|
179
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
180
|
+
const manifestPath = path.join(backupDir, 'manifest.json');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const manifest = await fs.readJson(manifestPath);
|
|
184
|
+
const restored = [];
|
|
185
|
+
|
|
186
|
+
for (const file of manifest.files) {
|
|
187
|
+
const backupFilePath = path.join(backupDir, file.backupName);
|
|
188
|
+
const targetPath = path.join(this.projectPath, file.path);
|
|
189
|
+
|
|
190
|
+
// Ensure directory exists
|
|
191
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
192
|
+
|
|
193
|
+
// Restore file
|
|
194
|
+
const content = await fs.readFile(backupFilePath);
|
|
195
|
+
await fs.writeFile(targetPath, content);
|
|
196
|
+
|
|
197
|
+
restored.push(file.path);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
backupId,
|
|
203
|
+
action: manifest.action,
|
|
204
|
+
timestamp: manifest.timestamp,
|
|
205
|
+
filesRestored: restored.length,
|
|
206
|
+
files: restored
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: error.message
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Restore to N steps ago
|
|
218
|
+
* @param {number} steps - Number of steps back
|
|
219
|
+
* @returns {Object} Restore result
|
|
220
|
+
*/
|
|
221
|
+
async restoreSteps(steps = 1) {
|
|
222
|
+
const backups = await this.listBackups();
|
|
223
|
+
|
|
224
|
+
if (steps > backups.length) {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
error: `Only ${backups.length} backups available`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const backup = backups[steps - 1];
|
|
232
|
+
return await this.restore(backup.id);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the most recent backup
|
|
237
|
+
* @returns {Object|null} Latest backup manifest or null
|
|
238
|
+
*/
|
|
239
|
+
async getLatestBackup() {
|
|
240
|
+
const backups = await this.listBackups();
|
|
241
|
+
return backups.length > 0 ? backups[0] : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Cleanup old backups to maintain MAX_BACKUPS limit
|
|
246
|
+
*/
|
|
247
|
+
async cleanupOldBackups() {
|
|
248
|
+
const backups = await this.listBackups();
|
|
249
|
+
|
|
250
|
+
if (backups.length > MAX_BACKUPS) {
|
|
251
|
+
const toDelete = backups.slice(MAX_BACKUPS);
|
|
252
|
+
|
|
253
|
+
for (const backup of toDelete) {
|
|
254
|
+
const backupDir = path.join(this.backupPath, backup.id);
|
|
255
|
+
await fs.remove(backupDir);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Delete specific backup
|
|
262
|
+
* @param {string} backupId - Backup ID to delete
|
|
263
|
+
*/
|
|
264
|
+
async deleteBackup(backupId) {
|
|
265
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
266
|
+
await fs.remove(backupDir);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all backups
|
|
271
|
+
*/
|
|
272
|
+
async clearAllBackups() {
|
|
273
|
+
await fs.remove(this.backupPath);
|
|
274
|
+
await this.init();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get backup size in bytes
|
|
279
|
+
* @param {string} backupId - Backup ID
|
|
280
|
+
* @returns {number} Size in bytes
|
|
281
|
+
*/
|
|
282
|
+
async getBackupSize(backupId) {
|
|
283
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
284
|
+
let totalSize = 0;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const files = await fs.readdir(backupDir);
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
const stats = await fs.stat(path.join(backupDir, file));
|
|
290
|
+
totalSize += stats.size;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// Ignore errors
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return totalSize;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Helper to wrap action with auto-backup
|
|
302
|
+
* @param {string} actionName - Name of action
|
|
303
|
+
* @param {Function} fn - Async function to execute
|
|
304
|
+
* @returns {*} Result of fn()
|
|
305
|
+
*/
|
|
306
|
+
export async function withBackup(actionName, fn) {
|
|
307
|
+
const backup = new BackupManager();
|
|
308
|
+
const backupId = await backup.createBackup(actionName);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await fn();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// Error occurred - backup is available for restore
|
|
314
|
+
console.log(`\n💾 Backup created: ${backupId}`);
|
|
315
|
+
console.log(` Run 'vibecode undo' to restore previous state.\n`);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create backup manager instance
|
|
322
|
+
*/
|
|
323
|
+
export function createBackupManager(projectPath) {
|
|
324
|
+
return new BackupManager(projectPath);
|
|
325
|
+
}
|
|
@@ -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
|
+
}
|
package/src/debug/index.js
CHANGED
|
@@ -27,6 +27,9 @@ import { RootCauseAnalyzer } from './analyzer.js';
|
|
|
27
27
|
import { FixGenerator } from './fixer.js';
|
|
28
28
|
import { FixVerifier } from './verifier.js';
|
|
29
29
|
import { spawnClaudeCode, isClaudeCodeAvailable } from '../providers/index.js';
|
|
30
|
+
import { translateError, formatTranslatedError } from '../ui/error-translator.js';
|
|
31
|
+
import { getLearningEngine } from '../core/learning.js';
|
|
32
|
+
import { askFeedback, showLearningSuggestion } from '../commands/learn.js';
|
|
30
33
|
|
|
31
34
|
const execAsync = promisify(exec);
|
|
32
35
|
|
|
@@ -103,6 +106,9 @@ export class DebugEngine {
|
|
|
103
106
|
return this.createResult('no_hypothesis', 'Could not generate fix hypotheses');
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
// Check for learning-based suggestions
|
|
110
|
+
await showLearningSuggestion(evidence.type, evidence.category);
|
|
111
|
+
|
|
106
112
|
// Step 5-7: TEST, FIX, VERIFY - Attempt fixes
|
|
107
113
|
let fixResult = null;
|
|
108
114
|
for (let attempt = 0; attempt < this.options.maxAttempts && !this.session.resolved; attempt++) {
|
|
@@ -145,6 +151,16 @@ export class DebugEngine {
|
|
|
145
151
|
await this.fixer.updateClaudeMd(fixAttempt);
|
|
146
152
|
|
|
147
153
|
console.log(chalk.green.bold('\n✅ Bug fixed and documented!\n'));
|
|
154
|
+
|
|
155
|
+
// Ask for feedback to improve future suggestions
|
|
156
|
+
if (this.options.interactive) {
|
|
157
|
+
await askFeedback({
|
|
158
|
+
errorType: evidence.type,
|
|
159
|
+
errorMessage: evidence.message,
|
|
160
|
+
errorCategory: evidence.category,
|
|
161
|
+
fixApplied: fixAttempt.description || hypothesis.description
|
|
162
|
+
});
|
|
163
|
+
}
|
|
148
164
|
} else {
|
|
149
165
|
console.log(chalk.yellow(' ⚠ Verification failed, trying next approach...'));
|
|
150
166
|
}
|
|
@@ -275,9 +291,22 @@ export class DebugEngine {
|
|
|
275
291
|
logEvidence(evidence) {
|
|
276
292
|
console.log(chalk.gray(` Type: ${evidence.type || 'Unknown'}`));
|
|
277
293
|
console.log(chalk.gray(` Category: ${evidence.category}`));
|
|
294
|
+
|
|
278
295
|
if (evidence.message) {
|
|
279
|
-
|
|
296
|
+
// Translate error for human-friendly display
|
|
297
|
+
const translated = translateError(evidence.message);
|
|
298
|
+
console.log(chalk.yellow(` Error: ${translated.title}`));
|
|
299
|
+
console.log(chalk.gray(` → ${translated.description.substring(0, 80)}${translated.description.length > 80 ? '...' : ''}`));
|
|
300
|
+
|
|
301
|
+
// Show suggestions
|
|
302
|
+
if (translated.suggestions && translated.suggestions.length > 0) {
|
|
303
|
+
console.log(chalk.gray(` Suggestions:`));
|
|
304
|
+
for (const s of translated.suggestions.slice(0, 2)) {
|
|
305
|
+
console.log(chalk.gray(` • ${s}`));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
280
308
|
}
|
|
309
|
+
|
|
281
310
|
if (evidence.files.length > 0) {
|
|
282
311
|
console.log(chalk.gray(` Files: ${evidence.files.slice(0, 3).join(', ')}${evidence.files.length > 3 ? '...' : ''}`));
|
|
283
312
|
}
|