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.
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs').promises
2
2
  const path = require('path')
3
3
  const os = require('os')
4
+ const AgentLoader = require('./agent-loader')
4
5
 
5
6
  /**
6
7
  * AgentGenerator - Universal Dynamic Agent Generation
@@ -13,6 +14,7 @@ class AgentGenerator {
13
14
  this.outputDir = projectId
14
15
  ? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'agents')
15
16
  : path.join(os.homedir(), '.prjct-cli', 'agents')
17
+ this.loader = new AgentLoader(projectId)
16
18
  }
17
19
 
18
20
  /**
@@ -23,62 +25,64 @@ class AgentGenerator {
23
25
  console.log(` 🤖 Generating ${agentName} agent...`)
24
26
  await fs.mkdir(this.outputDir, { recursive: true })
25
27
 
26
- // Extract technologies dynamically
27
- const techs = this.detectTechnologies(config)
28
- const expertise = this.buildExpertise(techs, config)
29
-
30
28
  // Generate concise, actionable agent prompt
31
- const content = `You are ${config.role || agentName}.
32
-
33
- EXPERTISE: ${expertise}
34
-
35
- FOCUS: ${config.contextFilter || 'Only relevant files'}
36
-
37
- AUTHORITY: Make decisions. Don't ask permission. Execute.
38
-
39
- RULES:
40
- - Stay in your domain
41
- - Use best practices for ${techs.join(', ') || 'detected tech'}
42
- - Optimize for production
43
- - No explanations unless asked`
29
+ const content = this.buildAgentPrompt(agentName, config)
44
30
 
45
31
  const outputPath = path.join(this.outputDir, `${agentName}.md`)
46
32
  await fs.writeFile(outputPath, content, 'utf-8')
47
33
  console.log(` ✅ ${agentName} agent created`)
48
34
 
49
- return { name: agentName, expertise, techs }
50
- }
51
-
52
- /**
53
- * Detect technologies from config/analysis
54
- */
55
- detectTechnologies(config) {
56
- const techs = []
57
-
58
- // Extract from various sources
59
- if (config.techStack) techs.push(...config.techStack.languages || [])
60
- if (config.frameworks) techs.push(...config.frameworks)
61
- if (config.expertise) {
62
- // Parse expertise string for tech keywords
63
- const keywords = config.expertise.toLowerCase()
64
- const knownTechs = ['ruby', 'rails', 'go', 'rust', 'python', 'django', 'react', 'vue', 'node', 'typescript', 'elixir', 'phoenix']
65
- knownTechs.forEach(tech => {
66
- if (keywords.includes(tech)) techs.push(tech)
67
- })
68
- }
69
-
70
- return [...new Set(techs)]
35
+ return { name: agentName }
71
36
  }
72
37
 
73
38
  /**
74
- * Build concise expertise string
39
+ * Build comprehensive agent prompt
75
40
  */
