groove-dev 0.27.118 → 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.
Files changed (41) hide show
  1. package/moe-training/client/trajectory-capture.js +7 -0
  2. package/moe-training/client/transmission-queue.js +6 -0
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/src/api.js +13 -4
  6. package/node_modules/@groove-dev/daemon/src/index.js +4 -0
  7. package/node_modules/@groove-dev/daemon/src/teams.js +70 -39
  8. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-BunEIVjD.js → index-BxPCaxlC.js} +131 -131
  10. package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  14. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
  15. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
  16. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  17. package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +151 -0
  18. package/node_modules/@groove-dev/gui/src/stores/groove.js +29 -5
  19. package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
  20. package/node_modules/@groove-dev/gui/src/views/teams.jsx +4 -0
  21. package/node_modules/moe-training/client/trajectory-capture.js +7 -0
  22. package/node_modules/moe-training/client/transmission-queue.js +6 -0
  23. package/package.json +1 -1
  24. package/packages/cli/package.json +1 -1
  25. package/packages/daemon/package.json +1 -1
  26. package/packages/daemon/src/api.js +13 -4
  27. package/packages/daemon/src/index.js +4 -0
  28. package/packages/daemon/src/teams.js +70 -39
  29. package/packages/daemon/src/validate.js +10 -0
  30. package/packages/gui/dist/assets/{index-BunEIVjD.js → index-BxPCaxlC.js} +131 -131
  31. package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
  32. package/packages/gui/dist/index.html +2 -2
  33. package/packages/gui/package.json +1 -1
  34. package/packages/gui/src/app.jsx +2 -0
  35. package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
  36. package/packages/gui/src/components/editor/file-tree.jsx +80 -3
  37. package/packages/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  38. package/packages/gui/src/components/ui/data-sharing-modal.jsx +151 -0
  39. package/packages/gui/src/stores/groove.js +29 -5
  40. package/packages/gui/src/views/agents.jsx +47 -11
  41. package/packages/gui/src/views/teams.jsx +4 -0
@@ -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 {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.118",
3
+ "version": "0.27.119",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.118",
3
+ "version": "0.27.119",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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.workingDir);
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 (projectDir) {
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, basename } from 'path';
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
- // Create the directory
74
- mkdirSync(workingDir, { recursive: true });
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
- if (
154
- team.workingDir &&
155
- team.workingDir !== this.daemon.projectDir &&
156
- existsSync(team.workingDir)
157
- ) {
158
- try {
159
- const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
160
- mkdirSync(archiveDir, { recursive: true });
161
- const slug = basename(team.workingDir);
162
- const archiveName = `${slug}-${Date.now()}`;
163
- const archivePath = resolve(archiveDir, archiveName);
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
- let workingDir = meta.originalWorkingDir || resolve(this.daemon.projectDir, slugify(name));
277
-
278
- if (existsSync(workingDir)) {
279
- workingDir = resolve(this.daemon.projectDir, `${slugify(name)}-${Date.now()}`);
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
- try {
283
- renameSync(archivePath, workingDir);
284
- } catch (err) {
285
- if (err.code === 'EXDEV') {
286
- cpSync(archivePath, workingDir, { recursive: true });
287
- rmSync(archivePath, { recursive: true, force: true });
288
- } else {
289
- throw err;
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
- // Remove the metadata file from the restored directory
294
- const restoredMetaPath = resolve(workingDir, 'metadata.json');
295
- try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
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.