groove-dev 0.22.31 → 0.24.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.
Files changed (52) hide show
  1. package/node_modules/@groove-dev/cli/src/setup.js +7 -9
  2. package/node_modules/@groove-dev/daemon/src/api.js +87 -4
  3. package/node_modules/@groove-dev/daemon/src/process.js +1 -0
  4. package/node_modules/@groove-dev/daemon/src/teams.js +77 -5
  5. package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
  6. package/node_modules/@groove-dev/daemon/test/teams.test.js +5 -5
  7. package/node_modules/@groove-dev/gui/dist/assets/index-CqdQP7yG.js +587 -0
  8. package/node_modules/@groove-dev/gui/dist/assets/index-DvcNOnKP.css +1 -0
  9. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  10. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +139 -0
  11. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +4 -1
  12. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +16 -14
  13. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +105 -0
  14. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -38
  15. package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +28 -9
  16. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
  17. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
  18. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +121 -0
  19. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +152 -34
  20. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
  21. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +4 -2
  23. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -52
  24. package/package.json +1 -1
  25. package/packages/cli/src/setup.js +7 -9
  26. package/packages/daemon/src/api.js +87 -4
  27. package/packages/daemon/src/process.js +1 -0
  28. package/packages/daemon/src/teams.js +77 -5
  29. package/packages/daemon/src/validate.js +1 -0
  30. package/packages/gui/dist/assets/index-CqdQP7yG.js +587 -0
  31. package/packages/gui/dist/assets/index-DvcNOnKP.css +1 -0
  32. package/packages/gui/dist/index.html +2 -2
  33. package/packages/gui/src/components/agents/agent-mdfiles.jsx +139 -0
  34. package/packages/gui/src/components/agents/agent-panel.jsx +4 -1
  35. package/packages/gui/src/components/dashboard/activity-feed.jsx +16 -14
  36. package/packages/gui/src/components/dashboard/cache-ring.jsx +105 -0
  37. package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -38
  38. package/packages/gui/src/components/dashboard/header-bar.jsx +28 -9
  39. package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
  40. package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
  41. package/packages/gui/src/components/dashboard/routing-chart.jsx +121 -0
  42. package/packages/gui/src/components/dashboard/token-chart.jsx +152 -34
  43. package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
  44. package/packages/gui/src/lib/theme-hex.js +7 -0
  45. package/packages/gui/src/stores/groove.js +4 -2
  46. package/packages/gui/src/views/dashboard.jsx +97 -52
  47. package/node_modules/@groove-dev/gui/dist/assets/index-CL4GvVoL.css +0 -1
  48. package/node_modules/@groove-dev/gui/dist/assets/index-D_tSBDCx.js +0 -577
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
  50. package/packages/gui/dist/assets/index-CL4GvVoL.css +0 -1
  51. package/packages/gui/dist/assets/index-D_tSBDCx.js +0 -577
  52. package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
