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.
- package/node_modules/@groove-dev/cli/src/setup.js +7 -9
- package/node_modules/@groove-dev/daemon/src/api.js +87 -4
- package/node_modules/@groove-dev/daemon/src/process.js +1 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +77 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
- package/node_modules/@groove-dev/daemon/test/teams.test.js +5 -5
- package/node_modules/@groove-dev/gui/dist/assets/index-CqdQP7yG.js +587 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DvcNOnKP.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +139 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +16 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -38
- package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +28 -9
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +121 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +152 -34
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +4 -2
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -52
- package/package.json +1 -1
- package/packages/cli/src/setup.js +7 -9
- package/packages/daemon/src/api.js +87 -4
- package/packages/daemon/src/process.js +1 -0
- package/packages/daemon/src/teams.js +77 -5
- package/packages/daemon/src/validate.js +1 -0
- package/packages/gui/dist/assets/index-CqdQP7yG.js +587 -0
- package/packages/gui/dist/assets/index-DvcNOnKP.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +139 -0
- package/packages/gui/src/components/agents/agent-panel.jsx +4 -1
- package/packages/gui/src/components/dashboard/activity-feed.jsx +16 -14
- package/packages/gui/src/components/dashboard/cache-ring.jsx +105 -0
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -38
- package/packages/gui/src/components/dashboard/header-bar.jsx +28 -9
- package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
- package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
- package/packages/gui/src/components/dashboard/routing-chart.jsx +121 -0
- package/packages/gui/src/components/dashboard/token-chart.jsx +152 -34
- package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
- package/packages/gui/src/lib/theme-hex.js +7 -0
- package/packages/gui/src/stores/groove.js +4 -2
- package/packages/gui/src/views/dashboard.jsx +97 -52
- package/node_modules/@groove-dev/gui/dist/assets/index-CL4GvVoL.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-D_tSBDCx.js +0 -577
- package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
- package/packages/gui/dist/assets/index-CL4GvVoL.css +0 -1
- package/packages/gui/dist/assets/index-D_tSBDCx.js +0 -577
- package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { useRef, useState, useEffect } from 'react';
|
|
3
3
|
import { useDashboard } from '../lib/hooks/use-dashboard';
|
|
4
4
|
import { DashboardHeader } from '../components/dashboard/header-bar';
|
|
5
|
-
import {
|
|
5
|
+
import { KpiStrip } from '../components/dashboard/kpi-card';
|
|
6
6
|
import { FleetPanel } from '../components/dashboard/fleet-panel';
|
|
7
|
-
import { SavingsPanel } from '../components/dashboard/savings-panel';
|
|
8
7
|
import { TokenChart } from '../components/dashboard/token-chart';
|
|
8
|
+
import { CacheRing } from '../components/dashboard/cache-ring';
|
|
9
|
+
import { RoutingChart } from '../components/dashboard/routing-chart';
|
|
10
|
+
import { IntelPanel } from '../components/dashboard/intel-panel';
|
|
9
11
|
import { ActivityFeed } from '../components/dashboard/activity-feed';
|
|
10
12
|
import { Skeleton } from '../components/ui/skeleton';
|
|
11
13
|
import { HEX } from '../lib/theme-hex';
|
|
@@ -14,45 +16,52 @@ import { BarChart3 } from 'lucide-react';
|
|
|
14
16
|
|
|
15
17
|
function DashboardSkeleton() {
|
|
16
18
|
return (
|
|
17
|
-
<div className="flex-1
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<div className="
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
<div className="flex-1 grid gap-px p-0" style={{
|
|
20
|
+
gridTemplateRows: 'auto minmax(0, 1fr) minmax(0, 1fr)',
|
|
21
|
+
gridTemplateColumns: '3fr 1.5fr 1.5fr',
|
|
22
|
+
background: '#1a1e25',
|
|
23
|
+
}}>
|
|
24
|
+
{/* KPI row */}
|
|
25
|
+
<div className="col-span-3"><Skeleton className="h-[72px] rounded-none" /></div>
|
|
26
|
+
{/* Chart row */}
|
|
27
|
+
<Skeleton className="rounded-none" />
|
|
28
|
+
<Skeleton className="rounded-none" />
|
|
29
|
+
<Skeleton className="rounded-none" />
|
|
30
|
+
{/* Intel row */}
|
|
31
|
+
<Skeleton className="rounded-none" />
|
|
32
|
+
<div className="col-span-2"><Skeleton className="h-full rounded-none" /></div>
|
|
28
33
|
</div>
|
|
29
34
|
);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export default function DashboardView() {
|
|
33
|
-
const {
|
|
34
|
-
|
|
38
|
+
const {
|
|
39
|
+
data, loading, agents, connected, kpiHistory, lastFetch,
|
|
40
|
+
agentBreakdown, routing, rotation, adaptive, journalist, rotating,
|
|
41
|
+
} = useDashboard();
|
|
42
|
+
|
|
43
|
+
const chartRef = useRef(null);
|
|
35
44
|
const [chartSize, setChartSize] = useState({ width: 400, height: 200 });
|
|
36
45
|
|
|
37
46
|
const runningCount = agents.filter((a) => a.status === 'running').length;
|
|
38
47
|
|
|
39
|
-
// Measure chart container
|
|
48
|
+
// Measure token chart container
|
|
40
49
|
useEffect(() => {
|
|
41
|
-
if (!
|
|
50
|
+
if (!chartRef.current) return;
|
|
42
51
|
const observer = new ResizeObserver((entries) => {
|
|
43
52
|
const { width, height } = entries[0].contentRect;
|
|
44
53
|
setChartSize({ width: Math.floor(width), height: Math.floor(height) });
|
|
45
54
|
});
|
|
46
|
-
observer.observe(
|
|
55
|
+
observer.observe(chartRef.current);
|
|
47
56
|
return () => observer.disconnect();
|
|
48
57
|
}, []);
|
|
49
58
|
|
|
50
59
|
if (!connected) {
|
|
51
60
|
return (
|
|
52
61
|
<div className="w-full h-full flex items-center justify-center">
|
|
53
|
-
<div className="text-center space-y-2 text-
|
|
54
|
-
<BarChart3 size={
|
|
55
|
-
<p className="text-
|
|
62
|
+
<div className="text-center space-y-2 text-[#3a3f4b] font-mono">
|
|
63
|
+
<BarChart3 size={28} className="mx-auto" />
|
|
64
|
+
<p className="text-[10px]">Connecting to daemon...</p>
|
|
56
65
|
</div>
|
|
57
66
|
</div>
|
|
58
67
|
);
|
|
@@ -68,24 +77,38 @@ export default function DashboardView() {
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
const rawTokens = data.tokens || {};
|
|
71
|
-
// Normalize field names from API to what the dashboard expects
|
|
72
80
|
const tokens = {
|
|
73
|
-
|
|
81
|
+
totalTokens: rawTokens.totalTokens || 0,
|
|
74
82
|
totalCostUsd: rawTokens.totalCostUsd || 0,
|
|
75
|
-
totalSaved: rawTokens.savings?.total || 0,
|
|
76
|
-
cacheHitRate: rawTokens.cacheHitRate || 0,
|
|
77
83
|
totalInputTokens: rawTokens.totalInputTokens || 0,
|
|
78
84
|
totalOutputTokens: rawTokens.totalOutputTokens || 0,
|
|
79
85
|
cacheReadTokens: rawTokens.cacheReadTokens || 0,
|
|
80
86
|
cacheCreationTokens: rawTokens.cacheCreationTokens || 0,
|
|
87
|
+
cacheHitRate: rawTokens.cacheHitRate || 0,
|
|
81
88
|
totalTurns: rawTokens.totalTurns || 0,
|
|
82
89
|
agentCount: rawTokens.agentCount || 0,
|
|
83
90
|
savings: rawTokens.savings || {},
|
|
84
|
-
perAgent: rawTokens.perAgent || [],
|
|
85
91
|
};
|
|
86
|
-
|
|
87
|
-
const totalHypothetical = tokens.
|
|
88
|
-
const efficiency = totalHypothetical > 0 ? (tokens.
|
|
92
|
+
|
|
93
|
+
const totalHypothetical = tokens.totalTokens + (tokens.savings.total || 0);
|
|
94
|
+
const efficiency = totalHypothetical > 0 ? ((tokens.savings.total || 0) / totalHypothetical) * 100 : 0;
|
|
95
|
+
const ioRatio = tokens.totalOutputTokens > 0 ? (tokens.totalInputTokens / tokens.totalOutputTokens).toFixed(1) : '—';
|
|
96
|
+
|
|
97
|
+
const timeline = data.timeline || {};
|
|
98
|
+
const snapshots = timeline.snapshots || [];
|
|
99
|
+
const events = timeline.events || data.events || [];
|
|
100
|
+
|
|
101
|
+
// Build KPI definitions
|
|
102
|
+
const kpis = [
|
|
103
|
+
{ label: 'Tokens Used', value: fmtNum(tokens.totalTokens), sparkData: kpiHistory.tokens, color: HEX.accent },
|
|
104
|
+
{ label: 'Total Cost', value: fmtDollar(tokens.totalCostUsd), sparkData: kpiHistory.cost, color: HEX.warning },
|
|
105
|
+
{ label: 'Tokens Saved', value: fmtNum(tokens.savings.total || 0), sparkData: kpiHistory.saved, color: HEX.success },
|
|
106
|
+
{ label: 'Efficiency', value: fmtPct(efficiency), sparkData: kpiHistory.efficiency, color: HEX.purple },
|
|
107
|
+
{ label: 'Cache Rate', value: fmtPct(tokens.cacheHitRate * 100), sparkData: kpiHistory.cache, color: HEX.info },
|
|
108
|
+
{ label: 'I/O Ratio', value: `${ioRatio}:1`, sparkData: kpiHistory.inputOutput, color: HEX.orange },
|
|
109
|
+
{ label: 'Agents', value: `${runningCount}/${agents.length}`, sparkData: kpiHistory.agents, color: HEX.accent },
|
|
110
|
+
{ label: 'Turns', value: fmtNum(tokens.totalTurns), sparkData: kpiHistory.turns, color: HEX.text2 },
|
|
111
|
+
];
|
|
89
112
|
|
|
90
113
|
return (
|
|
91
114
|
<div className="flex flex-col h-full">
|
|
@@ -96,46 +119,68 @@ export default function DashboardView() {
|
|
|
96
119
|
totalCount={agents.length}
|
|
97
120
|
uptime={data.uptime || 0}
|
|
98
121
|
lastFetch={lastFetch}
|
|
122
|
+
activeTeam={data.activeTeam}
|
|
99
123
|
/>
|
|
100
124
|
|
|
101
125
|
{/* KPI Strip */}
|
|
102
|
-
<
|
|
103
|
-
<KpiCard label="Tokens Used" value={fmtNum(tokens.totalUsed)} sparkData={kpiHistory.tokens} color={HEX.accent} className="flex-1" />
|
|
104
|
-
<KpiCard label="Total Cost" value={fmtDollar(tokens.totalCostUsd)} sparkData={kpiHistory.cost} color={HEX.warning} className="flex-1 border-l border-border-subtle" />
|
|
105
|
-
<KpiCard label="Tokens Saved" value={fmtNum(tokens.totalSaved)} sparkData={kpiHistory.saved} color={HEX.success} className="flex-1 border-l border-border-subtle" />
|
|
106
|
-
<KpiCard label="Efficiency" value={fmtPct(efficiency)} sparkData={kpiHistory.efficiency} color={HEX.purple} className="flex-1 border-l border-border-subtle" />
|
|
107
|
-
<KpiCard label="Cache Rate" value={fmtPct(tokens.cacheHitRate)} sparkData={kpiHistory.cache} color={HEX.info} className="flex-1 border-l border-border-subtle" />
|
|
108
|
-
</div>
|
|
126
|
+
<KpiStrip kpis={kpis} />
|
|
109
127
|
|
|
110
128
|
{/* Main grid */}
|
|
111
|
-
<div className="flex-1
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
<div className="flex-1 min-h-0 grid" style={{
|
|
130
|
+
gridTemplateRows: 'minmax(0, 1fr) minmax(0, 1fr)',
|
|
131
|
+
gridTemplateColumns: '3fr 1.5fr 1.5fr',
|
|
132
|
+
background: '#1a1e25',
|
|
133
|
+
gap: '1px',
|
|
134
|
+
}}>
|
|
135
|
+
{/* R3C1: Token Flow Chart */}
|
|
136
|
+
<div ref={chartRef} className="min-w-0 min-h-0 bg-[#1e2127]">
|
|
114
137
|
{chartSize.width > 0 && (
|
|
115
|
-
<TokenChart data={
|
|
138
|
+
<TokenChart data={snapshots} width={chartSize.width} height={chartSize.height} />
|
|
116
139
|
)}
|
|
117
140
|
</div>
|
|
118
141
|
|
|
119
|
-
{/*
|
|
120
|
-
<div className="
|
|
121
|
-
<div className="px-3
|
|
122
|
-
<span className="text-
|
|
142
|
+
{/* R3C2: Cache Ring */}
|
|
143
|
+
<div className="min-w-0 min-h-0 bg-[#1e2127] flex flex-col">
|
|
144
|
+
<div className="px-3 pt-2 pb-1">
|
|
145
|
+
<span className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest">Cache Performance</span>
|
|
123
146
|
</div>
|
|
124
|
-
<
|
|
147
|
+
<CacheRing
|
|
148
|
+
cacheRead={tokens.cacheReadTokens}
|
|
149
|
+
cacheCreation={tokens.cacheCreationTokens}
|
|
150
|
+
totalInput={tokens.totalInputTokens}
|
|
151
|
+
/>
|
|
125
152
|
</div>
|
|
126
153
|
|
|
127
|
-
{/*
|
|
128
|
-
<div className="
|
|
129
|
-
<div className="px-3
|
|
130
|
-
<span className="text-
|
|
154
|
+
{/* R3C3: Routing Chart */}
|
|
155
|
+
<div className="min-w-0 min-h-0 bg-[#1e2127] flex flex-col">
|
|
156
|
+
<div className="px-3 pt-2 pb-1">
|
|
157
|
+
<span className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest">Model Routing</span>
|
|
131
158
|
</div>
|
|
132
|
-
<
|
|
159
|
+
<RoutingChart routing={routing} />
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* R4C1: Agent Fleet */}
|
|
163
|
+
<div className="min-w-0 min-h-0 bg-[#1e2127] flex flex-col">
|
|
164
|
+
<div className="px-3 pt-2 pb-1 flex-shrink-0">
|
|
165
|
+
<span className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest">Agent Fleet</span>
|
|
166
|
+
</div>
|
|
167
|
+
<FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} />
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* R4C2-3: Intel Panel (spans 2 cols) */}
|
|
171
|
+
<div className="col-span-2 min-w-0 min-h-0 bg-[#1e2127] flex flex-col">
|
|
172
|
+
<IntelPanel
|
|
173
|
+
tokens={tokens}
|
|
174
|
+
rotation={rotation}
|
|
175
|
+
adaptive={adaptive}
|
|
176
|
+
journalist={journalist}
|
|
177
|
+
/>
|
|
133
178
|
</div>
|
|
134
179
|
</div>
|
|
135
180
|
|
|
136
181
|
{/* Activity feed */}
|
|
137
|
-
<div className="flex-shrink-0 border-t border-
|
|
138
|
-
<ActivityFeed events={
|
|
182
|
+
<div className="flex-shrink-0 bg-[#1e2127] border-t border-[#262a32]">
|
|
183
|
+
<ActivityFeed events={events} />
|
|
139
184
|
</div>
|
|
140
185
|
</div>
|
|
141
186
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -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(
|
|
216
|
-
console.log(
|
|
217
|
-
console.log(
|
|
218
|
-
console.log(
|
|
219
|
-
console.log(
|
|
220
|
-
console.log(
|
|
221
|
-
console.log(
|
|
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
|
-
|
|
271
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|