groove-dev 0.27.117 → 0.27.119
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/TRAINING_DATA_v4.md +6 -6
- package/moe-training/client/domain-tagger.js +3 -3
- package/moe-training/client/trajectory-capture.js +7 -0
- package/moe-training/client/transmission-queue.js +6 -0
- package/moe-training/test/shared/envelope-schema.test.js +3 -3
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +13 -4
- package/node_modules/@groove-dev/daemon/src/index.js +4 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +70 -39
- package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
- package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +29 -7
- package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +7 -3
- package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +29 -5
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +4 -0
- package/node_modules/moe-training/client/domain-tagger.js +3 -3
- package/node_modules/moe-training/client/trajectory-capture.js +7 -0
- package/node_modules/moe-training/client/transmission-queue.js +6 -0
- package/node_modules/moe-training/test/shared/envelope-schema.test.js +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +13 -4
- package/packages/daemon/src/index.js +4 -0
- package/packages/daemon/src/teams.js +70 -39
- package/packages/daemon/src/validate.js +10 -0
- package/packages/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
- package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
- package/packages/gui/src/components/editor/file-tree.jsx +80 -3
- package/packages/gui/src/components/settings/quick-connect.jsx +29 -7
- package/packages/gui/src/components/teams/team-removal-dialog.jsx +7 -3
- package/packages/gui/src/components/ui/data-sharing-modal.jsx +151 -0
- package/packages/gui/src/stores/groove.js +29 -5
- package/packages/gui/src/views/agents.jsx +47 -11
- package/packages/gui/src/views/teams.jsx +4 -0
|
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
|
|
|
10
10
|
import { cn } from '../lib/cn';
|
|
11
11
|
import { Button } from '../components/ui/button';
|
|
12
12
|
import { Badge } from '../components/ui/badge';
|
|
13
|
-
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, Archive } from 'lucide-react';
|
|
13
|
+
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, Archive, Box, HardDrive } from 'lucide-react';
|
|
14
14
|
import { PreviewWorkspace } from '../components/preview/preview-workspace';
|
|
15
15
|
import { WorkspaceMode } from '../components/agents/workspace-mode';
|
|
16
16
|
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
|
|
@@ -313,6 +313,7 @@ export function TeamTabBar() {
|
|
|
313
313
|
onOpenChange={(open) => !open && setArchiveConfirm(null)}
|
|
314
314
|
onArchive={archiveTeam}
|
|
315
315
|
onDeletePermanently={deleteTeamPermanently}
|
|
316
|
+
mode={archiveConfirm?.mode || 'sandbox'}
|
|
316
317
|
/>
|
|
317
318
|
</div>
|
|
318
319
|
);
|
|
@@ -1244,6 +1245,7 @@ function RecommendedTeamCard() {
|
|
|
1244
1245
|
const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
|
|
1245
1246
|
const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
|
|
1246
1247
|
const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
|
|
1248
|
+
const [tsMode, setTsMode] = useState(teamLaunchConfig?.mode || 'sandbox');
|
|
1247
1249
|
|
|
1248
1250
|
useEffect(() => {
|
|
1249
1251
|
fetchProviders().then((list) => {
|
|
@@ -1278,15 +1280,14 @@ function RecommendedTeamCard() {
|
|
|
1278
1280
|
async function handleLaunch() {
|
|
1279
1281
|
setLaunching(true);
|
|
1280
1282
|
// Save overrides to store so launchRecommendedTeam sends them
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1283
|
+
useGrooveStore.setState({
|
|
1284
|
+
teamLaunchConfig: {
|
|
1285
|
+
...(tsProvider && { provider: tsProvider, model: tsModel }),
|
|
1286
|
+
reasoningEffort: tsReasoning,
|
|
1287
|
+
...(showTemp && { temperature: tsTemp }),
|
|
1288
|
+
mode: tsMode,
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1290
1291
|
try {
|
|
1291
1292
|
const modified = [...agentEdits, ...phase2];
|
|
1292
1293
|
await launchRecommendedTeam(modified);
|
|
@@ -1362,6 +1363,41 @@ function RecommendedTeamCard() {
|
|
|
1362
1363
|
formatValue={(v) => v.toFixed(2)}
|
|
1363
1364
|
/>
|
|
1364
1365
|
)}
|
|
1366
|
+
{/* Build Mode */}
|
|
1367
|
+
<div className="space-y-1">
|
|
1368
|
+
<label className="text-2xs text-text-3 font-sans">Build Mode</label>
|
|
1369
|
+
<div className="flex rounded-md bg-surface-4 border border-border-subtle p-0.5">
|
|
1370
|
+
<button
|
|
1371
|
+
onClick={() => setTsMode('sandbox')}
|
|
1372
|
+
className={cn(
|
|
1373
|
+
'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
|
|
1374
|
+
tsMode === 'sandbox'
|
|
1375
|
+
? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
|
|
1376
|
+
: 'text-text-3 hover:text-text-1',
|
|
1377
|
+
)}
|
|
1378
|
+
>
|
|
1379
|
+
<Box size={11} />
|
|
1380
|
+
Sandbox
|
|
1381
|
+
</button>
|
|
1382
|
+
<button
|
|
1383
|
+
onClick={() => setTsMode('production')}
|
|
1384
|
+
className={cn(
|
|
1385
|
+
'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
|
|
1386
|
+
tsMode === 'production'
|
|
1387
|
+
? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
|
|
1388
|
+
: 'text-text-3 hover:text-text-1',
|
|
1389
|
+
)}
|
|
1390
|
+
>
|
|
1391
|
+
<HardDrive size={11} />
|
|
1392
|
+
Production
|
|
1393
|
+
</button>
|
|
1394
|
+
</div>
|
|
1395
|
+
<p className="text-2xs text-text-4 font-sans">
|
|
1396
|
+
{tsMode === 'sandbox'
|
|
1397
|
+
? 'Files live in a team directory, removable with the team'
|
|
1398
|
+
: 'Files live in the project directory, persist forever'}
|
|
1399
|
+
</p>
|
|
1400
|
+
</div>
|
|
1365
1401
|
</div>
|
|
1366
1402
|
)}
|
|
1367
1403
|
</div>
|
|
@@ -1394,7 +1430,7 @@ function RecommendedTeamCard() {
|
|
|
1394
1430
|
);
|
|
1395
1431
|
})}
|
|
1396
1432
|
|
|
1397
|
-
{recommendedTeam.projectDir && (
|
|
1433
|
+
{recommendedTeam.projectDir && tsMode === 'sandbox' && (
|
|
1398
1434
|
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
|
|
1399
1435
|
<span className="text-text-4">Project:</span>
|
|
1400
1436
|
<span className="text-accent">{recommendedTeam.projectDir}/</span>
|
|
@@ -73,6 +73,9 @@ function TeamsDashboard() {
|
|
|
73
73
|
<div className="flex items-center gap-2">
|
|
74
74
|
<span className="text-sm font-semibold text-text-0 font-sans">{team.name}</span>
|
|
75
75
|
{isActive && <Badge variant="accent" className="text-2xs">Active</Badge>}
|
|
76
|
+
<Badge variant={team.mode === 'production' ? 'success' : 'default'} className="text-2xs">
|
|
77
|
+
{team.mode === 'production' ? 'Production' : 'Sandbox'}
|
|
78
|
+
</Badge>
|
|
76
79
|
</div>
|
|
77
80
|
{team.workingDir && (
|
|
78
81
|
<div className="flex items-center gap-1 mt-0.5">
|
|
@@ -183,6 +186,7 @@ function TeamsDashboard() {
|
|
|
183
186
|
onOpenChange={(open) => !open && setArchiveConfirm(null)}
|
|
184
187
|
onArchive={archiveTeam}
|
|
185
188
|
onDeletePermanently={deleteTeamPermanently}
|
|
189
|
+
mode={archiveConfirm?.mode || 'sandbox'}
|
|
186
190
|
/>
|
|
187
191
|
|
|
188
192
|
<PurgeConfirmDialog
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';
|
|
4
4
|
|
|
5
|
-
const DEFAULT_MODEL = '
|
|
5
|
+
const DEFAULT_MODEL = 'BAAI/bge-small-en-v1.5';
|
|
6
6
|
const DEFAULT_TOP_K = 3;
|
|
7
7
|
|
|
8
8
|
// ~40 domains covering broad technical territory.
|
|
@@ -170,8 +170,8 @@ const DOMAIN_TAXONOMY = {
|
|
|
170
170
|
description: 'Scientific computing, numerical methods, MATLAB/SciPy/Julia, simulations, optimization, statistics',
|
|
171
171
|
},
|
|
172
172
|
planning_strategy: {
|
|
173
|
-
keywords: ['plan', 'strategy', 'architect', 'design doc', 'breakdown', 'scope', 'roadmap', 'milestone', 'prioritize', 'tradeoff', 'approach', 'recommend', 'team', 'coordinate', 'delegate', 'assign'],
|
|
174
|
-
description: 'Project planning, task
|
|
173
|
+
keywords: ['plan', 'strategy', 'architect', 'design doc', 'breakdown', 'scope', 'roadmap', 'milestone', 'prioritize', 'tradeoff', 'approach', 'recommend', 'team', 'coordinate', 'delegate', 'assign', 'orchestration', 'dispatch', 'routing', 'decompose', 'pipeline', 'inference', 'layer'],
|
|
174
|
+
description: 'Project planning, task decomposition, architecture design, system orchestration, routing and dispatch design, inference pipelines, team coordination, strategy, roadmaps, scoping, prioritization, tradeoff analysis',
|
|
175
175
|
},
|
|
176
176
|
conversational_reasoning: {
|
|
177
177
|
keywords: ['explain', 'why', 'how does', 'what is', 'clarify', 'understand', 'reason', 'think through', 'analyze', 'compare', 'evaluate', 'brainstorm', 'discuss', 'opinion', 'advice'],
|
|
@@ -34,6 +34,12 @@ export class TrajectoryCapture {
|
|
|
34
34
|
this._offlineRetryTimer = null;
|
|
35
35
|
this._contexts = new Map();
|
|
36
36
|
this._shutdown = false;
|
|
37
|
+
this._onEnvelopeSent = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set onEnvelopeSent(fn) {
|
|
41
|
+
this._onEnvelopeSent = typeof fn === 'function' ? fn : null;
|
|
42
|
+
if (this._transmissionQueue) this._transmissionQueue.onSent = this._onEnvelopeSent;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
async init() {
|
|
@@ -46,6 +52,7 @@ export class TrajectoryCapture {
|
|
|
46
52
|
this._scrubber = new PIIScrubber();
|
|
47
53
|
this._attestation = new SessionAttestation(this._centralCommandUrl);
|
|
48
54
|
this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);
|
|
55
|
+
if (this._onEnvelopeSent) this._transmissionQueue.onSent = this._onEnvelopeSent;
|
|
49
56
|
this._transmissionQueue.start();
|
|
50
57
|
this._domainTagger = new DomainTagger();
|
|
51
58
|
await this._domainTagger.init();
|
|
@@ -10,6 +10,7 @@ export class TransmissionQueue {
|
|
|
10
10
|
this._offlineQueue = [];
|
|
11
11
|
this._running = false;
|
|
12
12
|
this._drainPromise = null;
|
|
13
|
+
this._onSent = null;
|
|
13
14
|
|
|
14
15
|
if (process.env.NODE_ENV === 'production' && !centralCommandUrl.startsWith('https://')) {
|
|
15
16
|
console.warn('[TransmissionQueue] WARNING: centralCommandUrl does not use HTTPS in production');
|
|
@@ -20,6 +21,10 @@ export class TransmissionQueue {
|
|
|
20
21
|
return this._offlineQueue.length;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
set onSent(fn) {
|
|
25
|
+
this._onSent = typeof fn === 'function' ? fn : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
enqueue(signedEnvelope) {
|
|
24
29
|
if (this._queue.length >= this._maxSize) return;
|
|
25
30
|
if (signedEnvelope?.attestation?.session_hmac === 'OFFLINE') {
|
|
@@ -86,6 +91,7 @@ export class TransmissionQueue {
|
|
|
86
91
|
});
|
|
87
92
|
if (res.ok) {
|
|
88
93
|
success = true;
|
|
94
|
+
if (this._onSent) this._onSent(envelope);
|
|
89
95
|
break;
|
|
90
96
|
}
|
|
91
97
|
} catch {
|
|
@@ -475,7 +475,7 @@ describe('envelope-schema', () => {
|
|
|
475
475
|
it('accepts valid session_embedding object', () => {
|
|
476
476
|
const env = validEnvelope();
|
|
477
477
|
env.metadata.session_embedding = {
|
|
478
|
-
model: '
|
|
478
|
+
model: 'BAAI/bge-small-en-v1.5',
|
|
479
479
|
vector: [0.0234, -0.0891, 0.1247, 0.0562],
|
|
480
480
|
source_text: 'Write a Python decorator that caches function results',
|
|
481
481
|
};
|
|
@@ -486,7 +486,7 @@ describe('envelope-schema', () => {
|
|
|
486
486
|
it('rejects session_embedding with empty vector', () => {
|
|
487
487
|
const env = validEnvelope();
|
|
488
488
|
env.metadata.session_embedding = {
|
|
489
|
-
model: '
|
|
489
|
+
model: 'BAAI/bge-small-en-v1.5',
|
|
490
490
|
vector: [],
|
|
491
491
|
source_text: 'test',
|
|
492
492
|
};
|
|
@@ -498,7 +498,7 @@ describe('envelope-schema', () => {
|
|
|
498
498
|
it('rejects session_embedding with non-numeric vector values', () => {
|
|
499
499
|
const env = validEnvelope();
|
|
500
500
|
env.metadata.session_embedding = {
|
|
501
|
-
model: '
|
|
501
|
+
model: 'BAAI/bge-small-en-v1.5',
|
|
502
502
|
vector: [0.1, 'bad', 0.3],
|
|
503
503
|
source_text: 'test',
|
|
504
504
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.119",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, 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, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -16,7 +16,7 @@ import { OllamaProvider } from './providers/ollama.js';
|
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
18
18
|
import { ConsentManager } from '../../../moe-training/client/index.js';
|
|
19
|
-
import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from './validate.js';
|
|
19
|
+
import { validateAgentConfig, validateReasoningEffort, validateVerbosity, validateTeamMode } from './validate.js';
|
|
20
20
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
|
|
21
21
|
|
|
22
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1092,8 +1092,8 @@ export function createApi(app, daemon) {
|
|
|
1092
1092
|
|
|
1093
1093
|
app.post('/api/teams', (req, res) => {
|
|
1094
1094
|
try {
|
|
1095
|
-
const team = daemon.teams.create(req.body.name, req.body.
|
|
1096
|
-
daemon.audit.log('team.create', { id: team.id, name: team.name, workingDir: team.workingDir });
|
|
1095
|
+
const team = daemon.teams.create(req.body.name, { mode: req.body.mode });
|
|
1096
|
+
daemon.audit.log('team.create', { id: team.id, name: team.name, mode: team.mode, workingDir: team.workingDir });
|
|
1097
1097
|
res.status(201).json(team);
|
|
1098
1098
|
} catch (err) {
|
|
1099
1099
|
res.status(400).json({ error: err.message });
|
|
@@ -3447,9 +3447,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3447
3447
|
}
|
|
3448
3448
|
const defaultTeamId = launchTeamId || daemon.teams.getDefault()?.id || null;
|
|
3449
3449
|
|
|
3450
|
+
// Determine team build mode
|
|
3451
|
+
let launchMode;
|
|
3452
|
+
try { launchMode = validateTeamMode(req.body?.mode || raw.mode); } catch { launchMode = 'sandbox'; }
|
|
3453
|
+
|
|
3450
3454
|
// If planner specified a project directory, create it and use it as workingDir
|
|
3455
|
+
// Production mode: always use projectDir directly, skip subdirectory creation
|
|
3451
3456
|
let projectWorkingDir = baseDir;
|
|
3452
|
-
if (
|
|
3457
|
+
if (launchMode === 'production') {
|
|
3458
|
+
projectWorkingDir = daemon.projectDir;
|
|
3459
|
+
console.log(`[Groove] Production mode — working in project root: ${projectWorkingDir}`);
|
|
3460
|
+
} else if (projectDir) {
|
|
3453
3461
|
// Sanitize: kebab-case, no path traversal
|
|
3454
3462
|
const safeName = String(projectDir).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
|
|
3455
3463
|
projectWorkingDir = resolve(baseDir, safeName);
|
|
@@ -4990,6 +4998,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4990
4998
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
4991
4999
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
4992
5000
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
5001
|
+
'dataSharingDismissed',
|
|
4993
5002
|
];
|
|
4994
5003
|
for (const key of Object.keys(req.body)) {
|
|
4995
5004
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -686,6 +686,10 @@ export class Daemon {
|
|
|
686
686
|
centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
|
|
687
687
|
grooveVersion: version,
|
|
688
688
|
});
|
|
689
|
+
this.trajectoryCapture.onEnvelopeSent = () => {
|
|
690
|
+
const count = (this.state.get('training_envelopes_sent') || 0) + 1;
|
|
691
|
+
this.state.set('training_envelopes_sent', count);
|
|
692
|
+
};
|
|
689
693
|
this.trajectoryCapture.init();
|
|
690
694
|
} catch (e) {
|
|
691
695
|
// Training capture is never critical
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
|
|
5
|
-
import { resolve
|
|
5
|
+
import { resolve } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
|
-
import { validateTeamName } from './validate.js';
|
|
7
|
+
import { validateTeamName, validateTeamMode } from './validate.js';
|
|
8
8
|
|
|
9
9
|
function slugify(name) {
|
|
10
10
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64) || 'team';
|
|
@@ -44,6 +44,7 @@ export class Teams {
|
|
|
44
44
|
id,
|
|
45
45
|
name: 'Default',
|
|
46
46
|
isDefault: true,
|
|
47
|
+
mode: 'sandbox',
|
|
47
48
|
workingDir: defaultDir,
|
|
48
49
|
createdAt: new Date().toISOString(),
|
|
49
50
|
};
|
|
@@ -64,19 +65,25 @@ export class Teams {
|
|
|
64
65
|
/**
|
|
65
66
|
* Create a team with an auto-managed working directory.
|
|
66
67
|
*/
|
|
67
|
-
create(name) {
|
|
68
|
+
create(name, { mode = 'sandbox' } = {}) {
|
|
68
69
|
validateTeamName(name);
|
|
70
|
+
mode = validateTeamMode(mode);
|
|
69
71
|
const id = randomUUID().slice(0, 8);
|
|
70
|
-
const dirName = slugify(name);
|
|
71
|
-
const workingDir = resolve(this.daemon.projectDir, dirName);
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
let workingDir;
|
|
74
|
+
if (mode === 'production') {
|
|
75
|
+
workingDir = this.daemon.projectDir;
|
|
76
|
+
} else {
|
|
77
|
+
const dirName = slugify(name);
|
|
78
|
+
workingDir = resolve(this.daemon.projectDir, dirName);
|
|
79
|
+
mkdirSync(workingDir, { recursive: true });
|
|
80
|
+
}
|
|
75
81
|
|
|
76
82
|
const team = {
|
|
77
83
|
id,
|
|
78
84
|
name,
|
|
79
85
|
isDefault: false,
|
|
86
|
+
mode,
|
|
80
87
|
workingDir,
|
|
81
88
|
createdAt: new Date().toISOString(),
|
|
82
89
|
};
|
|
@@ -110,8 +117,9 @@ export class Teams {
|
|
|
110
117
|
const oldName = team.name;
|
|
111
118
|
team.name = name;
|
|
112
119
|
|
|
120
|
+
// Production teams use the project root — never rename directories
|
|
113
121
|
// Rename the directory if it was auto-managed (under projectDir)
|
|
114
|
-
if (team.workingDir && !team.isDefault) {
|
|
122
|
+
if (team.workingDir && !team.isDefault && team.mode !== 'production') {
|
|
115
123
|
const newDirName = slugify(name);
|
|
116
124
|
const newWorkingDir = resolve(this.daemon.projectDir, newDirName);
|
|
117
125
|
const oldWorkingDir = team.workingDir;
|
|
@@ -150,18 +158,30 @@ export class Teams {
|
|
|
150
158
|
|
|
151
159
|
const agents = this._killAndRemoveAgents(id);
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
161
|
+
try {
|
|
162
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
163
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
164
|
+
const slug = slugify(team.name);
|
|
165
|
+
const archiveName = `${slug}-${Date.now()}`;
|
|
166
|
+
const archivePath = resolve(archiveDir, archiveName);
|
|
167
|
+
|
|
168
|
+
if (team.mode === 'production') {
|
|
169
|
+
// Production teams: metadata-only archive (no directory move)
|
|
170
|
+
mkdirSync(archivePath, { recursive: true });
|
|
171
|
+
const metadata = {
|
|
172
|
+
originalName: team.name,
|
|
173
|
+
originalId: team.id,
|
|
174
|
+
mode: team.mode,
|
|
175
|
+
deletedAt: new Date().toISOString(),
|
|
176
|
+
agentCount: agents.length,
|
|
177
|
+
originalWorkingDir: team.workingDir,
|
|
178
|
+
};
|
|
179
|
+
writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
180
|
+
} else if (
|
|
181
|
+
team.workingDir &&
|
|
182
|
+
team.workingDir !== this.daemon.projectDir &&
|
|
183
|
+
existsSync(team.workingDir)
|
|
184
|
+
) {
|
|
165
185
|
try {
|
|
166
186
|
renameSync(team.workingDir, archivePath);
|
|
167
187
|
} catch (err) {
|
|
@@ -176,14 +196,15 @@ export class Teams {
|
|
|
176
196
|
const metadata = {
|
|
177
197
|
originalName: team.name,
|
|
178
198
|
originalId: team.id,
|
|
199
|
+
mode: team.mode || 'sandbox',
|
|
179
200
|
deletedAt: new Date().toISOString(),
|
|
180
201
|
agentCount: agents.length,
|
|
181
202
|
originalWorkingDir: team.workingDir,
|
|
182
203
|
};
|
|
183
204
|
writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
184
|
-
} catch (err) {
|
|
185
|
-
console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
|
|
186
205
|
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
this._removeTeamAndCleanup(team, id);
|
|
@@ -273,32 +294,42 @@ export class Teams {
|
|
|
273
294
|
try { meta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* use defaults */ }
|
|
274
295
|
|
|
275
296
|
const name = meta.originalName || archivedId;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
const mode = meta.mode || 'sandbox';
|
|
298
|
+
|
|
299
|
+
let workingDir;
|
|
300
|
+
if (mode === 'production') {
|
|
301
|
+
workingDir = this.daemon.projectDir;
|
|
302
|
+
// Production archive is metadata-only — just remove the archive directory
|
|
303
|
+
try { rmSync(archivePath, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
304
|
+
} else {
|
|
305
|
+
workingDir = meta.originalWorkingDir || resolve(this.daemon.projectDir, slugify(name));
|
|
306
|
+
|
|
307
|
+
if (existsSync(workingDir)) {
|
|
308
|
+
workingDir = resolve(this.daemon.projectDir, `${slugify(name)}-${Date.now()}`);
|
|
309
|
+
}
|
|
281
310
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
311
|
+
try {
|
|
312
|
+
renameSync(archivePath, workingDir);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err.code === 'EXDEV') {
|
|
315
|
+
cpSync(archivePath, workingDir, { recursive: true });
|
|
316
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
317
|
+
} else {
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
290
320
|
}
|
|
291
|
-
}
|
|
292
321
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
322
|
+
// Remove the metadata file from the restored directory
|
|
323
|
+
const restoredMetaPath = resolve(workingDir, 'metadata.json');
|
|
324
|
+
try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
|
|
325
|
+
}
|
|
296
326
|
|
|
297
327
|
const id = randomUUID().slice(0, 8);
|
|
298
328
|
const team = {
|
|
299
329
|
id,
|
|
300
330
|
name,
|
|
301
331
|
isDefault: false,
|
|
332
|
+
mode,
|
|
302
333
|
workingDir,
|
|
303
334
|
createdAt: new Date().toISOString(),
|
|
304
335
|
};
|
|
@@ -239,6 +239,16 @@ export function validateVerbosity(value) {
|
|
|
239
239
|
return value;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
const VALID_TEAM_MODES = ['sandbox', 'production'];
|
|
243
|
+
|
|
244
|
+
export function validateTeamMode(mode) {
|
|
245
|
+
if (!mode) return 'sandbox';
|
|
246
|
+
if (!VALID_TEAM_MODES.includes(mode)) {
|
|
247
|
+
throw new Error(`Invalid team mode: must be one of ${VALID_TEAM_MODES.join(', ')}`);
|
|
248
|
+
}
|
|
249
|
+
return mode;
|
|
250
|
+
}
|
|
251
|
+
|
|
242
252
|
export function escapeMd(text) {
|
|
243
253
|
if (!text) return '';
|
|
244
254
|
// Escape markdown special chars that could break table rendering or inject formatting.
|