@@ -209,16 +209,14 @@ const PROVIDERS = [
209
209
  export async function runSetupWizard() {
210
210
  const version = getVersion();
211
211
 
212
- const line = '──────────────────────────────────────';
213
- const vPad = ` v${version}`;
214
212
  console.log('');
215
- console.log(` ┌${line}┐`);
216
- console.log(` │${' '.repeat(38)}│`);
217
- console.log(` ${chalk.bold.cyan('G R O O V E')} │`);
218
- console.log(` Agent Orchestration Layer │`);
219
- console.log(` │${' '.repeat(38)}│`);
220
- console.log(` ${chalk.dim(vPad)}${' '.repeat(Math.max(0, 34 - vPad.length))}│`);
221
- console.log(` └${line}┘`);
213
+ console.log(' ┌────────────────────────────────────────┐');
214
+ console.log(' │ │');
215
+ console.log(' ' + chalk.bold.cyan('G R O O V E') + ' │');
216
+ console.log(' Agent Orchestration Layer │');
217
+ console.log(' │ │');
218
+ console.log(' ' + chalk.dim(`v${version}`) + ' '.repeat(Math.max(1, 36 - version.length)) + '│');
219
+ console.log(' └────────────────────────────────────────┘');
222
220
  console.log('');
223
221
  console.log(chalk.dim(' First time? Let\'s get you set up in under a minute.'));
224
222
  console.log('');
@@ -61,6 +61,11 @@ export function createApi(app, daemon) {
61
61
  try {
62
62
  const config = validateAgentConfig(req.body);
63
63
  config.teamId = req.body.teamId || daemon.teams.getDefault()?.id || null;
64
+ // Inherit team working directory if agent doesn't specify one
65
+ if (!config.workingDir) {
66
+ const team = daemon.teams.get(config.teamId);
67
+ if (team?.workingDir) config.workingDir = team.workingDir;
68
+ }
64
69
  const agent = await daemon.processes.spawn(config);
65
70
  daemon.audit.log('agent.spawn', { id: agent.id, role: agent.role, provider: agent.provider });
66
71
  res.status(201).json(agent);
@@ -257,8 +262,8 @@ export function createApi(app, daemon) {
257
262
 
258
263
  app.post('/api/teams', (req, res) => {
259
264
  try {
260
- const team = daemon.teams.create(req.body.name);
261
- daemon.audit.log('team.create', { id: team.id, name: team.name });
265
+ const team = daemon.teams.create(req.body.name, req.body.workingDir);
266
+ daemon.audit.log('team.create', { id: team.id, name: team.name, workingDir: team.workingDir });
262
267
  res.status(201).json(team);
263
268
  } catch (err) {
264
269
  res.status(400).json({ error: err.message });
@@ -267,8 +272,10 @@ export function createApi(app, daemon) {
267
272
 
268
273
  app.patch('/api/teams/:id', (req, res) => {
269
274
  try {
270
- const team = daemon.teams.rename(req.params.id, req.body.name);
271
- daemon.audit.log('team.rename', { id: team.id, name: team.name });
275
+ if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
276
+ if (req.body.workingDir !== undefined) daemon.teams.setWorkingDir(req.params.id, req.body.workingDir);
277
+ const team = daemon.teams.get(req.params.id);
278
+ daemon.audit.log('team.update', { id: team.id, name: team.name, workingDir: team.workingDir });
272
279
  res.json(team);
273
280
  } catch (err) {
274
281
  res.status(400).json({ error: err.message });
@@ -384,6 +391,82 @@ export function createApi(app, daemon) {
384
391
  }
385
392
  });
386
393
 
394
+ // List MD files for an agent (from its working directory + .groove)
395
+ app.get('/api/agents/:id/mdfiles', (req, res) => {
396
+ const agent = daemon.registry.get(req.params.id);
397
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
398
+
399
+ const dir = agent.workingDir || daemon.projectDir;
400
+ const files = [];
401
+
402
+ // Scan working directory for .md files (top level + .groove/)
403
+ try {
404
+ for (const entry of readdirSync(dir)) {
405
+ if (entry.endsWith('.md') && !entry.startsWith('.')) {
406
+ const fullPath = resolve(dir, entry);
407
+ if (statSync(fullPath).isFile()) {
408
+ files.push({ name: entry, path: entry, size: statSync(fullPath).size });
409
+ }
410
+ }
411
+ }
412
+ const grooveDir = resolve(dir, '.groove');
413
+ if (existsSync(grooveDir)) {
414
+ for (const entry of readdirSync(grooveDir)) {
415
+ if (entry.endsWith('.md')) {
416
+ const fullPath = resolve(grooveDir, entry);
417
+ if (statSync(fullPath).isFile()) {
418
+ files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size });
419
+ }
420
+ }
421
+ }
422
+ }
423
+ } catch { /* dir might not exist */ }
424
+
425
+ res.json({ files, workingDir: dir });
426
+ });
427
+
428
+ // Read a specific MD file for an agent
429
+ app.get('/api/agents/:id/mdfiles/read', (req, res) => {
430
+ const agent = daemon.registry.get(req.params.id);
431
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
432
+
433
+ const dir = agent.workingDir || daemon.projectDir;
434
+ const relPath = req.query.path;
435
+ if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
436
+
437
+ const fullPath = resolve(dir, relPath);
438
+ if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
439
+
440
+ try {
441
+ const content = readFileSync(fullPath, 'utf8');
442
+ res.json({ path: relPath, content });
443
+ } catch {
444
+ res.status(404).json({ error: 'File not found' });
445
+ }
446
+ });
447
+
448
+ // Save a MD file for an agent
449
+ app.put('/api/agents/:id/mdfiles/write', (req, res) => {
450
+ const agent = daemon.registry.get(req.params.id);
451
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
452
+
453
+ const dir = agent.workingDir || daemon.projectDir;
454
+ const { path: relPath, content } = req.body;
455
+ if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
456
+ if (typeof content !== 'string') return res.status(400).json({ error: 'Content required' });
457
+
458
+ const fullPath = resolve(dir, relPath);
459
+ if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
460
+
461
+ try {
462
+ writeFileSync(fullPath, content, 'utf8');
463
+ daemon.audit.log('mdfile.write', { agentId: agent.id, path: relPath });
464
+ res.json({ ok: true });
465
+ } catch (err) {
466
+ res.status(500).json({ error: err.message });
467
+ }
468
+ });
469
+
387
470
  // Rotation stats
388
471
  app.get('/api/rotation', (req, res) => {
389
472
  res.json({
@@ -436,6 +436,7 @@ For normal file edits within your scope, proceed without review.
436
436
  for (const config of group.agents) {
437
437
  try {
438
438
  const validated = validateAgentConfig(config);
439
+ if (!validated.teamId) validated.teamId = this.daemon.teams.getDefault()?.id || null;
439
440
  this.spawn(validated).then((agent) => {
440
441
  this.daemon.broadcast({
441
442
  type: 'phase2:spawned',
@@ -1,11 +1,15 @@
1
1
  // GROOVE — Teams (Live Agent Groups)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync } from 'fs';
5
5
  import { resolve } from 'path';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { validateTeamName } from './validate.js';
8
8
 
9
+ function slugify(name) {
10
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64) || 'team';
11
+ }
12
+
9
13
  export class Teams {
10
14
  constructor(daemon) {
11
15
  this.daemon = daemon;
@@ -34,15 +38,33 @@ export class Teams {
34
38
  if (!hasDefault) {
35
39
  const id = randomUUID().slice(0, 8);
36
40
  const team = { id, name: 'Default', isDefault: true, createdAt: new Date().toISOString() };
41
+ // Default team uses the project directory (no subdirectory)
42
+ team.workingDir = this.daemon.projectDir;
37
43
  this.teams.set(id, team);
38
44
  this._save();
39
45
  }
40
46
  }
41
47
 
48
+ /**
49
+ * Create a team with an auto-managed working directory.
50
+ */
42
51
  create(name) {
43
52
  validateTeamName(name);
44
53
  const id = randomUUID().slice(0, 8);
45
- const team = { id, name, isDefault: false, createdAt: new Date().toISOString() };
54
+ const dirName = slugify(name);
55
+ const workingDir = resolve(this.daemon.projectDir, dirName);
56
+
57
+ // Create the directory
58
+ mkdirSync(workingDir, { recursive: true });
59
+
60
+ const team = {
61
+ id,
62
+ name,
63
+ isDefault: false,
64
+ workingDir,
65
+ createdAt: new Date().toISOString(),
66
+ };
67
+
46
68
  this.teams.set(id, team);
47
69
  this._save();
48
70
  this.daemon.broadcast({ type: 'team:created', team });
@@ -61,30 +83,80 @@ export class Teams {
61
83
  return [...this.teams.values()].find((t) => t.isDefault) || null;
62
84
  }
63
85
 
86
+ /**
87
+ * Rename a team — updates the directory name and all agent references.
88
+ */
64
89
  rename(id, name) {
65
90
  validateTeamName(name);
66
91
  const team = this.teams.get(id);
67
92
  if (!team) throw new Error('Team not found');
93
+
94
+ const oldName = team.name;
68
95
  team.name = name;
96
+
97
+ // Rename the directory if it was auto-managed (under projectDir)
98
+ if (team.workingDir && !team.isDefault) {
99
+ const newDirName = slugify(name);
100
+ const newWorkingDir = resolve(this.daemon.projectDir, newDirName);
101
+ const oldWorkingDir = team.workingDir;
102
+
103
+ if (oldWorkingDir !== newWorkingDir && existsSync(oldWorkingDir)) {
104
+ try {
105
+ renameSync(oldWorkingDir, newWorkingDir);
106
+ team.workingDir = newWorkingDir;
107
+
108
+ // Update all agents in this team with the new working directory
109
+ const agents = this.daemon.registry.getAll().filter((a) => a.teamId === id);
110
+ for (const agent of agents) {
111
+ if (agent.workingDir === oldWorkingDir) {
112
+ this.daemon.registry.update(agent.id, { workingDir: newWorkingDir });
113
+ }
114
+ }
115
+ } catch (err) {
116
+ console.log(`[Groove:Teams] Failed to rename directory: ${err.message}`);
117
+ // Keep old dir — name still updates
118
+ }
119
+ }
120
+ }
121
+
69
122
  this._save();
70
123
  this.daemon.broadcast({ type: 'team:updated', team });
71
124
  return team;
72
125
  }
73
126
 
127
+ /**
128
+ * Delete a team — removes directory and all contents, moves agents to default.
129
+ */
74
130
  delete(id) {
75
131
  const team = this.teams.get(id);
76
132
  if (!team) throw new Error('Team not found');
77
133
  if (team.isDefault) throw new Error('Cannot delete the default team');
78
134
 
79
- const defaultTeam = this.getDefault();
135
+ // Kill any running agents in this team
80
136
  const agents = this.daemon.registry.getAll().filter((a) => a.teamId === id);
81
137
  for (const agent of agents) {
82
- this.daemon.registry.update(agent.id, { teamId: defaultTeam.id });
138
+ if (agent.status === 'running' || agent.status === 'starting') {
139
+ try { this.daemon.processes.kill(agent.id); } catch { /* ignore */ }
140
+ }
141
+ }
142
+
143
+ // Remove agents from registry
144
+ for (const agent of agents) {
145
+ this.daemon.registry.remove(agent.id);
146
+ }
147
+
148
+ // Remove the working directory
149
+ if (team.workingDir && !team.isDefault && existsSync(team.workingDir)) {
150
+ try {
151
+ rmSync(team.workingDir, { recursive: true, force: true });
152
+ } catch (err) {
153
+ console.log(`[Groove:Teams] Failed to remove directory: ${err.message}`);
154
+ }
83
155
  }
84
156
 
85
157
  this.teams.delete(id);
86
158
  this._save();
87
- this.daemon.broadcast({ type: 'team:deleted', teamId: id, movedTo: defaultTeam.id });
159
+ this.daemon.broadcast({ type: 'team:deleted', teamId: id });
88
160
  return true;
89
161
  }
90
162
 
@@ -84,6 +84,7 @@ export function validateAgentConfig(config) {
84
84
  provider: config.provider || 'claude-code',
85
85
  model: typeof config.model === 'string' ? config.model : null,
86
86
  workingDir: typeof config.workingDir === 'string' ? config.workingDir : undefined,
87
+ teamId: config.teamId || undefined,
87
88
  permission,
88
89
  skills,
89
90
  integrations,
@@ -22,7 +22,9 @@ function createMockDaemon() {
22
22
  const daemon = {
23
23
  registry,
24
24
  grooveDir,
25
+ projectDir: tmpDir,
25
26
  broadcast(msg) { broadcasts.push(msg); },
27
+ processes: { kill() {} },
26
28
  };
27
29
 
28
30
  return { daemon, tmpDir, grooveDir, broadcasts };
@@ -114,25 +116,23 @@ describe('Teams', () => {
114
116
  assert.equal(teams.get(team.id), null);
115
117
  });
116
118
 
117
- it('should move agents to default on team delete', () => {
119
+ it('should remove agents on team delete', () => {
118
120
  const team = teams.create('Temp');
119
121
  const agent = daemon.registry.add({ role: 'backend', teamId: team.id });
120
122
  assert.equal(daemon.registry.get(agent.id).teamId, team.id);
121
123
 
122
124
  teams.delete(team.id);
123
125
 
124
- const defaultTeam = teams.getDefault();
125
- assert.equal(daemon.registry.get(agent.id).teamId, defaultTeam.id);
126
+ assert.equal(daemon.registry.get(agent.id), null);
126
127
  });
127
128
 
128
- it('should broadcast on delete with movedTo', () => {
129
+ it('should broadcast on delete', () => {
129
130
  const team = teams.create('Temp');
130
131
  broadcasts.length = 0;
131
132
  teams.delete(team.id);
132
133
  const msg = broadcasts.find((b) => b.type === 'team:deleted');
133
134
  assert.ok(msg);
134
135
  assert.equal(msg.teamId, team.id);
135
- assert.equal(msg.movedTo, teams.getDefault().id);
136
136
  });
137
137
 
138
138
  it('should throw when deleting the default team', () => {