76
- buildExpertise(techs, config) {
77
- const tech = techs.length > 0 ? techs.join(', ') : 'detected stack'
78
- const domain = config.domain || 'assigned domain'
79
- const focus = config.responsibilities || 'task at hand'
80
-
81
- return `${tech} expert. ${domain}. Focus: ${focus}`
41
+ buildAgentPrompt(agentName, config) {
42
+ const domain = config.domain || 'general';
43
+
44
+ const projectContext = (typeof config.projectContext === 'object')
45
+ ? JSON.stringify(config.projectContext || {}, null, 2)
46
+ : (config.projectContext || 'No specific project context provided.');
47
+
48
+ return `# AGENT: ${agentName.toUpperCase()}
49
+ Role: ${config.role || agentName}
50
+
51
+ ## META-INSTRUCTION
52
+ You are an intelligent agent responsible for this domain.
53
+ Your first task is to ANALYZE the provided PROJECT CONTEXT to determine:
54
+ 1. The technology stack being used.
55
+ 2. The architectural patterns in place.
56
+ 3. The specific best practices relevant to this stack.
57
+
58
+ ## DOMAIN AUTHORITY
59
+ You are the owner of the ${domain} domain.
60
+ You have full authority to make technical decisions within this scope.
61
+
62
+ ## DYNAMIC STANDARDS
63
+ Instead of following a hardcoded list, you must:
64
+ - **DETECT**: Identify the languages, frameworks, and tools from the file extensions and content.
65
+ - **ADAPT**: Adopt the persona of a Senior Engineer specializing in the detected stack.
66
+ - **ENFORCE**: Apply the idiomatic best practices, naming conventions, and patterns of that stack.
67
+
68
+ ## ORCHESTRATION PROTOCOL
69
+ 1. **ANALYZE**: Read the context. Determine the stack.
70
+ 2. **PLAN**: Create a plan that fits the detected architecture.
71
+ 3. **EXECUTE**: Implement using the detected tools and patterns.
72
+ 4. **VERIFY**: Ensure code matches the project's existing style.
73
+
74
+ ## PROJECT CONTEXT
75
+ ${projectContext}
76
+
77
+ ## CONTEXT FOCUS
78
+ ${config.contextFilter || 'Only relevant files'}
79
+
80
+ ## RULES
81
+ - Stay in your domain (${domain})
82
+ - Do not assume a specific stack until you see the code.
83
+ - Optimize for production.
84
+ - No explanations unless asked.
85
+ `;
82
86
  }
83
87
 
84
88
  /**
@@ -124,6 +128,33 @@ RULES:
124
128
  return []
125
129
  }
126
130
  }
131
+
132
+ /**
133
+ * Load an agent from its file
134
+ * CRITICAL: This is how agents are actually used in prompts
135
+ * @param {string} agentName - Name of the agent (without .md extension)
136
+ * @returns {Promise<Object|null>} - Agent object with name, content, role, domain, skills, or null if not found
137
+ */
138
+ async loadAgent(agentName) {
139
+ return await this.loader.loadAgent(agentName)
140
+ }
141
+
142
+ /**
143
+ * Load all agents for the project
144
+ * @returns {Promise<Array<Object>>} - Array of agent objects
145
+ */
146
+ async loadAllAgents() {
147
+ return await this.loader.loadAllAgents()
148
+ }
149
+
150
+ /**
151
+ * Check if an agent exists
152
+ * @param {string} agentName - Name of the agent
153
+ * @returns {Promise<boolean>} - True if agent file exists
154
+ */
155
+ async agentExists(agentName) {
156
+ return await this.loader.agentExists(agentName)
157
+ }
127
158
  }
128
159
 
129
160
  module.exports = AgentGenerator
