@wipal/agent-team 1.1.3 → 1.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipal/agent-team",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "CLI tool to add AI agent teams to existing projects with specialized roles, skills, and workflows (v2.1 with OpenFang patterns)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Projects API Routes
3
+ */
4
+
5
+ import { Router } from 'express';
6
+ import { listValidProjects, getProject, updateProjectAccess } from '../../utils/global-registry.js';
7
+
8
+ const router = Router();
9
+
10
+ /**
11
+ * List all registered projects
12
+ */
13
+ router.get('/', async (req, res) => {
14
+ try {
15
+ const projects = await listValidProjects();
16
+ res.json({ projects });
17
+ } catch (error) {
18
+ res.status(500).json({ error: error.message });
19
+ }
20
+ });
21
+
22
+ /**
23
+ * Get current project
24
+ */
25
+ router.get('/current', async (req, res) => {
26
+ try {
27
+ const projectRoot = req.app.get('projectRoot');
28
+ const project = await getProject(projectRoot);
29
+
30
+ res.json({
31
+ path: projectRoot,
32
+ ...project
33
+ });
34
+ } catch (error) {
35
+ res.status(500).json({ error: error.message });
36
+ }
37
+ });
38
+
39
+ /**
40
+ * Switch to a different project
41
+ */
42
+ router.post('/switch', async (req, res) => {
43
+ try {
44
+ const { path: projectPath } = req.body;
45
+
46
+ if (!projectPath) {
47
+ return res.status(400).json({ error: 'Project path is required' });
48
+ }
49
+
50
+ const project = await getProject(projectPath);
51
+ if (!project) {
52
+ return res.status(404).json({ error: 'Project not found in registry' });
53
+ }
54
+
55
+ // Update app's project root
56
+ req.app.set('projectRoot', projectPath);
57
+ await updateProjectAccess(projectPath);
58
+
59
+ res.json({
60
+ success: true,
61
+ project: {
62
+ path: projectPath,
63
+ ...project
64
+ }
65
+ });
66
+ } catch (error) {
67
+ res.status(500).json({ error: error.message });
68
+ }
69
+ });
70
+
71
+ export default router;
@@ -10,6 +10,7 @@ import open from 'open';
10
10
  import agentsRouter from './api/agents.js';
11
11
  import rolesRouter from './api/roles.js';
12
12
  import skillsRouter from './api/skills.js';
13
+ import projectsRouter from './api/projects.js';
13
14
 
14
15
  const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = path.dirname(__filename);
