@wipal/agent-team 1.1.3 → 1.2.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.
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.0",
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) => {
@@ -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
  `;