prjct-cli 0.9.2 → 0.10.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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * AgentValidator - Validates agents before and after generation
3
+ *
4
+ * Ensures agents are useful and not generic
5
+ * Compares with existing agents before generating
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ class AgentValidator {
11
+ /**
12
+ * Validate if agent should be generated
13
+ * @param {string} agentName - Proposed agent name
14
+ * @param {Object} config - Agent configuration
15
+ * @param {Array<Object>} existingAgents - Existing agents
16
+ * @returns {Object} Validation result
17
+ */
18
+ validateBeforeGeneration(agentName, config, existingAgents = []) {
19
+ const issues = []
20
+ const warnings = []
21
+
22
+ // Check if similar agent exists
23
+ const similar = this.findSimilarAgent(agentName, config, existingAgents)
24
+ if (similar) {
25
+ warnings.push(`Similar agent exists: ${similar.name}`)
26
+ }
27
+
28
+ // Check if agent has specific skills
29
+ if (!config.expertise || config.expertise.length < 10) {
30
+ issues.push('Agent expertise is too generic')
31
+ }
32
+
33
+ // Check if agent has project context
34
+ if (!config.projectContext || Object.keys(config.projectContext).length === 0) {
35
+ warnings.push('Agent has no project-specific context')
36
+ }
37
+
38
+ // Check if agent name is descriptive
39
+ if (agentName.includes('specialist') && !config.expertise) {
40
+ issues.push('Agent name suggests specialization but has no expertise defined')
41
+ }
42
+
43
+ return {
44
+ valid: issues.length === 0,
45
+ issues,
46
+ warnings,
47
+ similarAgent: similar
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Validate agent after generation
53
+ * @param {Object} agent - Generated agent
54
+ * @returns {Object} Validation result
55
+ */
56
+ validateAfterGeneration(agent) {
57
+ const issues = []
58
+ const warnings = []
59
+
60
+ // Check if agent has content
61
+ if (!agent.content || agent.content.length < 100) {
62
+ issues.push('Agent content is too short or missing')
63
+ }
64
+
65
+ // Check if agent has skills extracted
66
+ if (!agent.skills || agent.skills.length === 0) {
67
+ warnings.push('Agent has no skills detected')
68
+ }
69
+
70
+ // Check if agent has domain
71
+ if (!agent.domain || agent.domain === 'general') {
72
+ warnings.push('Agent domain is generic')
73
+ }
74
+
75
+ // Check if agent is useful (not too generic)
76
+ const usefulness = this.calculateUsefulness(agent)
77
+ if (usefulness < 0.5) {
78
+ issues.push('Agent is too generic to be useful')
79
+ }
80
+
81
+ return {
82
+ valid: issues.length === 0,
83
+ issues,
84
+ warnings,
85
+ usefulness
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Find similar existing agent
91
+ */
92
+ findSimilarAgent(agentName, config, existingAgents) {
93
+ for (const existing of existingAgents) {
94
+ // Check name similarity
95
+ if (this.namesSimilar(agentName, existing.name)) {
96
+ return existing
97
+ }
98
+
99
+ // Check domain similarity
100
+ if (config.domain && existing.domain && config.domain === existing.domain) {
101
+ // Check if skills overlap significantly
102
+ const skillOverlap = this.calculateSkillOverlap(config, existing)
103
+ if (skillOverlap > 0.7) {
104
+ return existing
105
+ }
106
+ }
107
+ }
108
+
109
+ return null
110
+ }
111
+
112
+ /**
113
+ * Check if agent names are similar
114
+ */
115
+ namesSimilar(name1, name2) {
116
+ const n1 = name1.toLowerCase()
117
+ const n2 = name2.toLowerCase()
118
+
119
+ // Exact match
120
+ if (n1 === n2) return true
121
+
122
+ // One contains the other
123
+ if (n1.includes(n2) || n2.includes(n1)) return true
124
+
125
+ // Check word overlap
126
+ const words1 = new Set(n1.split('-'))
127
+ const words2 = new Set(n2.split('-'))
128
+ const intersection = new Set([...words1].filter(w => words2.has(w)))
129
+ const union = new Set([...words1, ...words2])
130
+
131
+ return intersection.size / union.size > 0.5
132
+ }
133
+
134
+ /**
135
+ * Calculate skill overlap between config and existing agent
136
+ */
137
+ calculateSkillOverlap(config, existingAgent) {
138
+ if (!existingAgent.skills || existingAgent.skills.length === 0) {
139
+ return 0
140
+ }
141
+
142
+ // Extract skills from config expertise
143
+ const configSkills = this.extractSkillsFromText(config.expertise || '')
144
+ if (configSkills.length === 0) {
145
+ return 0
146
+ }
147
+
148
+ // Calculate overlap
149
+ const existingSet = new Set(existingAgent.skills.map(s => s.toLowerCase()))
150
+ const configSet = new Set(configSkills.map(s => s.toLowerCase()))
151
+
152
+ const intersection = new Set([...existingSet].filter(s => configSet.has(s)))
153
+ const union = new Set([...existingSet, ...configSet])
154
+
155
+ return intersection.size / union.size
156
+ }
157
+
158
+ /**
159
+ * Extract skills from text
160
+ */
161
+ extractSkillsFromText(text) {
162
+ // Common technology keywords
163
+ const techKeywords = [
164
+ 'React', 'Vue', 'Angular', 'Svelte',
165
+ 'Next.js', 'Nuxt', 'SvelteKit',
166
+ 'TypeScript', 'JavaScript',
167
+ 'Node.js', 'Express', 'Fastify',
168
+ 'Python', 'Django', 'Flask', 'FastAPI',
169
+ 'Go', 'Rust', 'Ruby', 'Rails',
170
+ 'PostgreSQL', 'MySQL', 'MongoDB'
171
+ ]
172
+
173
+ return techKeywords.filter(tech =>
174
+ text.toLowerCase().includes(tech.toLowerCase())
175
+ )
176
+ }
177
+
178
+ /**
179
+ * Calculate agent usefulness score
180
+ */
181
+ calculateUsefulness(agent) {
182
+ let score = 0
183
+
184
+ // Has skills
185
+ if (agent.skills && agent.skills.length > 0) {
186
+ score += 0.3
187
+ if (agent.skills.length > 3) {
188
+ score += 0.1 // Bonus for multiple skills
189
+ }
190
+ }
191
+
192
+ // Has specific domain
193
+ if (agent.domain && agent.domain !== 'general') {
194
+ score += 0.2
195
+ }
196
+
197
+ // Has content
198
+ if (agent.content && agent.content.length > 200) {
199
+ score += 0.2
200
+ }
201
+
202
+ // Has role
203
+ if (agent.role && agent.role.length > 10) {
204
+ score += 0.1
205
+ }
206
+
207
+ // Not generic name
208
+ if (agent.name && !agent.name.includes('generalist')) {
209
+ score += 0.1
210
+ }
211
+
212
+ return Math.min(score, 1.0)
213
+ }
214
+ }
215
+
216
+ module.exports = AgentValidator
217
+
@@ -0,0 +1,175 @@
1
+ /**
2
+ * ContextEstimator - Pre-filter files before building context
3
+ *
4
+ * Estimates which files are needed based on task analysis
5
+ * BEFORE building full context - saves I/O
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ const path = require('path')
11
+ const { glob } = require('glob')
12
+
13
+ class ContextEstimator {
14
+ /**
15
+ * Estimate which files are needed for a task
16
+ * @param {Object} taskAnalysis - Task analysis result
17
+ * @param {string} projectPath - Project path
18
+ * @returns {Promise<Array<string>>} Estimated file paths
19
+ */
20
+ async estimateFiles(taskAnalysis, projectPath) {
21
+ const domain = taskAnalysis.primaryDomain
22
+ const projectTech = taskAnalysis.projectTechnologies || {}
23
+ const semantic = taskAnalysis.semantic || {}
24
+
25
+ // Get patterns for this domain
26
+ const patterns = this.getPatternsForDomain(domain, projectTech)
27
+
28
+ // Expand with semantic understanding
29
+ if (semantic.requiresMultipleAgents && semantic.agents) {
30
+ // Multi-agent task - combine patterns
31
+ const allPatterns = semantic.agents.reduce((acc, agentDomain) => {
32
+ const agentPatterns = this.getPatternsForDomain(agentDomain, projectTech)
33
+ return {
34
+ include: [...acc.include, ...agentPatterns.include],
35
+ extensions: [...acc.extensions, ...agentPatterns.extensions]
36
+ }
37
+ }, { include: [], extensions: [] })
38
+
39
+ patterns.include = [...new Set(allPatterns.include)]
40
+ patterns.extensions = [...new Set(allPatterns.extensions)]
41
+ }
42
+
43
+ // Find files matching patterns
44
+ const files = await this.findMatchingFiles(projectPath, patterns)
45
+
46
+ // Limit to reasonable number
47
+ const maxFiles = 200
48
+ return files.slice(0, maxFiles)
49
+ }
50
+
51
+ /**
52
+ * Get file patterns for a domain
53
+ */
54
+ getPatternsForDomain(domain, projectTech) {
55
+ const patterns = {
56
+ include: [],
57
+ extensions: [],
58
+ exclude: ['node_modules', 'dist', 'build', '.git']
59
+ }
60
+
61
+ switch (domain) {
62
+ case 'frontend':
63
+ patterns.include = ['src', 'components', 'pages', 'views', 'app']
64
+ patterns.extensions = ['.tsx', '.jsx', '.vue', '.svelte', '.css', '.scss', '.styled.js']
65
+
66
+ // Add framework-specific patterns
67
+ if (projectTech.frameworks) {
68
+ if (projectTech.frameworks.some(f => f.toLowerCase().includes('next'))) {
69
+ patterns.include.push('pages', 'app', 'components')
70
+ }
71
+ if (projectTech.frameworks.some(f => f.toLowerCase().includes('react'))) {
72
+ patterns.extensions.push('.tsx', '.jsx')
73
+ }
74
+ }
75
+ break
76
+
77
+ case 'backend':
78
+ patterns.include = ['src', 'lib', 'api', 'routes', 'controllers', 'services', 'app']
79
+ patterns.extensions = ['.js', '.ts', '.py', '.rb', '.go', '.rs']
80
+
81
+ // Framework-specific
82
+ if (projectTech.frameworks) {
83
+ if (projectTech.frameworks.some(f => f.toLowerCase().includes('express'))) {
84
+ patterns.include.push('routes', 'middleware', 'controllers')
85
+ }
86
+ if (projectTech.frameworks.some(f => f.toLowerCase().includes('django'))) {
87
+ patterns.include.push('views', 'urls', 'models')
88
+ }
89
+ }
90
+ break
91
+
92
+ case 'database':
93
+ patterns.include = ['migrations', 'models', 'schemas', 'db', 'database']
94
+ patterns.extensions = ['.sql', '.js', '.ts', '.rb', '.py']
95
+ break
96
+
97
+ case 'qa':
98
+ patterns.include = ['tests', '__tests__', 'spec', 'test']
99
+ patterns.extensions = ['.test.js', '.test.ts', '.spec.js', '.spec.ts']
100
+ break
101
+
102
+ case 'devops':
103
+ patterns.include = ['.github', '.gitlab', 'docker', 'k8s', 'kubernetes', 'terraform']
104
+ patterns.extensions = ['.yml', '.yaml', '.dockerfile', '.sh', '.tf']
105
+ break
106
+
107
+ default:
108
+ // General - include common source directories
109
+ patterns.include = ['src', 'lib', 'app']
110
+ patterns.extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb']
111
+ }
112
+
113
+ return patterns
114
+ }
115
+
116
+ /**
117
+ * Find files matching patterns
118
+ */
119
+ async findMatchingFiles(projectPath, patterns) {
120
+ const files = []
121
+
122
+ try {
123
+ // Build glob patterns
124
+ const globPatterns = []
125
+
126
+ // Add include patterns
127
+ for (const include of patterns.include) {
128
+ for (const ext of patterns.extensions) {
129
+ globPatterns.push(`${include}/**/*${ext}`)
130
+ globPatterns.push(`**/${include}/**/*${ext}`)
131
+ }
132
+ }
133
+
134
+ // Also search root level
135
+ for (const ext of patterns.extensions) {
136
+ globPatterns.push(`*${ext}`)
137
+ }
138
+
139
+ // Execute glob searches
140
+ for (const pattern of globPatterns) {
141
+ try {
142
+ const matches = await glob(pattern, {
143
+ cwd: projectPath,
144
+ ignore: patterns.exclude.map(ex => `**/${ex}/**`),
145
+ nodir: true,
146
+ follow: false
147
+ })
148
+
149
+ if (Array.isArray(matches)) {
150
+ files.push(...matches)
151
+ }
152
+ } catch (error) {
153
+ // Skip invalid patterns
154
+ }
155
+ }
156
+
157
+ // Remove duplicates and sort
158
+ return [...new Set(files)].sort()
159
+ } catch (error) {
160
+ console.error('Error finding files:', error.message)
161
+ return []
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Estimate context size (number of files)
167
+ */
168
+ async estimateContextSize(taskAnalysis, projectPath) {
169
+ const files = await this.estimateFiles(taskAnalysis, projectPath)
170
+ return files.length
171
+ }
172
+ }
173
+
174
+ module.exports = ContextEstimator
175
+
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Modern Product Standards
3
+ * Defines what "Good" looks like for each domain.
4
+ * Used to inject high standards into agent prompts.
5
+ */
6
+
7
+ const ProductStandards = {
8
+ // General standards applicable to all agents
9
+ general: [
10
+ 'SHIP IT: Bias for action. Better to ship and iterate than perfect and delay.',
11
+ 'USER CENTRIC: Always ask "How does this help the user?"',
12
+ 'CLEAN CODE: Write code that is easy to read, test, and maintain.',
13
+ 'NO BS: Avoid over-engineering. Simple is better than complex.'
14
+ ],
15
+
16
+ // Domain-specific standards
17
+ domains: {
18
+ frontend: {
19
+ title: 'Modern Frontend Standards',
20
+ rules: [
21
+ 'PERFORMANCE: Core Web Vitals matter. Optimize LCP, CLS, FID.',
22
+ 'ACCESSIBILITY: Semantic HTML, ARIA labels, keyboard navigation (WCAG 2.1 AA).',
23
+ 'RESPONSIVE: Mobile-first design. Works on all devices.',
24
+ 'UX/UI: Smooth transitions, loading states, error boundaries. No dead clicks.',
25
+ 'STATE: Local state for UI, Global state (Context/Zustand) for data. No prop drilling.'
26
+ ]
27
+ },
28
+ backend: {
29
+ title: 'Robust Backend Standards',
30
+ rules: [
31
+ 'SECURITY: Validate ALL inputs. Sanitize outputs. OWASP Top 10 awareness.',
32
+ 'SCALABILITY: Stateless services. Caching strategies (Redis/CDN).',
33
+ 'RELIABILITY: Graceful error handling. Structured logging. Health checks.',
34
+ 'API DESIGN: RESTful or GraphQL best practices. Consistent response envelopes.',
35
+ 'DB: Indexed queries. Migrations for schema changes. No N+1 queries.'
36
+ ]
37
+ },
38
+ database: {
39
+ title: 'Data Integrity Standards',
40
+ rules: [
41
+ 'INTEGRITY: Foreign keys, constraints, transactions.',
42
+ 'PERFORMANCE: Index usage analysis. Query optimization.',
43
+ 'BACKUPS: Point-in-time recovery awareness.',
44
+ 'MIGRATIONS: Idempotent scripts. Zero-downtime changes.'
45
+ ]
46
+ },
47
+ devops: {
48
+ title: 'Modern Ops Standards',
49
+ rules: [
50
+ 'AUTOMATION: CI/CD for everything. No manual deployments.',
51
+ 'IaC: Infrastructure as Code (Terraform/Pulumi).',
52
+ 'OBSERVABILITY: Metrics, Logs, Traces (OpenTelemetry).',
53
+ 'SECURITY: Least privilege access. Secrets management.'
54
+ ]
55
+ },
56
+ qa: {
57
+ title: 'Quality Assurance Standards',
58
+ rules: [
59
+ 'PYRAMID: Many unit tests, some integration, few E2E.',
60
+ 'COVERAGE: Critical paths must be tested.',
61
+ 'REALISM: Test with realistic data and scenarios.',
62
+ 'SPEED: Fast feedback loops. Parallel execution.'
63
+ ]
64
+ },
65
+ architecture: {
66
+ title: 'System Architecture Standards',
67
+ rules: [
68
+ 'MODULARITY: High cohesion, low coupling.',
69
+ 'EVOLVABILITY: Easy to change. Hard to break.',
70
+ 'SIMPLICITY: Choose boring technology. Innovation tokens are limited.',
71
+ 'DOCS: Architecture Decision Records (ADRs).'
72
+ ]
73
+ }
74
+ },
75
+
76
+ /**
77
+ * Get standards for a specific domain
78
+ * @param {string} domain - The domain (frontend, backend, etc.)
79
+ * @returns {Object} The standards object
80
+ */
81
+ getStandards(domain) {
82
+ const key = domain?.toLowerCase();
83
+ const specific = this.domains[key] || { title: 'General Standards', rules: [] };
84
+
85
+ return {
86
+ title: specific.title,
87
+ rules: [...this.general, ...specific.rules]
88
+ };
89
+ }
90
+ };
91
+
92
+ module.exports = ProductStandards;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * SmartCache - Intelligent Persistent Cache for Agents
3
+ *
4
+ * Cache with specific keys: {projectId}-{domain}-{techStack}
5
+ * Persists to disk for cross-session caching
6
+ * Invalidates only when stack changes
7
+ *
8
+ * @version 1.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises
12
+ const path = require('path')
13
+ const os = require('os')
14
+ const crypto = require('crypto')
15
+
16
+ class SmartCache {
17
+ constructor(projectId = null) {
18
+ this.projectId = projectId
19
+ this.memoryCache = new Map()
20
+ this.cacheDir = path.join(os.homedir(), '.prjct-cli', 'cache')
21
+ this.cacheFile = projectId
22
+ ? path.join(this.cacheDir, `agents-${projectId}.json`)
23
+ : path.join(this.cacheDir, 'agents-global.json')
24
+ }
25
+
26
+ /**
27
+ * Initialize cache - load from disk
28
+ */
29
+ async initialize() {
30
+ try {
31
+ await fs.mkdir(this.cacheDir, { recursive: true })
32
+ await this.loadFromDisk()
33
+ } catch (error) {
34
+ // Cache file doesn't exist yet - that's ok
35
+ this.memoryCache = new Map()
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Generate cache key
41
+ * Format: {projectId}-{domain}-{techStackHash}
42
+ */
43
+ generateKey(projectId, domain, techStack = {}) {
44
+ const techString = JSON.stringify(techStack)
45
+ const techHash = crypto.createHash('md5').update(techString).digest('hex').substring(0, 8)
46
+ return `${projectId || 'global'}-${domain}-${techHash}`
47
+ }
48
+
49
+ /**
50
+ * Get from cache
51
+ */
52
+ async get(key) {
53
+ // Check memory first
54
+ if (this.memoryCache.has(key)) {
55
+ return this.memoryCache.get(key)
56
+ }
57
+
58
+ // Load from disk if not in memory
59
+ await this.loadFromDisk()
60
+ return this.memoryCache.get(key) || null
61
+ }
62
+
63
+ /**
64
+ * Set in cache
65
+ */
66
+ async set(key, value) {
67
+ // Set in memory
68
+ this.memoryCache.set(key, value)
69
+
70
+ // Persist to disk (async, don't wait)
71
+ this.persistToDisk().catch(err => {
72
+ console.error('Cache persist error:', err.message)
73
+ })
74
+ }
75
+
76
+ /**
77
+ * Check if key exists
78
+ */
79
+ async has(key) {
80
+ if (this.memoryCache.has(key)) {
81
+ return true
82
+ }
83
+
84
+ await this.loadFromDisk()
85
+ return this.memoryCache.has(key)
86
+ }
87
+
88
+ /**
89
+ * Clear cache
90
+ */
91
+ async clear() {
92
+ this.memoryCache.clear()
93
+ try {
94
+ await fs.unlink(this.cacheFile)
95
+ } catch {
96
+ // File doesn't exist - that's ok
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Invalidate cache for a project (when stack changes)
102
+ */
103
+ async invalidateProject(projectId) {
104
+ const keysToDelete = []
105
+ for (const key of this.memoryCache.keys()) {
106
+ if (key.startsWith(`${projectId}-`)) {
107
+ keysToDelete.push(key)
108
+ }
109
+ }
110
+
111
+ keysToDelete.forEach(key => this.memoryCache.delete(key))
112
+ await this.persistToDisk()
113
+ }
114
+
115
+ /**
116
+ * Load cache from disk
117
+ */
118
+ async loadFromDisk() {
119
+ try {
120
+ const content = await fs.readFile(this.cacheFile, 'utf-8')
121
+ const data = JSON.parse(content)
122
+
123
+ // Restore to memory cache
124
+ for (const [key, value] of Object.entries(data)) {
125
+ this.memoryCache.set(key, value)
126
+ }
127
+ } catch {
128
+ // File doesn't exist or invalid - start fresh
129
+ this.memoryCache = new Map()
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Persist cache to disk
135
+ */
136
+ async persistToDisk() {
137
+ try {
138
+ const data = Object.fromEntries(this.memoryCache)
139
+ await fs.writeFile(this.cacheFile, JSON.stringify(data, null, 2), 'utf-8')
140
+ } catch (error) {
141
+ // Fail silently - cache is best effort
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get cache statistics
147
+ */
148
+ getStats() {
149
+ return {
150
+ size: this.memoryCache.size,
151
+ keys: Array.from(this.memoryCache.keys())
152
+ }
153
+ }
154
+ }
155
+
156
+ module.exports = SmartCache
157
+