@@ -0,0 +1,183 @@
1
+ /**
2
+ * AgentLoader - Loads agents from project files
3
+ *
4
+ * CRITICAL: This ensures agents generated for a project are actually USED
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+
9
+ const fs = require('fs').promises
10
+ const path = require('path')
11
+ const os = require('os')
12
+
13
+ class AgentLoader {
14
+ constructor(projectId = null) {
15
+ this.projectId = projectId
16
+ this.agentsDir = projectId
17
+ ? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'agents')
18
+ : path.join(os.homedir(), '.prjct-cli', 'agents')
19
+ this.cache = new Map()
20
+ }
21
+
22
+ /**
23
+ * Load an agent from its file
24
+ * @param {string} agentName - Name of the agent (without .md extension)
25
+ * @returns {Promise<Object|null>} - Agent object with name and content, or null if not found
26
+ */
27
+ async loadAgent(agentName) {
28
+ // Check cache first
29
+ const cacheKey = `${this.projectId || 'global'}-${agentName}`
30
+ if (this.cache.has(cacheKey)) {
31
+ return this.cache.get(cacheKey)
32
+ }
33
+
34
+ try {
35
+ const agentPath = path.join(this.agentsDir, `${agentName}.md`)
36
+ const content = await fs.readFile(agentPath, 'utf-8')
37
+
38
+ // Parse agent metadata from content
39
+ const agent = {
40
+ name: agentName,
41
+ content,
42
+ path: agentPath,
43
+ // Extract role if present in content
44
+ role: this.extractRole(content),
45
+ // Extract domain if present
46
+ domain: this.extractDomain(content),
47
+ // Extract skills/technologies mentioned
48
+ skills: this.extractSkills(content),
49
+ // Last modified time
50
+ modified: (await fs.stat(agentPath)).mtime
51
+ }
52
+
53
+ // Cache it
54
+ this.cache.set(cacheKey, agent)
55
+
56
+ return agent
57
+ } catch (error) {
58
+ if (error.code === 'ENOENT') {
59
+ return null // Agent file doesn't exist
60
+ }
61
+ throw error
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Load all agents for the project
67
+ * @returns {Promise<Array<Object>>} - Array of agent objects
68
+ */
69
+ async loadAllAgents() {
70
+ try {
71
+ const files = await fs.readdir(this.agentsDir)
72
+ const agentFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.'))
73
+
74
+ const agents = []
75
+ for (const file of agentFiles) {
76
+ const agentName = file.replace('.md', '')
77
+ const agent = await this.loadAgent(agentName)
78
+ if (agent) {
79
+ agents.push(agent)
80
+ }
81
+ }
82
+
83
+ return agents
84
+ } catch (error) {
85
+ if (error.code === 'ENOENT') {
86
+ return [] // Agents directory doesn't exist yet
87
+ }
88
+ throw error
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check if an agent exists
94
+ * @param {string} agentName - Name of the agent
95
+ * @returns {Promise<boolean>} - True if agent file exists
96
+ */
97
+ async agentExists(agentName) {
98
+ try {
99
+ const agentPath = path.join(this.agentsDir, `${agentName}.md`)
100
+ await fs.access(agentPath)
101
+ return true
102
+ } catch {
103
+ return false
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Clear cache (useful after agent updates)
109
+ */
110
+ clearCache() {
111
+ this.cache.clear()
112
+ }
113
+
114
+ /**
115
+ * Extract role from agent content
116
+ * @private
117
+ */
118
+ extractRole(content) {
119
+ const roleMatch = content.match(/Role:\s*(.+)/i)
120
+ return roleMatch ? roleMatch[1].trim() : null
121
+ }
122
+
123
+ /**
124
+ * Extract domain from agent content
125
+ * @private
126
+ */
127
+ extractDomain(content) {
128
+ const domainMatch = content.match(/DOMAIN AUTHORITY[\s\S]*?the\s+(\w+)\s+domain/i)
129
+ if (domainMatch) {
130
+ return domainMatch[1].toLowerCase()
131
+ }
132
+
133
+ // Fallback: try to detect from agent name
134
+ const name = this.projectId ? '' : ''
135
+ if (name.includes('frontend')) return 'frontend'
136
+ if (name.includes('backend')) return 'backend'
137
+ if (name.includes('database')) return 'database'
138
+ if (name.includes('devops')) return 'devops'
139
+ if (name.includes('qa')) return 'qa'
140
+ if (name.includes('architect')) return 'architecture'
141
+
142
+ return 'general'
143
+ }
144
+
145
+ /**
146
+ * Extract skills/technologies mentioned in agent content
147
+ * @private
148
+ */
149
+ extractSkills(content) {
150
+ const skills = []
151
+
152
+ // Look for common technology mentions
153
+ const techKeywords = [
154
+ 'React', 'Vue', 'Angular', 'Svelte',
155
+ 'Next.js', 'Nuxt', 'SvelteKit',
156
+ 'TypeScript', 'JavaScript',
157
+ 'Node.js', 'Express', 'Fastify',
158
+ 'Python', 'Django', 'Flask', 'FastAPI',
159
+ 'Go', 'Rust', 'Ruby', 'Rails',
160
+ 'PostgreSQL', 'MySQL', 'MongoDB',
161
+ 'Docker', 'Kubernetes', 'Terraform'
162
+ ]
163
+
164
+ for (const tech of techKeywords) {
165
+ if (content.includes(tech)) {
166
+ skills.push(tech)
167
+ }
168
+ }
169
+
170
+ return skills
171
+ }
172
+
173
+ /**
174
+ * Get agents directory path
175
+ * @returns {string} - Path to agents directory
176
+ */
177
+ getAgentsDir() {
178
+ return this.agentsDir
179
+ }
180
+ }
181
+
182
+ module.exports = AgentLoader
183
+
@@ -0,0 +1,217 @@
1
+ /**
2
+ * AgentMatcher - Intelligent Agent Matching with Scoring
3
+ *
4
+ * Matches tasks to agents using multi-factor scoring:
5
+ * - Agent skills and capabilities
6
+ * - Historical success rates
7
+ * - Project technologies
8
+ * - Task complexity
9
+ *
10
+ * @version 1.0.0
11
+ */
12
+
13
+ class AgentMatcher {
14
+ constructor() {
15
+ this.historyCache = new Map()
16
+ }
17
+
18
+ /**
19
+ * Find best agent for a task using intelligent scoring
20
+ * @param {Array<Object>} availableAgents - All available agents
21
+ * @param {Object} taskAnalysis - Task analysis result
22
+ * @returns {Object|null} Best matching agent with score
23
+ */
24
+ findBestAgent(availableAgents, taskAnalysis) {
25
+ if (!availableAgents || availableAgents.length === 0) {
26
+ return null
27
+ }
28
+
29
+ // Score each agent
30
+ const scored = availableAgents.map(agent => {
31
+ const score = this.scoreAgent(agent, taskAnalysis)
32
+ return { agent, score }
33
+ })
34
+
35
+ // Sort by score descending
36
+ scored.sort((a, b) => b.score - a.score)
37
+
38
+ // Return best match if score is above threshold
39
+ const best = scored[0]
40
+ if (best && best.score > 0.3) {
41
+ return {
42
+ agent: best.agent,
43
+ score: best.score,
44
+ alternatives: scored.slice(1, 3).map(s => ({
45
+ agent: s.agent,
46
+ score: s.score
47
+ }))
48
+ }
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * Score an agent for a specific task
56
+ * Multi-factor scoring system
57
+ */
58
+ scoreAgent(agent, taskAnalysis) {
59
+ let score = 0
60
+
61
+ // Factor 1: Domain Match (40% weight)
62
+ const domainScore = this.scoreDomainMatch(agent, taskAnalysis)
63
+ score += domainScore * 0.4
64
+
65
+ // Factor 2: Skills Match (30% weight)
66
+ const skillsScore = this.scoreSkillsMatch(agent, taskAnalysis)
67
+ score += skillsScore * 0.3
68
+
69
+ // Factor 3: Historical Success (20% weight)
70
+ const historyScore = this.scoreHistoricalSuccess(agent, taskAnalysis)
71
+ score += historyScore * 0.2
72
+
73
+ // Factor 4: Complexity Match (10% weight)
74
+ const complexityScore = this.scoreComplexityMatch(agent, taskAnalysis)
75
+ score += complexityScore * 0.1
76
+
77
+ return Math.min(score, 1.0)
78
+ }
79
+
80
+ /**
81
+ * Score domain match
82
+ */
83
+ scoreDomainMatch(agent, taskAnalysis) {
84
+ const agentDomain = agent.domain || ''
85
+ const taskDomain = taskAnalysis.primaryDomain || ''
86
+
87
+ // Exact match
88
+ if (agentDomain === taskDomain) {
89
+ return 1.0
90
+ }
91
+
92
+ // Partial match (agent name contains domain)
93
+ if (agent.name && agent.name.includes(taskDomain)) {
94
+ return 0.7
95
+ }
96
+
97
+ // Check alternatives
98
+ if (taskAnalysis.alternatives && taskAnalysis.alternatives.includes(agentDomain)) {
99
+ return 0.5
100
+ }
101
+
102
+ return 0.1
103
+ }
104
+
105
+ /**
106
+ * Score skills match
107
+ */
108
+ scoreSkillsMatch(agent, taskAnalysis) {
109
+ if (!agent.skills || agent.skills.length === 0) {
110
+ return 0.2 // Generic agent penalty
111
+ }
112
+
113
+ const projectTech = taskAnalysis.projectTechnologies || {}
114
+ const allTech = [
115
+ ...(projectTech.languages || []),
116
+ ...(projectTech.frameworks || []),
117
+ ...(projectTech.tools || [])
118
+ ]
119
+
120
+ // Count matching skills
121
+ const matchingSkills = agent.skills.filter(skill => {
122
+ const skillLower = skill.toLowerCase()
123
+ return allTech.some(tech => tech.toLowerCase().includes(skillLower) ||
124
+ skillLower.includes(tech.toLowerCase()))
125
+ })
126
+
127
+ if (matchingSkills.length === 0) {
128
+ return 0.1 // No matching skills
129
+ }
130
+
131
+ // Score based on match ratio
132
+ const matchRatio = matchingSkills.length / Math.max(agent.skills.length, allTech.length)
133
+ return Math.min(matchRatio * 2, 1.0) // Boost for good matches
134
+ }
135
+
136
+ /**
137
+ * Score historical success
138
+ */
139
+ scoreHistoricalSuccess(agent, taskAnalysis) {
140
+ // TODO: Load from persistent history
141
+ // For now, return neutral score
142
+ const cacheKey = `${agent.name}-${taskAnalysis.primaryDomain}`
143
+ const history = this.historyCache.get(cacheKey)
144
+
145
+ if (history) {
146
+ // Success rate from history
147
+ return history.successRate || 0.5
148
+ }
149
+
150
+ return 0.5 // Neutral - no history
151
+ }
152
+
153
+ /**
154
+ * Score complexity match
155
+ */
156
+ scoreComplexityMatch(agent, taskAnalysis) {
157
+ const taskComplexity = taskAnalysis.complexity || 'medium'
158
+
159
+ // Generic agents are better for simple tasks
160
+ // Specialized agents are better for complex tasks
161
+ const isGeneric = !agent.skills || agent.skills.length === 0
162
+
163
+ if (taskComplexity === 'low' && isGeneric) {
164
+ return 0.8
165
+ }
166
+
167
+ if (taskComplexity === 'high' && !isGeneric) {
168
+ return 0.9
169
+ }
170
+
171
+ return 0.5 // Neutral
172
+ }
173
+
174
+ /**
175
+ * Record agent success for learning
176
+ */
177
+ recordSuccess(agent, taskAnalysis, success = true) {
178
+ const cacheKey = `${agent.name}-${taskAnalysis.primaryDomain}`
179
+ const history = this.historyCache.get(cacheKey) || {
180
+ attempts: 0,
181
+ successes: 0,
182
+ successRate: 0.5
183
+ }
184
+
185
+ history.attempts++
186
+ if (success) {
187
+ history.successes++
188
+ }
189
+ history.successRate = history.successes / history.attempts
190
+
191
+ this.historyCache.set(cacheKey, history)
192
+ }
193
+
194
+ /**
195
+ * Get match explanation
196
+ */
197
+ explainMatch(match) {
198
+ if (!match) {
199
+ return 'No suitable agent found'
200
+ }
201
+
202
+ const reasons = []
203
+
204
+ if (match.score > 0.8) {
205
+ reasons.push('Excellent match')
206
+ } else if (match.score > 0.6) {
207
+ reasons.push('Good match')
208
+ } else {
209
+ reasons.push('Acceptable match')
210
+ }
211
+
212
+ return reasons.join(', ')
213
+ }
214
+ }
215
+
216
+ module.exports = AgentMatcher
217
+