@@ -37,6 +38,7 @@ export function createApp(projectRoot = process.cwd()) {
37
38
  app.use('/api/agents', agentsRouter);
38
39
  app.use('/api/roles', rolesRouter);
39
40
  app.use('/api/skills', skillsRouter);
41
+ app.use('/api/projects', projectsRouter);
40
42
 
41
43
  // Main page redirect
42
44
  app.get('/', (req, res) => {
@@ -26,7 +26,7 @@
26
26
 
27
27
  <!-- Add Agent Button -->
28
28
  <div class="actions-bar">
29
- <button class="btn btn-primary" @click="showAddModal = true">
29
+ <button class="btn btn-primary" @click="openAddModal()">
30
30
  ➕ Add New Agent
31
31
  </button>
32
32
  </div>
@@ -49,42 +49,55 @@
49
49
  </div>
50
50
 
51
51
  <div class="modal-body">
52
- <!-- Step 1: Basic Info -->
52
+ <!-- Step 1: Agent Name -->
53
53
  <div class="form-group">
54
- <label>Agent Name</label>
54
+ <label>Agent Name *</label>
55
55
  <input type="text" x-model="newAgent.name" placeholder="my-agent"
56
56
  class="input" pattern="[a-z0-9-]+">
57
57
  <small class="hint">Use lowercase letters, numbers, and hyphens</small>
58
58
  </div>
59
59
 
60
- <!-- Step 2: Select Role -->
60
+ <!-- Step 2: Select Role (Dropdown) -->
61
61
  <div class="form-group">
62
- <label>Role</label>
63
- <div class="role-selector" hx-get="/api/agents/meta/roles" hx-trigger="load">
64
- <div class="loading">Loading roles...</div>
65
- </div>
62
+ <label>Role *</label>
63
+ <select x-model="newAgent.role" class="input" @change="onRoleChange()" :disabled="loadingRoles">
64
+ <option value="">-- Select Role --</option>
65
+ <template x-for="role in roles" :key="role.name">
66
+ <option :value="role.name" x-text="role.name"></option>
67
+ </template>
68
+ </select>
69
+ <small class="hint" x-show="selectedRoleDescription" x-text="selectedRoleDescription"></small>
66
70
  </div>
67
71
 
68
- <!-- Step 3: Select Variants (shown when role selected) -->
69
- <div x-show="newAgent.role" x-cloak class="variants-section">
70
- <h3>Select Variants</h3>
71
- <div id="variants-container"
72
- hx-get="/api/agents/meta/variants/"
73
- :hx-get="'/api/agents/meta/variants/' + newAgent.role"
74
- hx-trigger="roleSelected from:body">
75
- <div class="loading">Loading variants...</div>
76
- </div>
72
+ <!-- Step 3: Select Variants (Dropdowns) -->
73
+ <div x-show="newAgent.role && Object.keys(variantCategories).length > 0" x-cloak class="variants-section">
74
+ <h3>Select Variants (Optional)</h3>
75
+ <template x-for="(category, key) in variantCategories" :key="key">
76
+ <div class="form-group">
77
+ <label x-text="category.label"></label>
78
+ <select class="input" @change="selectVariant(key, $event.target.value)">
79
+ <option value="">-- Select --</option>
80
+ <template x-for="option in category.options" :key="option.value">
81
+ <option :value="option.value"
82
+ :selected="newAgent.variants[key] === option.value"
83
+ x-text="option.label"></option>
84
+ </template>
85
+ </select>
86
+ </div>
87
+ </template>
77
88
  </div>
78
89
 
79
90
  <!-- Step 4: Preview Skills -->
80
91
  <div x-show="newAgent.role" x-cloak class="skills-preview">
81
92
  <h3>Skills Preview</h3>
82
- <div id="skills-preview"
83
- hx-post="/api/agents/preview-skills"
84
- hx-include="[name='variants']"
85
- hx-vals='{"role": newAgent.role}'
86
- hx-trigger="load, change from:.variant-checkbox">
87
- <div class="loading">Calculating skills...</div>
93
+ <div x-show="loadingSkills" class="loading">Calculating skills...</div>
94
+ <div x-show="!loadingSkills && previewSkills.length > 0" class="skills-list">
95
+ <template x-for="skill in previewSkills" :key="skill">
96
+ <span class="skill-tag" x-text="skill"></span>
97
+ </template>
98
+ </div>
99
+ <div x-show="!loadingSkills && previewSkills.length === 0" class="hint">
100
+ Select variants to see included skills
88
101
  </div>
89
102
  </div>
90
103
  </div>
@@ -92,8 +105,9 @@
92
105
  <div class="modal-footer">
93
106
  <button class="btn btn-secondary" @click="showAddModal = false">Cancel</button>
94
107
  <button class="btn btn-primary" @click="createAgent()"
95
- :disabled="!newAgent.name || !newAgent.role">
96
- Create Agent
108
+ :disabled="!newAgent.name || !newAgent.role || creating">
109
+ <span x-show="!creating">Create Agent</span>
110
+ <span x-show="creating">Creating...</span>
97
111
  </button>
98
112
  </div>
99
113
  </div>
@@ -104,27 +118,102 @@
104
118
  function agentApp() {
105
119
  return {
106
120
  showAddModal: false,
121
+ loadingRoles: false,
122
+ loadingSkills: false,
123
+ creating: false,
124
+ roles: [],
125
+ variantCategories: {},
126
+ previewSkills: [],
107
127
  newAgent: {
108
128
  name: '',
109
129
  role: '',
110
130
  variants: {}
111
131
  },
112
132
 
113
- selectRole(role) {
114
- this.newAgent.role = role;
115
- // Trigger variant loading
116
- htmx.trigger(document.body, 'roleSelected', { role });
133
+ get selectedRoleDescription() {
134
+ const role = this.roles.find(r => r.name === this.newAgent.role);
135
+ return role ? role.description : '';
117
136
  },
118
137
 
119
- toggleVariant(category, value) {
120
- if (this.newAgent.variants[category] === value) {
121
- delete this.newAgent.variants[category];
122
- } else {
138
+ async openAddModal() {
139
+ this.showAddModal = true;
140
+ await this.loadRoles();
141
+ },
142
+
143
+ async loadRoles() {
144
+ if (this.roles.length > 0) return; // Already loaded
145
+
146
+ this.loadingRoles = true;
147
+ try {
148
+ const response = await fetch('/api/agents/meta/roles');
149
+ this.roles = await response.json();
150
+ } catch (error) {
151
+ console.error('Failed to load roles:', error);
152
+ } finally {
153
+ this.loadingRoles = false;
154
+ }
155
+ },
156
+
157
+ async onRoleChange() {
158
+ // Reset variants and skills
159
+ this.newAgent.variants = {};
160
+ this.previewSkills = [];
161
+ this.variantCategories = {};
162
+
163
+ if (!this.newAgent.role) return;
164
+
165
+ // Load variants for selected role
166
+ await this.loadVariants(this.newAgent.role);
167
+ // Load skills preview
168
+ await this.loadSkillsPreview();
169
+ },
170
+
171
+ async loadVariants(role) {
172
+ try {
173
+ const response = await fetch(`/api/agents/meta/variants/${role}`);
174
+ const data = await response.json();
175
+ this.variantCategories = data.categories || data;
176
+ } catch (error) {
177
+ console.error('Failed to load variants:', error);
178
+ }
179
+ },
180
+
181
+ async selectVariant(category, value) {
182
+ if (value) {
123
183
  this.newAgent.variants[category] = value;
184
+ } else {
185
+ delete this.newAgent.variants[category];
186
+ }
187
+ // Update skills preview
188
+ await this.loadSkillsPreview();
189
+ },
190
+
191
+ async loadSkillsPreview() {
192
+ if (!this.newAgent.role) return;
193
+
194
+ this.loadingSkills = true;
195
+ try {
196
+ const response = await fetch('/api/agents/preview-skills', {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify({
200
+ role: this.newAgent.role,
201
+ variants: this.newAgent.variants
202
+ })
203
+ });
204
+ const data = await response.json();
205
+ this.previewSkills = data.skills || data.core?.concat(data.role || []) || [];
206
+ } catch (error) {
207
+ console.error('Failed to load skills preview:', error);
208
+ } finally {
209
+ this.loadingSkills = false;
124
210
  }
125
211
  },
126
212
 
127
213
  async createAgent() {
214
+ if (!this.newAgent.name || !this.newAgent.role) return;
215
+
216
+ this.creating = true;
128
217
  try {
129
218
  const response = await fetch('/api/agents', {
130
219
  method: 'POST',
@@ -143,32 +232,18 @@
143
232
  }
144
233
  } catch (error) {
145
234
  alert('Error creating agent: ' + error.message);
235
+ } finally {
236
+ this.creating = false;
146
237
  }
147
238
  },
148
239
 
149
240
  resetForm() {
150
241
  this.newAgent = { name: '', role: '', variants: {} };
242
+ this.variantCategories = {};
243
+ this.previewSkills = [];
151
244
  }
152
245
  }
153
246
  }
154
-
155
- // Transform role list to radio buttons
156
- document.body.addEventListener('htmx:beforeSwap', function(evt) {
157
- if (evt.detail.pathInfo.requestPath.includes('/meta/roles')) {
158
- try {
159
- const roles = JSON.parse(evt.detail.xhr.response);
160
- evt.detail.xhr.response = roles.map(r => `
161
- <label class="role-option" :class="{'selected': newAgent.role === '${r.name}'}">
162
- <input type="radio" name="role" value="${r.name}" @click="selectRole('${r.name}')">
163
- <div class="role-info">
164
- <strong>${r.name}</strong>
165
- <p>${r.description}</p>
166
- </div>
167
- </label>
168
- `).join('');
169
- } catch (e) {}
170
- }
171
- });
172
247
  </script>
173
248
  </body>
174
249
  </html>
@@ -36,15 +36,63 @@ body {
36
36
 
37
37
  /* Header */
38
38
  .header {
39
- text-align: center;
40
39
  margin-bottom: 2rem;
41
40
  }
42
41
 
42
+ .header-row {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: flex-start;
46
+ flex-wrap: wrap;
47
+ gap: 1rem;
48
+ }
49
+
50
+ .header-row > div:first-child {
51
+ text-align: left;
52
+ }
53
+
43
54
  .header h1 {
44
55
  font-size: 2rem;
45
56
  margin-bottom: 0.5rem;
46
57
  }
47
58
 
59
+ /* Project Selector */
60
+ .project-selector {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 0.5rem;
64
+ padding: 0.75rem 1rem;
65
+ background: var(--bg-card);
66
+ border-radius: 0.5rem;
67
+ box-shadow: var(--shadow);
68
+ }
69
+
70
+ .project-selector label {
71
+ font-weight: 500;
72
+ color: var(--text-muted);
73
+ font-size: 0.875rem;
74
+ }
75
+
76
+ .project-selector select {
77
+ padding: 0.5rem 0.75rem;
78
+ border: 1px solid var(--border);
79
+ border-radius: 0.375rem;
80
+ font-size: 0.875rem;
81
+ min-width: 200px;
82
+ cursor: pointer;
83
+ }
84
+
85
+ .project-selector select:focus {
86
+ outline: none;
87
+ border-color: var(--primary);
88
+ }
89
+
90
+ .loading-indicator {
91
+ font-size: 0.75rem;
92
+ color: var(--primary);
93
+ font-style: italic;
94
+ }
95
+
48
96
  .subtitle {
49
97
  color: var(--text-muted);
50
98
  }
package/src/ui/index.html CHANGED
@@ -10,10 +10,26 @@
10
10
  </head>
11
11
  <body>
12
12
  <div class="container">
13
- <!-- Header -->
13
+ <!-- Header with Project Selector -->
14
14
  <header class="header">
15
- <h1>🤖 Agent Team Dashboard</h1>
16
- <p class="subtitle">Manage AI agents for your project</p>
15
+ <div class="header-row">
16
+ <div>
17
+ <h1>🤖 Agent Team Dashboard</h1>
18
+ <p class="subtitle">Manage AI agents for your project</p>
19
+ </div>
20
+ <!-- Project Selector -->
21
+ <div class="project-selector" x-data="projectSelector()">
22
+ <label>Project:</label>
23
+ <select @change="switchProject($event)" x-model="currentProject" :disabled="loading">
24
+ <template x-for="project in projects" :key="project.path">
25
+ <option :value="project.path" :selected="project.path === currentProject">
26
+ <span x-text="project.name"></span>
27
+ </option>
28
+ </template>
29
+ </select>
30
+ <span x-show="loading" class="loading-indicator">Switching...</span>
31
+ </div>
32
+ </div>
17
33
  </header>
18
34
 
19
35
  <!-- Navigation -->
@@ -25,7 +41,7 @@
25
41
  </nav>
26
42
 
27
43
  <!-- Stats Cards -->
28
- <div class="stats-grid" hx-get="/api/agents" hx-trigger="load" hx-swap="innerHTML">
44
+ <div class="stats-grid" hx-get="/api/agents" hx-trigger="load, projectChanged from:body" hx-swap="innerHTML">
29
45
  <!-- Loaded via HTMX -->
30
46
  <div class="loading">Loading...</div>
31
47
  </div>
@@ -49,7 +65,7 @@
49
65
  <!-- Recent Agents -->
50
66
  <section class="section">
51
67
  <h2>Your Agents</h2>
52
- <div id="agents-list" hx-get="/api/agents" hx-trigger="load">
68
+ <div id="agents-list" hx-get="/api/agents" hx-trigger="load, projectChanged from:body">
53
69
  <div class="loading">Loading agents...</div>
54
70
  </div>
55
71
  </section>
@@ -64,13 +80,75 @@
64
80
  </div>
65
81
 
66
82
  <script>
83
+ // Project Selector Alpine Component
84
+ function projectSelector() {
85
+ return {
86
+ projects: [],
87
+ currentProject: '',
88
+ loading: false,
89
+
90
+ async init() {
91
+ await this.loadProjects();
92
+ await this.loadCurrentProject();
93
+ },
94
+
95
+ async loadProjects() {
96
+ try {
97
+ const response = await fetch('/api/projects');
98
+ const data = await response.json();
99
+ this.projects = data.projects.filter(p => p.valid);
100
+ } catch (error) {
101
+ console.error('Failed to load projects:', error);
102
+ }
103
+ },
104
+
105
+ async loadCurrentProject() {
106
+ try {
107
+ const response = await fetch('/api/projects/current');
108
+ const project = await response.json();
109
+ this.currentProject = project.path;
110
+ } catch (error) {
111
+ console.error('Failed to load current project:', error);
112
+ }
113
+ },
114
+
115
+ async switchProject(event) {
116
+ const newPath = event.target.value;
117
+ if (newPath === this.currentProject) return;
118
+
119
+ this.loading = true;
120
+ try {
121
+ const response = await fetch('/api/projects/switch', {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({ path: newPath })
125
+ });
126
+
127
+ if (response.ok) {
128
+ this.currentProject = newPath;
129
+ // Trigger HTMX refresh
130
+ document.body.dispatchEvent(new CustomEvent('projectChanged'));
131
+ } else {
132
+ const error = await response.json();
133
+ alert('Failed to switch project: ' + error.error);
134
+ }
135
+ } catch (error) {
136
+ alert('Failed to switch project: ' + error.message);
137
+ } finally {
138
+ this.loading = false;
139
+ }
140
+ }
141
+ }
142
+ }
143
+
67
144
  // Transform stats response
68
145
  document.body.addEventListener('htmx:beforeSwap', function(evt) {
69
146
  if (evt.detail.pathInfo.requestPath === '/api/agents') {
70
147
  // First load - render as stats
71
148
  if (evt.detail.target.matches('.stats-grid')) {
72
149
  try {
73
- const agents = JSON.parse(evt.detail.xhr.response);
150
+ const data = JSON.parse(evt.detail.xhr.response);
151
+ const agents = data.agents || [];
74
152
  const statsHtml = renderStats(agents);
75
153
  evt.detail.swap = 'innerHTML';
76
154
  evt.detail.xhr.response = statsHtml;
@@ -93,11 +171,11 @@
93
171
  <div class="stat-label">Roles Used</div>
94
172
  </div>
95
173
  <div class="stat-card">
96
- <div class="stat-value">37</div>
174
+ <div class="stat-value">50</div>
97
175
  <div class="stat-label">Skills Available</div>
98
176
  </div>
99
177
  <div class="stat-card">
100
- <div class="stat-value">7</div>
178
+ <div class="stat-value">9</div>
101
179
  <div class="stat-label">Roles Available</div>
102
180
  </div>
103
181
  `;