codebakers 1.0.45

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/.vscodeignore +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -0
  4. package/codebakers-1.0.0.vsix +0 -0
  5. package/codebakers-1.0.10.vsix +0 -0
  6. package/codebakers-1.0.11.vsix +0 -0
  7. package/codebakers-1.0.12.vsix +0 -0
  8. package/codebakers-1.0.13.vsix +0 -0
  9. package/codebakers-1.0.14.vsix +0 -0
  10. package/codebakers-1.0.15.vsix +0 -0
  11. package/codebakers-1.0.16.vsix +0 -0
  12. package/codebakers-1.0.17.vsix +0 -0
  13. package/codebakers-1.0.18.vsix +0 -0
  14. package/codebakers-1.0.19.vsix +0 -0
  15. package/codebakers-1.0.20.vsix +0 -0
  16. package/codebakers-1.0.21.vsix +0 -0
  17. package/codebakers-1.0.22.vsix +0 -0
  18. package/codebakers-1.0.23.vsix +0 -0
  19. package/codebakers-1.0.24.vsix +0 -0
  20. package/codebakers-1.0.25.vsix +0 -0
  21. package/codebakers-1.0.26.vsix +0 -0
  22. package/codebakers-1.0.27.vsix +0 -0
  23. package/codebakers-1.0.28.vsix +0 -0
  24. package/codebakers-1.0.29.vsix +0 -0
  25. package/codebakers-1.0.30.vsix +0 -0
  26. package/codebakers-1.0.31.vsix +0 -0
  27. package/codebakers-1.0.32.vsix +0 -0
  28. package/codebakers-1.0.35.vsix +0 -0
  29. package/codebakers-1.0.36.vsix +0 -0
  30. package/codebakers-1.0.37.vsix +0 -0
  31. package/codebakers-1.0.38.vsix +0 -0
  32. package/codebakers-1.0.39.vsix +0 -0
  33. package/codebakers-1.0.40.vsix +0 -0
  34. package/codebakers-1.0.41.vsix +0 -0
  35. package/codebakers-1.0.42.vsix +0 -0
  36. package/codebakers-1.0.43.vsix +0 -0
  37. package/codebakers-1.0.44.vsix +0 -0
  38. package/codebakers-1.0.45.vsix +0 -0
  39. package/dist/extension.js +1394 -0
  40. package/esbuild.js +63 -0
  41. package/media/icon.png +0 -0
  42. package/media/icon.svg +7 -0
  43. package/nul +1 -0
  44. package/package.json +127 -0
  45. package/preview.html +547 -0
  46. package/src/ChatPanelProvider.ts +1815 -0
  47. package/src/ChatViewProvider.ts +749 -0
  48. package/src/CodeBakersClient.ts +1146 -0
  49. package/src/CodeValidator.ts +645 -0
  50. package/src/FileOperations.ts +410 -0
  51. package/src/ProjectContext.ts +526 -0
  52. package/src/extension.ts +332 -0
  53. package/tsconfig.json +19 -0
