@itz4blitz/agentful 0.1.0 → 0.1.4

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.
@@ -0,0 +1,685 @@
1
+ /**
2
+ * Smart Agent Generation System
3
+ *
4
+ * Analyzes codebase and generates contextually-aware agents
5
+ * that understand the project's tech stack, patterns, and conventions.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import TemplateEngine from './template-engine.js';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ class AgentGenerator {
17
+ constructor(projectPath, analysis) {
18
+ this.projectPath = projectPath;
19
+ this.analysis = analysis;
20
+ this.templatesDir = path.join(__dirname, '../templates/agents');
21
+ this.agentsDir = path.join(projectPath, '.claude/agents/auto-generated');
22
+ }
23
+
24
+ /**
25
+ * Main entry point - generates all agents based on analysis
26
+ */
27
+ async generateAgents() {
28
+ console.log('🤖 Generating agents...');
29
+
30
+ // Ensure agents directory exists
31
+ await fs.mkdir(this.agentsDir, { recursive: true });
32
+
33
+ // Generate core agents (always)
34
+ const coreAgents = await this.generateCoreAgents();
35
+
36
+ // Generate domain agents (conditional based on detected domains)
37
+ const domainAgents = await this.generateDomainAgents();
38
+
39
+ // Generate tech-specific agents (conditional based on tech stack)
40
+ const techAgents = await this.generateTechAgents();
41
+
42
+ // Update architecture.json with generated agent info
43
+ await this.updateArchitectureConfig({
44
+ core: coreAgents,
45
+ domains: domainAgents,
46
+ tech: techAgents,
47
+ });
48
+
49
+ console.log(`✅ Generated ${coreAgents.length + domainAgents.length + techAgents.length} agents`);
50
+
51
+ return {
52
+ core: coreAgents,
53
+ domains: domainAgents,
54
+ tech: techAgents,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Generate core agents (always needed)
60
+ */
61
+ async generateCoreAgents() {
62
+ const coreAgentTypes = ['backend', 'frontend', 'tester', 'reviewer', 'fixer'];
63
+
64
+ const agents = [];
65
+
66
+ for (const type of coreAgentTypes) {
67
+ const agentPath = path.join(this.agentsDir, `${type}.md`);
68
+ const template = await this.loadTemplate(`${type}-agent.template.md`);
69
+
70
+ if (!template) {
71
+ console.warn(`⚠️ No template found for ${type}, skipping`);
72
+ continue;
73
+ }
74
+
75
+ // Extract patterns from actual code
76
+ const patterns = await this.extractPatterns(type);
77
+
78
+ // Interpolate template with project-specific data
79
+ const content = TemplateEngine.render(template, {
80
+ language: this.analysis.primaryLanguage || 'javascript',
81
+ framework: this.analysis.primaryFramework || 'custom',
82
+ patterns: patterns.code,
83
+ conventions: patterns.conventions,
84
+ samples: patterns.samples,
85
+ generated_at: new Date().toISOString(),
86
+ });
87
+
88
+ await fs.writeFile(agentPath, content);
89
+ agents.push({ type, path: agentPath });
90
+ }
91
+
92
+ return agents;
93
+ }
94
+
95
+ /**
96
+ * Generate domain-specific agents (auth, billing, etc.)
97
+ */
98
+ async generateDomainAgents() {
99
+ const domains = this.analysis.domains || [];
100
+ const agents = [];
101
+
102
+ for (const domain of domains) {
103
+ const agentPath = path.join(this.agentsDir, `${domain.name}-agent.md`);
104
+
105
+ // Extract domain-specific code samples
106
+ const samples = await this.extractDomainSamples(domain);
107
+
108
+ // Generate domain context
109
+ const domainContext = {
110
+ domain: domain.name,
111
+ features: domain.features || [],
112
+ language: this.analysis.primaryLanguage || 'javascript',
113
+ framework: this.analysis.primaryFramework || 'custom',
114
+ confidence: domain.confidence || 0.5,
115
+ codeSamples: samples.code,
116
+ patterns: samples.patterns,
117
+ endpoints: samples.endpoints,
118
+ models: samples.models,
119
+ generated_at: new Date().toISOString(),
120
+ };
121
+
122
+ const template = await this.loadTemplate('domain-agent.template.md');
123
+ const content = TemplateEngine.render(template, domainContext);
124
+
125
+ await fs.writeFile(agentPath, content);
126
+ agents.push({ type: domain.name, path: agentPath });
127
+ }
128
+
129
+ return agents;
130
+ }
131
+
132
+ /**
133
+ * Generate tech-specific agents (Next.js, Prisma, etc.)
134
+ */
135
+ async generateTechAgents() {
136
+ const techStack = this.analysis.techStack || {};
137
+ const agents = [];
138
+
139
+ // Framework-specific agents
140
+ if (techStack.framework) {
141
+ const framework = techStack.framework.toLowerCase();
142
+ if (['nextjs', 'nuxt', 'remix'].includes(framework)) {
143
+ const agent = await this.generateFrameworkAgent(framework);
144
+ if (agent) agents.push(agent);
145
+ }
146
+ }
147
+
148
+ // ORM-specific agents
149
+ if (techStack.orm) {
150
+ const orm = techStack.orm.toLowerCase();
151
+ if (['prisma', 'drizzle', 'typeorm', 'mongoose'].includes(orm)) {
152
+ const agent = await this.generateORMAgent(orm);
153
+ if (agent) agents.push(agent);
154
+ }
155
+ }
156
+
157
+ // Database-specific agents
158
+ if (techStack.database) {
159
+ const db = techStack.database.toLowerCase();
160
+ if (['postgresql', 'mongodb', 'mysql', 'sqlite'].includes(db)) {
161
+ const agent = await this.generateDatabaseAgent(db);
162
+ if (agent) agents.push(agent);
163
+ }
164
+ }
165
+
166
+ return agents;
167
+ }
168
+
169
+ /**
170
+ * Generate framework-specific agent
171
+ */
172
+ async generateFrameworkAgent(framework) {
173
+ const agentPath = path.join(this.agentsDir, `${framework}-agent.md`);
174
+ const template = await this.loadTemplate('tech-agent.template.md');
175
+
176
+ if (!template) return null;
177
+
178
+ const samples = await this.extractFrameworkSamples(framework);
179
+
180
+ const content = TemplateEngine.render(template, {
181
+ tech: framework,
182
+ techType: 'framework',
183
+ language: this.analysis.primaryLanguage || 'javascript',
184
+ framework: framework,
185
+ patterns: samples.patterns,
186
+ conventions: samples.conventions,
187
+ samples: samples.code,
188
+ generated_at: new Date().toISOString(),
189
+ });
190
+
191
+ await fs.writeFile(agentPath, content);
192
+ return { type: framework, path: agentPath };
193
+ }
194
+
195
+ /**
196
+ * Generate ORM-specific agent
197
+ */
198
+ async generateORMAgent(orm) {
199
+ const agentPath = path.join(this.agentsDir, `${orm}-agent.md`);
200
+ const template = await this.loadTemplate('tech-agent.template.md');
201
+
202
+ if (!template) return null;
203
+
204
+ const samples = await this.extractORMSamples(orm);
205
+
206
+ const content = TemplateEngine.render(template, {
207
+ tech: orm,
208
+ techType: 'orm',
209
+ language: this.analysis.primaryLanguage || 'javascript',
210
+ framework: this.analysis.primaryFramework || 'custom',
211
+ patterns: samples.patterns,
212
+ conventions: samples.conventions,
213
+ samples: samples.code,
214
+ generated_at: new Date().toISOString(),
215
+ });
216
+
217
+ await fs.writeFile(agentPath, content);
218
+ return { type: orm, path: agentPath };
219
+ }
220
+
221
+ /**
222
+ * Generate database-specific agent
223
+ */
224
+ async generateDatabaseAgent(database) {
225
+ const agentPath = path.join(this.agentsDir, `${database}-agent.md`);
226
+ const template = await this.loadTemplate('tech-agent.template.md');
227
+
228
+ if (!template) return null;
229
+
230
+ const samples = await this.extractDatabaseSamples(database);
231
+
232
+ const content = TemplateEngine.render(template, {
233
+ tech: database,
234
+ techType: 'database',
235
+ language: this.analysis.primaryLanguage || 'javascript',
236
+ framework: this.analysis.primaryFramework || 'custom',
237
+ patterns: samples.patterns,
238
+ conventions: samples.conventions,
239
+ samples: samples.code,
240
+ generated_at: new Date().toISOString(),
241
+ });
242
+
243
+ await fs.writeFile(agentPath, content);
244
+ return { type: database, path: agentPath };
245
+ }
246
+
247
+ /**
248
+ * Extract code patterns for a specific agent type
249
+ */
250
+ async extractPatterns(agentType) {
251
+ const patterns = {
252
+ code: [],
253
+ conventions: [],
254
+ samples: [],
255
+ };
256
+
257
+ // Define patterns for each agent type
258
+ const agentPatterns = {
259
+ backend: {
260
+ directories: ['src/repositories', 'src/services', 'src/controllers', 'src/routes', 'api'],
261
+ keywords: ['repository', 'service', 'controller', 'route', 'handler'],
262
+ },
263
+ frontend: {
264
+ directories: ['src/components', 'src/pages', 'src/app', 'components', 'pages'],
265
+ keywords: ['component', 'hook', 'page', 'view'],
266
+ },
267
+ tester: {
268
+ directories: ['tests', 'test', '__tests__', '__tests__', 'spec'],
269
+ keywords: ['describe', 'test', 'it', 'expect', 'mock'],
270
+ },
271
+ reviewer: {
272
+ directories: ['src'],
273
+ keywords: ['export', 'function', 'class', 'interface'],
274
+ },
275
+ fixer: {
276
+ directories: ['src'],
277
+ keywords: ['error', 'bug', 'fix', 'throw'],
278
+ },
279
+ };
280
+
281
+ const config = agentPatterns[agentType];
282
+ if (!config) return patterns;
283
+
284
+ // Scan directories for patterns
285
+ for (const dir of config.directories) {
286
+ const dirPath = path.join(this.projectPath, dir);
287
+ try {
288
+ const files = await this.scanDirectory(dirPath, 10); // Sample up to 10 files
289
+
290
+ for (const file of files) {
291
+ const content = await fs.readFile(file, 'utf-8');
292
+ const relativePath = path.relative(this.projectPath, file);
293
+
294
+ // Extract code samples
295
+ if (content.length > 0 && content.length < 2000) {
296
+ patterns.samples.push({
297
+ path: relativePath,
298
+ content: content,
299
+ });
300
+ }
301
+
302
+ // Identify patterns
303
+ for (const keyword of config.keywords) {
304
+ if (content.toLowerCase().includes(keyword)) {
305
+ patterns.code.push({
306
+ keyword,
307
+ context: this.extractContext(content, keyword),
308
+ });
309
+ }
310
+ }
311
+ }
312
+ } catch (error) {
313
+ // Directory doesn't exist, skip
314
+ }
315
+ }
316
+
317
+ // Detect naming conventions
318
+ patterns.conventions = await this.detectConventions(agentType);
319
+
320
+ return patterns;
321
+ }
322
+
323
+ /**
324
+ * Extract domain-specific code samples
325
+ */
326
+ async extractDomainSamples(domain) {
327
+ const samples = {
328
+ code: [],
329
+ patterns: [],
330
+ endpoints: [],
331
+ models: [],
332
+ };
333
+
334
+ // Find domain-specific files
335
+ const domainPatterns = {
336
+ 'auth-agent': ['auth', 'user', 'login', 'register', 'session', 'token'],
337
+ 'billing-agent': ['billing', 'payment', 'subscription', 'invoice', 'stripe'],
338
+ 'content-agent': ['content', 'post', 'article', 'blog', 'page'],
339
+ 'notification-agent': ['notification', 'email', 'sms', 'push'],
340
+ };
341
+
342
+ const keywords = domainPatterns[domain.name] || [domain.name];
343
+
344
+ // Scan for domain files
345
+ const files = await this.findFilesByKeywords(keywords, 5);
346
+
347
+ for (const file of files) {
348
+ const content = await fs.readFile(file, 'utf-8');
349
+ const relativePath = path.relative(this.projectPath, file);
350
+
351
+ samples.code.push({
352
+ path: relativePath,
353
+ content: content.substring(0, 1500), // Limit size
354
+ });
355
+
356
+ // Extract API endpoints
357
+ if (content.includes('router.') || content.includes('app.') || content.includes('@Get')) {
358
+ samples.endpoints.push(...this.extractEndpoints(content, relativePath));
359
+ }
360
+
361
+ // Extract data models
362
+ if (content.includes('model') || content.includes('schema') || content.includes('interface')) {
363
+ samples.models.push(...this.extractModels(content, relativePath));
364
+ }
365
+ }
366
+
367
+ return samples;
368
+ }
369
+
370
+ /**
371
+ * Extract framework-specific samples
372
+ */
373
+ async extractFrameworkSamples(framework) {
374
+ const samples = {
375
+ code: [],
376
+ patterns: [],
377
+ conventions: [],
378
+ };
379
+
380
+ const frameworkPatterns = {
381
+ nextjs: ['app/', 'pages/', 'middleware.ts', 'next.config'],
382
+ nuxt: ['pages/', 'components/', 'nuxt.config'],
383
+ remix: ['routes/', 'app/routes/', 'loader', 'action'],
384
+ };
385
+
386
+ const patterns = frameworkPatterns[framework] || [];
387
+
388
+ for (const pattern of patterns) {
389
+ const files = await this.findFilesByPattern(pattern, 3);
390
+
391
+ for (const file of files) {
392
+ const content = await fs.readFile(file, 'utf-8');
393
+ const relativePath = path.relative(this.projectPath, file);
394
+
395
+ samples.code.push({
396
+ path: relativePath,
397
+ content: content.substring(0, 1000),
398
+ });
399
+ }
400
+ }
401
+
402
+ return samples;
403
+ }
404
+
405
+ /**
406
+ * Extract ORM-specific samples
407
+ */
408
+ async extractORMSamples(orm) {
409
+ const samples = {
410
+ code: [],
411
+ patterns: [],
412
+ conventions: [],
413
+ };
414
+
415
+ const ormFiles = {
416
+ prisma: ['schema.prisma', 'client.ts'],
417
+ drizzle: ['schema.ts', 'db.ts'],
418
+ typeorm: ['entity.ts', 'repository.ts'],
419
+ mongoose: ['model.ts', 'schema.ts'],
420
+ };
421
+
422
+ const files = ormFiles[orm] || [];
423
+
424
+ for (const file of files) {
425
+ const foundFiles = await this.findFilesByPattern(file, 2);
426
+
427
+ for (const foundFile of foundFiles) {
428
+ const content = await fs.readFile(foundFile, 'utf-8');
429
+ const relativePath = path.relative(this.projectPath, foundFile);
430
+
431
+ samples.code.push({
432
+ path: relativePath,
433
+ content: content.substring(0, 1000),
434
+ });
435
+ }
436
+ }
437
+
438
+ return samples;
439
+ }
440
+
441
+ /**
442
+ * Extract database-specific samples
443
+ */
444
+ async extractDatabaseSamples(database) {
445
+ const samples = {
446
+ code: [],
447
+ patterns: [],
448
+ conventions: [],
449
+ };
450
+
451
+ // Look for migration files, SQL files, etc.
452
+ const patterns = ['migrations/', '*.sql', 'schema.sql', 'seeds/'];
453
+
454
+ for (const pattern of patterns) {
455
+ const files = await this.findFilesByPattern(pattern, 3);
456
+
457
+ for (const file of files) {
458
+ const content = await fs.readFile(file, 'utf-8');
459
+ const relativePath = path.relative(this.projectPath, file);
460
+
461
+ samples.code.push({
462
+ path: relativePath,
463
+ content: content.substring(0, 1000),
464
+ });
465
+ }
466
+ }
467
+
468
+ return samples;
469
+ }
470
+
471
+ /**
472
+ * Scan directory for files
473
+ */
474
+ async scanDirectory(dirPath, maxFiles = 10) {
475
+ const files = [];
476
+
477
+ try {
478
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
479
+
480
+ for (const entry of entries) {
481
+ if (files.length >= maxFiles) break;
482
+
483
+ const fullPath = path.join(dirPath, entry.name);
484
+
485
+ if (entry.isDirectory()) {
486
+ const subFiles = await this.scanDirectory(fullPath, maxFiles - files.length);
487
+ files.push(...subFiles);
488
+ } else if (entry.isFile() && this.isSourceFile(entry.name)) {
489
+ files.push(fullPath);
490
+ }
491
+ }
492
+ } catch (error) {
493
+ // Directory doesn't exist or can't be read
494
+ }
495
+
496
+ return files;
497
+ }
498
+
499
+ /**
500
+ * Find files by keywords in name
501
+ */
502
+ async findFilesByKeywords(keywords, maxFiles = 5) {
503
+ const allFiles = [];
504
+
505
+ for (const keyword of keywords) {
506
+ const files = await this.findFilesByPattern(keyword, maxFiles);
507
+ allFiles.push(...files);
508
+ }
509
+
510
+ return allFiles.slice(0, maxFiles);
511
+ }
512
+
513
+ /**
514
+ * Find files by pattern
515
+ */
516
+ async findFilesByPattern(pattern, maxFiles = 5) {
517
+ const files = [];
518
+
519
+ const scanDir = async (dirPath) => {
520
+ try {
521
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
522
+
523
+ for (const entry of entries) {
524
+ if (files.length >= maxFiles) return;
525
+
526
+ const fullPath = path.join(dirPath, entry.name);
527
+
528
+ if (entry.isDirectory()) {
529
+ // Skip node_modules and similar
530
+ if (!['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
531
+ await scanDir(fullPath);
532
+ }
533
+ } else if (entry.isFile()) {
534
+ if (entry.name.toLowerCase().includes(pattern.toLowerCase()) ||
535
+ fullPath.toLowerCase().includes(pattern.toLowerCase())) {
536
+ files.push(fullPath);
537
+ }
538
+ }
539
+ }
540
+ } catch (error) {
541
+ // Can't read directory
542
+ }
543
+ };
544
+
545
+ await scanDir(this.projectPath);
546
+ return files;
547
+ }
548
+
549
+ /**
550
+ * Check if file is a source file
551
+ */
552
+ isSourceFile(filename) {
553
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs'];
554
+ return extensions.some(ext => filename.endsWith(ext));
555
+ }
556
+
557
+ /**
558
+ * Extract context around a keyword
559
+ */
560
+ extractContext(content, keyword) {
561
+ const lines = content.split('\n');
562
+ const context = [];
563
+
564
+ for (let i = 0; i < lines.length; i++) {
565
+ if (lines[i].toLowerCase().includes(keyword.toLowerCase())) {
566
+ const start = Math.max(0, i - 2);
567
+ const end = Math.min(lines.length, i + 3);
568
+ context.push(lines.slice(start, end).join('\n'));
569
+ }
570
+ }
571
+
572
+ return context.slice(0, 3); // Max 3 contexts
573
+ }
574
+
575
+ /**
576
+ * Detect naming conventions
577
+ */
578
+ async detectConventions(agentType) {
579
+ const conventions = [];
580
+
581
+ // Sample some files to detect conventions
582
+ const files = await this.findFilesByPattern('.ts', 5);
583
+
584
+ for (const file of files) {
585
+ const content = await fs.readFile(file, 'utf-8');
586
+
587
+ // Detect import style
588
+ if (content.includes('@/')) {
589
+ conventions.push('Uses @ alias for imports');
590
+ }
591
+
592
+ // Detect naming patterns
593
+ if (content.match(/class \w+/)) {
594
+ conventions.push('Uses class-based components');
595
+ }
596
+
597
+ if (content.match(/export (const|function) \w+/)) {
598
+ conventions.push('Uses functional exports');
599
+ }
600
+ }
601
+
602
+ return [...new Set(conventions)]; // Deduplicate
603
+ }
604
+
605
+ /**
606
+ * Extract API endpoints from code
607
+ */
608
+ extractEndpoints(content, filePath) {
609
+ const endpoints = [];
610
+ const lines = content.split('\n');
611
+
612
+ for (const line of lines) {
613
+ if (line.includes('router.') || line.includes('app.')) {
614
+ endpoints.push({
615
+ file: filePath,
616
+ code: line.trim(),
617
+ });
618
+ }
619
+ }
620
+
621
+ return endpoints.slice(0, 5);
622
+ }
623
+
624
+ /**
625
+ * Extract data models from code
626
+ */
627
+ extractModels(content, filePath) {
628
+ const models = [];
629
+ const lines = content.split('\n');
630
+
631
+ for (const line of lines) {
632
+ if (line.includes('model ') || line.includes('schema ') || line.includes('interface ')) {
633
+ models.push({
634
+ file: filePath,
635
+ code: line.trim(),
636
+ });
637
+ }
638
+ }
639
+
640
+ return models.slice(0, 5);
641
+ }
642
+
643
+ /**
644
+ * Load template file
645
+ */
646
+ async loadTemplate(templateName) {
647
+ const templatePath = path.join(this.templatesDir, templateName);
648
+
649
+ try {
650
+ return await fs.readFile(templatePath, 'utf-8');
651
+ } catch (error) {
652
+ console.warn(`Template not found: ${templateName}`);
653
+ return null;
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Update architecture.json with agent info
659
+ */
660
+ async updateArchitectureConfig(agents) {
661
+ const configPath = path.join(this.projectPath, '.agentful/architecture.json');
662
+
663
+ let config = {};
664
+
665
+ try {
666
+ const content = await fs.readFile(configPath, 'utf-8');
667
+ config = JSON.parse(content);
668
+ } catch (error) {
669
+ // Config doesn't exist yet
670
+ }
671
+
672
+ config.agents = {
673
+ generated: {
674
+ core: agents.core.map(a => a.type),
675
+ domains: agents.domains.map(a => a.type),
676
+ tech: agents.tech.map(a => a.type),
677
+ },
678
+ generatedAt: new Date().toISOString(),
679
+ };
680
+
681
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
682
+ }
683
+ }
684
+
685
+ export default AgentGenerator;