@itz4blitz/agentful 0.1.0 → 0.1.5
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/.claude/agents/architect.md +283 -11
- package/.claude/agents/backend.md +282 -218
- package/.claude/agents/frontend.md +242 -319
- package/.claude/agents/orchestrator.md +27 -27
- package/.claude/agents/reviewer.md +1 -1
- package/.claude/agents/tester.md +375 -284
- package/.claude/commands/agentful-decide.md +104 -29
- package/.claude/commands/agentful-start.md +18 -16
- package/.claude/commands/agentful-status.md +28 -22
- package/.claude/commands/agentful-validate.md +42 -20
- package/.claude/commands/agentful.md +329 -0
- package/.claude/product/README.md +1 -1
- package/.claude/product/index.md +1 -1
- package/.claude/settings.json +4 -3
- package/.claude/skills/conversation/SKILL.md +1130 -0
- package/LICENSE +1 -1
- package/README.md +557 -222
- package/bin/cli.js +319 -36
- package/lib/agent-generator.js +685 -0
- package/lib/domain-detector.js +468 -0
- package/lib/domain-structure-generator.js +770 -0
- package/lib/index.js +40 -0
- package/lib/project-analyzer.js +701 -0
- package/lib/tech-stack-detector.js +1091 -0
- package/lib/template-engine.js +153 -0
- package/package.json +14 -5
- package/template/CLAUDE.md +62 -21
- package/template/PRODUCT.md +89 -1
|
@@ -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;
|