@@ -0,0 +1,526 @@
1
+ import * as vscode from 'vscode';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ interface ProjectState {
6
+ // From .codebakers.json
7
+ version?: string;
8
+ projectType?: 'new' | 'existing';
9
+ stack?: {
10
+ framework?: string;
11
+ database?: string;
12
+ auth?: string;
13
+ ui?: string;
14
+ payments?: string[];
15
+ };
16
+ decisions?: Record<string, string>;
17
+ currentWork?: {
18
+ lastUpdated?: string;
19
+ activeFeature?: string;
20
+ status?: string;
21
+ summary?: string;
22
+ pendingTasks?: string[];
23
+ };
24
+
25
+ // Computed context
26
+ recentFiles?: string[];
27
+ packageDeps?: string[];
28
+ hasTests?: boolean;
29
+ openFile?: string;
30
+ selectedText?: string;
31
+ fileTree?: string; // Project directory structure
32
+ existingTypes?: string; // Type inventory for reuse
33
+ installedPackages?: string[]; // List of npm packages
34
+
35
+ // Conversation memory
36
+ keyDecisions?: string[];
37
+ completedTasks?: string[];
38
+ blockers?: string[];
39
+ }
40
+
41
+ interface ProjectUpdate {
42
+ patterns?: string[];
43
+ tasks?: string[];
44
+ decisions?: Record<string, string>;
45
+ }
46
+
47
+ export class ProjectContext {
48
+ private _cache: ProjectState | null = null;
49
+ private _cacheTime: number = 0;
50
+ private readonly CACHE_TTL = 30000; // 30 seconds
51
+
52
+ constructor() {}
53
+
54
+ /**
55
+ * Get current project state for context injection
56
+ * This is the "perfect recall" - we maintain state outside the conversation
57
+ */
58
+ async getProjectState(): Promise<ProjectState | null> {
59
+ // Check cache
60
+ if (this._cache && Date.now() - this._cacheTime < this.CACHE_TTL) {
61
+ // Still add dynamic context (selected text, open file)
62
+ return {
63
+ ...this._cache,
64
+ ...this._getDynamicContext()
65
+ };
66
+ }
67
+
68
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
69
+ if (!workspaceFolder) return null;
70
+
71
+ const rootPath = workspaceFolder.uri.fsPath;
72
+ const state: ProjectState = {};
73
+
74
+ // Read .codebakers.json if exists
75
+ const codebakersPath = path.join(rootPath, '.codebakers.json');
76
+ if (fs.existsSync(codebakersPath)) {
77
+ try {
78
+ const content = fs.readFileSync(codebakersPath, 'utf-8');
79
+ const codebakersJson = JSON.parse(content);
80
+ Object.assign(state, codebakersJson);
81
+ } catch (error) {
82
+ console.error('Failed to read .codebakers.json:', error);
83
+ }
84
+ }
85
+
86
+ // Read package.json for dependencies
87
+ const packagePath = path.join(rootPath, 'package.json');
88
+ if (fs.existsSync(packagePath)) {
89
+ try {
90
+ const content = fs.readFileSync(packagePath, 'utf-8');
91
+ const pkg = JSON.parse(content);
92
+ state.packageDeps = [
93
+ ...Object.keys(pkg.dependencies || {}),
94
+ ...Object.keys(pkg.devDependencies || {})
95
+ ];
96
+
97
+ // Detect stack from dependencies
98
+ if (!state.stack) {
99
+ state.stack = this._detectStack(state.packageDeps);
100
+ }
101
+ } catch (error) {
102
+ console.error('Failed to read package.json:', error);
103
+ }
104
+ }
105
+
106
+ // Check for test files
107
+ state.hasTests = await this._hasTestFiles(rootPath);
108
+
109
+ // Get recently modified files
110
+ state.recentFiles = await this._getRecentFiles(rootPath);
111
+
112
+ // Get file tree structure (for knowing where to create files)
113
+ state.fileTree = await this._getFileTree(rootPath);
114
+
115
+ // Get existing types for AI to reuse
116
+ state.existingTypes = await this._scanExistingTypes(rootPath);
117
+
118
+ // Store installed packages list
119
+ state.installedPackages = state.packageDeps?.slice(0, 50);
120
+
121
+ // Read devlog for recent context
122
+ const devlogPath = path.join(rootPath, '.codebakers', 'DEVLOG.md');
123
+ if (fs.existsSync(devlogPath)) {
124
+ try {
125
+ const content = fs.readFileSync(devlogPath, 'utf-8');
126
+ // Extract recent decisions and tasks from devlog
127
+ state.keyDecisions = this._extractFromDevlog(content, 'decisions');
128
+ state.completedTasks = this._extractFromDevlog(content, 'tasks');
129
+ } catch (error) {
130
+ console.error('Failed to read devlog:', error);
131
+ }
132
+ }
133
+
134
+ // Read blockers file
135
+ const blockedPath = path.join(rootPath, '.codebakers', 'BLOCKED.md');
136
+ if (fs.existsSync(blockedPath)) {
137
+ try {
138
+ const content = fs.readFileSync(blockedPath, 'utf-8');
139
+ state.blockers = this._extractBlockers(content);
140
+ } catch (error) {
141
+ console.error('Failed to read blockers:', error);
142
+ }
143
+ }
144
+
145
+ // Cache the result
146
+ this._cache = state;
147
+ this._cacheTime = Date.now();
148
+
149
+ // Add dynamic context
150
+ return {
151
+ ...state,
152
+ ...this._getDynamicContext()
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Apply updates to project state (called after Claude responses)
158
+ */
159
+ async applyUpdates(updates: ProjectUpdate): Promise<void> {
160
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
161
+ if (!workspaceFolder) return;
162
+
163
+ const rootPath = workspaceFolder.uri.fsPath;
164
+ const codebakersPath = path.join(rootPath, '.codebakers.json');
165
+
166
+ // Read existing state
167
+ let existing: any = {};
168
+ if (fs.existsSync(codebakersPath)) {
169
+ try {
170
+ existing = JSON.parse(fs.readFileSync(codebakersPath, 'utf-8'));
171
+ } catch {
172
+ existing = {};
173
+ }
174
+ }
175
+
176
+ // Apply updates
177
+ if (updates.decisions) {
178
+ existing.decisions = {
179
+ ...existing.decisions,
180
+ ...updates.decisions
181
+ };
182
+ }
183
+
184
+ if (updates.tasks) {
185
+ existing.currentWork = existing.currentWork || {};
186
+ existing.currentWork.pendingTasks = updates.tasks;
187
+ existing.currentWork.lastUpdated = new Date().toISOString();
188
+ }
189
+
190
+ if (updates.patterns) {
191
+ existing.analytics = existing.analytics || {};
192
+ existing.analytics.modulesUsed = existing.analytics.modulesUsed || {};
193
+ updates.patterns.forEach(p => {
194
+ existing.analytics.modulesUsed[p] = (existing.analytics.modulesUsed[p] || 0) + 1;
195
+ });
196
+ }
197
+
198
+ // Write back
199
+ fs.writeFileSync(codebakersPath, JSON.stringify(existing, null, 2));
200
+
201
+ // Invalidate cache
202
+ this._cache = null;
203
+ }
204
+
205
+ /**
206
+ * Invalidate cache when files change
207
+ */
208
+ invalidateCache(): void {
209
+ this._cache = null;
210
+ }
211
+
212
+ /**
213
+ * Get dynamic context (current editor state)
214
+ */
215
+ private _getDynamicContext(): Partial<ProjectState> {
216
+ const editor = vscode.window.activeTextEditor;
217
+ if (!editor) return {};
218
+
219
+ const context: Partial<ProjectState> = {
220
+ openFile: vscode.workspace.asRelativePath(editor.document.uri)
221
+ };
222
+
223
+ const selection = editor.selection;
224
+ if (!selection.isEmpty) {
225
+ context.selectedText = editor.document.getText(selection);
226
+ }
227
+
228
+ return context;
229
+ }
230
+
231
+ /**
232
+ * Detect tech stack from dependencies
233
+ */
234
+ private _detectStack(deps: string[]): ProjectState['stack'] {
235
+ const stack: ProjectState['stack'] = {};
236
+
237
+ // Framework
238
+ if (deps.includes('next')) stack.framework = 'nextjs';
239
+ else if (deps.includes('react')) stack.framework = 'react';
240
+ else if (deps.includes('vue')) stack.framework = 'vue';
241
+ else if (deps.includes('svelte')) stack.framework = 'svelte';
242
+
243
+ // Database/ORM
244
+ if (deps.includes('drizzle-orm')) stack.database = 'drizzle';
245
+ else if (deps.includes('prisma')) stack.database = 'prisma';
246
+ else if (deps.includes('mongoose')) stack.database = 'mongodb';
247
+
248
+ // Auth
249
+ if (deps.includes('@supabase/supabase-js')) stack.auth = 'supabase';
250
+ else if (deps.includes('next-auth')) stack.auth = 'next-auth';
251
+ else if (deps.includes('@clerk/nextjs')) stack.auth = 'clerk';
252
+
253
+ // UI
254
+ if (deps.some(d => d.includes('@radix-ui'))) stack.ui = 'shadcn';
255
+ else if (deps.includes('@chakra-ui/react')) stack.ui = 'chakra';
256
+ else if (deps.includes('@mui/material')) stack.ui = 'mui';
257
+
258
+ // Payments
259
+ const payments: string[] = [];
260
+ if (deps.includes('stripe')) payments.push('stripe');
261
+ if (deps.includes('@paypal/react-paypal-js')) payments.push('paypal');
262
+ if (payments.length > 0) stack.payments = payments;
263
+
264
+ return stack;
265
+ }
266
+
267
+ /**
268
+ * Check if project has test files
269
+ */
270
+ private async _hasTestFiles(rootPath: string): Promise<boolean> {
271
+ const testPatterns = ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/tests/**'];
272
+
273
+ for (const pattern of testPatterns) {
274
+ const files = await vscode.workspace.findFiles(pattern, '**/node_modules/**', 1);
275
+ if (files.length > 0) return true;
276
+ }
277
+
278
+ return false;
279
+ }
280
+
281
+ /**
282
+ * Get recently modified files
283
+ */
284
+ private async _getRecentFiles(rootPath: string): Promise<string[]> {
285
+ const files = await vscode.workspace.findFiles(
286
+ '**/*.{ts,tsx,js,jsx}',
287
+ '**/node_modules/**',
288
+ 50
289
+ );
290
+
291
+ // Sort by modification time and take top 10
292
+ const withStats = await Promise.all(
293
+ files.map(async (f) => {
294
+ try {
295
+ const stat = await vscode.workspace.fs.stat(f);
296
+ return { path: vscode.workspace.asRelativePath(f), mtime: stat.mtime };
297
+ } catch {
298
+ return null;
299
+ }
300
+ })
301
+ );
302
+
303
+ return withStats
304
+ .filter((f): f is { path: string; mtime: number } => f !== null)
305
+ .sort((a, b) => b.mtime - a.mtime)
306
+ .slice(0, 10)
307
+ .map(f => f.path);
308
+ }
309
+
310
+ /**
311
+ * Get file tree structure for AI context
312
+ * This helps the AI understand where files exist and where to create new ones
313
+ */
314
+ private async _getFileTree(rootPath: string, maxDepth: number = 4): Promise<string> {
315
+ const IGNORE_DIRS = new Set([
316
+ 'node_modules', '.git', '.next', 'dist', 'build', '.vercel',
317
+ '.turbo', 'coverage', '.cache', '.nuxt', '.output', '__pycache__'
318
+ ]);
319
+
320
+ const IMPORTANT_DIRS = new Set([
321
+ 'src', 'app', 'pages', 'components', 'lib', 'utils', 'hooks',
322
+ 'api', 'services', 'types', 'styles', 'public', 'tests', '__tests__'
323
+ ]);
324
+
325
+ const lines: string[] = [];
326
+
327
+ const scan = (dir: string, prefix: string, depth: number): void => {
328
+ if (depth > maxDepth) return;
329
+
330
+ try {
331
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
332
+
333
+ // Separate directories and files
334
+ const dirs = entries.filter(e => e.isDirectory() && !IGNORE_DIRS.has(e.name) && !e.name.startsWith('.'));
335
+ const files = entries.filter(e => e.isFile());
336
+
337
+ // Sort: important dirs first, then alphabetically
338
+ dirs.sort((a, b) => {
339
+ const aImportant = IMPORTANT_DIRS.has(a.name);
340
+ const bImportant = IMPORTANT_DIRS.has(b.name);
341
+ if (aImportant && !bImportant) return -1;
342
+ if (!aImportant && bImportant) return 1;
343
+ return a.name.localeCompare(b.name);
344
+ });
345
+
346
+ // Show directories
347
+ for (let i = 0; i < dirs.length; i++) {
348
+ const entry = dirs[i];
349
+ const isLast = i === dirs.length - 1 && files.length === 0;
350
+ const connector = isLast ? '└── ' : '├── ';
351
+ const childPrefix = isLast ? ' ' : '│ ';
352
+
353
+ lines.push(`${prefix}${connector}${entry.name}/`);
354
+ scan(path.join(dir, entry.name), prefix + childPrefix, depth + 1);
355
+ }
356
+
357
+ // Show key files at depth 0-1, or all files at deeper levels (limited)
358
+ const keyFiles = files.filter(f =>
359
+ depth <= 1 ?
360
+ ['package.json', 'tsconfig.json', '.env.example', 'next.config.js', 'next.config.mjs', 'drizzle.config.ts'].includes(f.name) ||
361
+ f.name.endsWith('.ts') || f.name.endsWith('.tsx')
362
+ : true
363
+ );
364
+
365
+ // Limit files shown at each level
366
+ const maxFiles = depth === 0 ? 5 : (depth === 1 ? 10 : 15);
367
+ const filesToShow = keyFiles.slice(0, maxFiles);
368
+ const hiddenCount = keyFiles.length - filesToShow.length;
369
+
370
+ for (let i = 0; i < filesToShow.length; i++) {
371
+ const file = filesToShow[i];
372
+ const isLast = i === filesToShow.length - 1 && hiddenCount === 0;
373
+ const connector = isLast ? '└── ' : '├── ';
374
+ lines.push(`${prefix}${connector}${file.name}`);
375
+ }
376
+
377
+ if (hiddenCount > 0) {
378
+ lines.push(`${prefix}└── ... (${hiddenCount} more files)`);
379
+ }
380
+
381
+ } catch (error) {
382
+ // Ignore permission errors
383
+ }
384
+ };
385
+
386
+ lines.push(path.basename(rootPath) + '/');
387
+ scan(rootPath, '', 0);
388
+
389
+ return lines.join('\n');
390
+ }
391
+
392
+ /**
393
+ * Scan project for existing types that AI can reuse
394
+ * This prevents creating duplicate types
395
+ */
396
+ private async _scanExistingTypes(rootPath: string): Promise<string> {
397
+ const types: { name: string; file: string; kind: string }[] = [];
398
+
399
+ // Find TypeScript files in src/types, src/lib, etc
400
+ const typeDirs = ['src/types', 'src/lib', 'types', 'lib'];
401
+
402
+ for (const dir of typeDirs) {
403
+ const dirPath = path.join(rootPath, dir);
404
+ if (!fs.existsSync(dirPath)) continue;
405
+
406
+ try {
407
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
408
+
409
+ for (const file of files.slice(0, 10)) { // Limit files per dir
410
+ const filePath = path.join(dirPath, file);
411
+ try {
412
+ const content = fs.readFileSync(filePath, 'utf-8');
413
+ const relativePath = path.join(dir, file);
414
+
415
+ // Extract interfaces
416
+ const interfaceRegex = /export\s+interface\s+(\w+)/g;
417
+ let match;
418
+ while ((match = interfaceRegex.exec(content)) !== null) {
419
+ types.push({ name: match[1], file: relativePath, kind: 'interface' });
420
+ }
421
+
422
+ // Extract type aliases
423
+ const typeRegex = /export\s+type\s+(\w+)/g;
424
+ while ((match = typeRegex.exec(content)) !== null) {
425
+ types.push({ name: match[1], file: relativePath, kind: 'type' });
426
+ }
427
+
428
+ // Extract enums
429
+ const enumRegex = /export\s+enum\s+(\w+)/g;
430
+ while ((match = enumRegex.exec(content)) !== null) {
431
+ types.push({ name: match[1], file: relativePath, kind: 'enum' });
432
+ }
433
+ } catch {
434
+ // Skip files we can't read
435
+ }
436
+ }
437
+ } catch {
438
+ // Skip dirs we can't read
439
+ }
440
+ }
441
+
442
+ if (types.length === 0) {
443
+ return '';
444
+ }
445
+
446
+ // Format for AI context
447
+ const lines = ['EXISTING TYPES (import these instead of creating new):'];
448
+ const byFile = new Map<string, typeof types>();
449
+
450
+ for (const t of types) {
451
+ const arr = byFile.get(t.file) || [];
452
+ arr.push(t);
453
+ byFile.set(t.file, arr);
454
+ }
455
+
456
+ for (const [file, fileTypes] of byFile) {
457
+ lines.push(` ${file}:`);
458
+ for (const t of fileTypes.slice(0, 5)) {
459
+ lines.push(` - ${t.kind} ${t.name}`);
460
+ }
461
+ if (fileTypes.length > 5) {
462
+ lines.push(` ... and ${fileTypes.length - 5} more`);
463
+ }
464
+ }
465
+
466
+ return lines.slice(0, 30).join('\n'); // Limit output size
467
+ }
468
+
469
+ /**
470
+ * Extract information from devlog
471
+ */
472
+ private _extractFromDevlog(content: string, type: 'decisions' | 'tasks'): string[] {
473
+ const results: string[] = [];
474
+ const lines = content.split('\n');
475
+
476
+ // Look for the most recent entry's relevant section
477
+ let inRecentEntry = false;
478
+ let inSection = false;
479
+
480
+ for (const line of lines) {
481
+ if (line.startsWith('## ')) {
482
+ inRecentEntry = true;
483
+ } else if (line.startsWith('---')) {
484
+ break; // Only process most recent entry
485
+ }
486
+
487
+ if (inRecentEntry) {
488
+ if (type === 'decisions' && line.toLowerCase().includes('decision')) {
489
+ inSection = true;
490
+ continue;
491
+ }
492
+ if (type === 'tasks' && line.toLowerCase().includes('what was done')) {
493
+ inSection = true;
494
+ continue;
495
+ }
496
+
497
+ if (inSection) {
498
+ if (line.startsWith('###') || line.startsWith('## ')) {
499
+ inSection = false;
500
+ } else if (line.startsWith('- ')) {
501
+ results.push(line.substring(2).trim());
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ return results.slice(0, 5); // Limit to 5 items
508
+ }
509
+
510
+ /**
511
+ * Extract blockers from BLOCKED.md
512
+ */
513
+ private _extractBlockers(content: string): string[] {
514
+ const blockers: string[] = [];
515
+ const lines = content.split('\n');
516
+
517
+ for (let i = 0; i < lines.length; i++) {
518
+ const line = lines[i];
519
+ if (line.startsWith('**Blocking Issue:**')) {
520
+ blockers.push(line.replace('**Blocking Issue:**', '').trim());
521
+ }
522
+ }
523
+
524
+ return blockers;
525
+ }
526
+ }