groove-dev 0.27.113 → 0.27.116

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 (70) hide show
  1. package/CENTRAL_COMMAND_REBUILD.md +689 -0
  2. package/EMBEDDING_DIAGNOSTIC.md +197 -0
  3. package/TRAINING_DATA_v4.md +6 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/team.js +59 -2
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +27 -2
  8. package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
  9. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  10. package/node_modules/@groove-dev/daemon/src/process.js +254 -208
  11. package/node_modules/@groove-dev/daemon/src/teams.js +143 -20
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +78 -45
  13. package/node_modules/@groove-dev/gui/dist/assets/index-DdN9RVnC.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
  18. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
  20. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  21. package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -12
  22. package/node_modules/@groove-dev/gui/src/views/agents.jsx +23 -4
  23. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
  24. package/node_modules/@groove-dev/gui/src/views/teams.jsx +84 -5
  25. package/package.json +1 -1
  26. package/packages/cli/package.json +1 -1
  27. package/packages/cli/src/commands/team.js +59 -2
  28. package/packages/daemon/package.json +1 -1
  29. package/packages/daemon/src/api.js +27 -2
  30. package/packages/daemon/src/filewatcher.js +45 -0
  31. package/packages/daemon/src/index.js +14 -2
  32. package/packages/daemon/src/process.js +254 -208
  33. package/packages/daemon/src/teams.js +143 -20
  34. package/packages/daemon/src/tunnel-manager.js +78 -45
  35. package/packages/gui/dist/assets/index-DdN9RVnC.css +1 -0
  36. package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
  40. package/packages/gui/src/components/layout/status-bar.jsx +43 -45
  41. package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
  42. package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  43. package/packages/gui/src/stores/groove.js +57 -12
  44. package/packages/gui/src/views/agents.jsx +23 -4
  45. package/packages/gui/src/views/editor.jsx +1 -20
  46. package/packages/gui/src/views/teams.jsx +84 -5
  47. package/TRAINING_DATA_v3.md +0 -11
  48. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
  49. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
  50. package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
  51. package/codex-test/offroad-nitro-racer/index.html +0 -21
  52. package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
  53. package/codex-test/offroad-nitro-racer/package.json +0 -15
  54. package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
  55. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
  56. package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
  57. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
  58. package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
  59. package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
  60. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
  61. package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
  62. package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
  63. package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
  64. package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
  65. package/codex-test/offroad-nitro-racer/src/style.css +0 -291
  66. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
  67. package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
  68. package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
  69. package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
  70. package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
@@ -12,17 +12,30 @@ import { cn } from '../lib/cn';
12
12
  import {
13
13
  Clock, CheckCircle, XCircle, AlertTriangle, ShieldCheck, ShieldX,
14
14
  Users, Folder, Cpu, Trash2, Play, Pause, LayoutDashboard, ListChecks, Calendar,
15
+ Archive, RotateCcw, ChevronRight,
15
16
  } from 'lucide-react';
17
+ import { TeamRemovalDialog, PurgeConfirmDialog } from '../components/teams/team-removal-dialog';
16
18
 
17
19
  // ── Team Dashboard ────────────────────────────────────────────
18
20
  function TeamsDashboard() {
19
21
  const teams = useGrooveStore((s) => s.teams);
20
22
  const agents = useGrooveStore((s) => s.agents);
21
23
  const activeTeamId = useGrooveStore((s) => s.activeTeamId);
22
- const deleteTeam = useGrooveStore((s) => s.deleteTeam);
24
+ const archiveTeam = useGrooveStore((s) => s.archiveTeam);
25
+ const deleteTeamPermanently = useGrooveStore((s) => s.deleteTeamPermanently);
23
26
  const addToast = useGrooveStore((s) => s.addToast);
27
+ const archivedTeams = useGrooveStore((s) => s.archivedTeams);
28
+ const fetchArchivedTeams = useGrooveStore((s) => s.fetchArchivedTeams);
29
+ const restoreTeam = useGrooveStore((s) => s.restoreTeam);
30
+ const purgeTeam = useGrooveStore((s) => s.purgeTeam);
24
31
 
25
- if (teams.length === 0) {
32
+ const [archiveConfirm, setArchiveConfirm] = useState(null);
33
+ const [purgeConfirm, setPurgeConfirm] = useState(null);
34
+ const [archivedOpen, setArchivedOpen] = useState(false);
35
+
36
+ useEffect(() => { fetchArchivedTeams(); }, []);
37
+
38
+ if (teams.length === 0 && archivedTeams.length === 0) {
26
39
  return (
27
40
  <div className="flex-1 flex items-center justify-center">
28
41
  <div className="text-center space-y-2">
@@ -70,14 +83,14 @@ function TeamsDashboard() {
70
83
  </div>
71
84
  <button
72
85
  onClick={() => {
73
- if (teamAgents.some((a) => a.status === 'running')) {
86
+ if (teamAgents.some((a) => a.status === 'running' || a.status === 'starting')) {
74
87
  addToast('error', 'Stop running agents first');
75
88
  return;
76
89
  }
77
- deleteTeam(team.id);
90
+ setArchiveConfirm(team);
78
91
  }}
79
92
  className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
80
- title="Delete team"
93
+ title="Archive team"
81
94
  >
82
95
  <Trash2 size={13} />
83
96
  </button>
@@ -112,6 +125,72 @@ function TeamsDashboard() {
112
125
  );
113
126
  })}
114
127
  </div>
128
+
129
+ {/* Archived Teams */}
130
+ {archivedTeams.length > 0 && (
131
+ <div className="border-t border-border-subtle">
132
+ <button
133
+ onClick={() => setArchivedOpen(!archivedOpen)}
134
+ className="w-full flex items-center gap-2 px-5 py-3 text-left cursor-pointer hover:bg-surface-5/30 transition-colors"
135
+ >
136
+ <ChevronRight
137
+ size={12}
138
+ className={cn('text-text-4 transition-transform duration-200', archivedOpen && 'rotate-90')}
139
+ />
140
+ <Archive size={13} className="text-text-3" />
141
+ <span className="text-xs font-semibold text-text-2 font-sans uppercase tracking-wider flex-1">
142
+ Archived Teams
143
+ </span>
144
+ <span className="text-2xs font-mono text-text-4 bg-surface-4 px-1.5 py-0.5 rounded">
145
+ {archivedTeams.length}
146
+ </span>
147
+ </button>
148
+ {archivedOpen && (
149
+ <div className="px-4 pb-4 space-y-2">
150
+ {archivedTeams.map((at) => (
151
+ <div key={at.id} className="flex items-center gap-3 px-3 py-2.5 rounded-md bg-surface-0 border border-border-subtle">
152
+ <Archive size={13} className="text-text-4 flex-shrink-0" />
153
+ <div className="flex-1 min-w-0">
154
+ <span className="text-xs font-semibold text-text-1 font-sans">{at.originalName || at.name}</span>
155
+ {(at.deletedAt || at.archivedAt) && (
156
+ <div className="text-2xs text-text-4 font-mono mt-0.5">Archived {timeAgo(at.deletedAt || at.archivedAt)}</div>
157
+ )}
158
+ </div>
159
+ <button
160
+ onClick={() => restoreTeam(at.id)}
161
+ className="p-1.5 text-text-3 hover:text-accent rounded transition-colors cursor-pointer"
162
+ title="Restore team"
163
+ >
164
+ <RotateCcw size={13} />
165
+ </button>
166
+ <button
167
+ onClick={() => setPurgeConfirm(at)}
168
+ className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
169
+ title="Permanently delete"
170
+ >
171
+ <Trash2 size={13} />
172
+ </button>
173
+ </div>
174
+ ))}
175
+ </div>
176
+ )}
177
+ </div>
178
+ )}
179
+
180
+ <TeamRemovalDialog
181
+ team={archiveConfirm}
182
+ open={!!archiveConfirm}
183
+ onOpenChange={(open) => !open && setArchiveConfirm(null)}
184
+ onArchive={archiveTeam}
185
+ onDeletePermanently={deleteTeamPermanently}
186
+ />
187
+
188
+ <PurgeConfirmDialog
189
+ team={purgeConfirm}
190
+ open={!!purgeConfirm}
191
+ onOpenChange={(open) => !open && setPurgeConfirm(null)}
192
+ onPurge={purgeTeam}
193
+ />
115
194
  </div>
116
195
  );
117
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.113",
3
+ "version": "0.27.116",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.113",
3
+ "version": "0.27.116",
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,9 +1,17 @@
1
1
  // GROOVE CLI — team commands
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
+ import { createInterface } from 'readline';
4
5
  import chalk from 'chalk';
5
6
  import { apiCall } from '../client.js';
6
7
 
8
+ function prompt(question) {
9
+ return new Promise((resolve) => {
10
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
11
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
12
+ });
13
+ }
14
+
7
15
  export async function teamCreate(name) {
8
16
  try {
9
17
  const team = await apiCall('POST', '/api/teams', { name });
@@ -37,9 +45,58 @@ export async function teamList() {
37
45
  }
38
46
 
39
47
  export async function teamDelete(id) {
48
+ const choice = await prompt(` Archive or permanently delete? [${chalk.bold('a')}rchive / ${chalk.bold('D')}elete] `);
49
+ const normalized = choice.toLowerCase();
50
+
51
+ if (normalized === 'd' || normalized === 'delete') {
52
+ const teamName = await prompt(chalk.yellow(' WARNING: All files in this team will be permanently lost.\n') + ` Type the team name to confirm: `);
53
+ if (!teamName) {
54
+ console.log(chalk.dim(' Cancelled.'));
55
+ return;
56
+ }
57
+ try {
58
+ await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}?permanent=true`);
59
+ console.log(chalk.green(` Permanently deleted team "${id}".`));
60
+ } catch (err) {
61
+ console.error(chalk.red(' Failed:'), err.message);
62
+ process.exit(1);
63
+ }
64
+ } else if (normalized === 'a' || normalized === 'archive' || normalized === '') {
65
+ try {
66
+ await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}`);
67
+ console.log(chalk.green(` Archived team "${id}"`) + chalk.dim(' — restore with `groove team restore <id>`'));
68
+ } catch (err) {
69
+ console.error(chalk.red(' Failed:'), err.message);
70
+ process.exit(1);
71
+ }
72
+ } else {
73
+ console.log(chalk.dim(' Cancelled.'));
74
+ }
75
+ }
76
+
77
+ export async function teamArchived() {
78
+ try {
79
+ const { archived } = await apiCall('GET', '/api/teams/archived');
80
+ if (archived.length === 0) {
81
+ console.log(chalk.dim(' No archived teams.'));
82
+ return;
83
+ }
84
+ console.log(chalk.bold(`\n Archived Teams (${archived.length})\n`));
85
+ for (const t of archived) {
86
+ const date = t.deletedAt ? new Date(t.deletedAt).toLocaleDateString() : 'unknown';
87
+ console.log(` ${chalk.bold(t.originalName || t.id)} — archive-id: ${t.id} — deleted ${date} — ${t.agentCount} agent(s)`);
88
+ }
89
+ console.log('');
90
+ } catch {
91
+ console.error(chalk.red(' Cannot connect to daemon.'));
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ export async function teamRestore(id) {
40
97
  try {
41
- await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}`);
42
- console.log(chalk.green(` Deleted team "${id}"`));
98
+ const team = await apiCall('POST', `/api/teams/archived/${encodeURIComponent(id)}/restore`);
99
+ console.log(chalk.green(` Restored team "${team.name}"`) + ` (new id: ${team.id})`);
43
100
  } catch (err) {
44
101
  console.error(chalk.red(' Failed:'), err.message);
45
102
  process.exit(1);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.113",
3
+ "version": "0.27.116",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1100,6 +1100,30 @@ export function createApi(app, daemon) {
1100
1100
  }
1101
1101
  });
1102
1102
 
1103
+ app.get('/api/teams/archived', (req, res) => {
1104
+ res.json({ archived: daemon.teams.listArchived() });
1105
+ });
1106
+
1107
+ app.post('/api/teams/archived/:id/restore', (req, res) => {
1108
+ try {
1109
+ const team = daemon.teams.restore(req.params.id);
1110
+ daemon.audit.log('team.restore', { archivedId: req.params.id, newId: team.id, name: team.name });
1111
+ res.json(team);
1112
+ } catch (err) {
1113
+ res.status(400).json({ error: err.message });
1114
+ }
1115
+ });
1116
+
1117
+ app.delete('/api/teams/archived/:id', (req, res) => {
1118
+ try {
1119
+ daemon.teams.purge(req.params.id);
1120
+ daemon.audit.log('team.purge', { archivedId: req.params.id });
1121
+ res.json({ ok: true });
1122
+ } catch (err) {
1123
+ res.status(400).json({ error: err.message });
1124
+ }
1125
+ });
1126
+
1103
1127
  app.patch('/api/teams/:id', (req, res) => {
1104
1128
  try {
1105
1129
  if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
@@ -1114,8 +1138,9 @@ export function createApi(app, daemon) {
1114
1138
 
1115
1139
  app.delete('/api/teams/:id', (req, res) => {
1116
1140
  try {
1117
- daemon.teams.delete(req.params.id);
1118
- daemon.audit.log('team.delete', { id: req.params.id });
1141
+ const permanent = req.query.permanent === 'true';
1142
+ daemon.teams.delete(req.params.id, { permanent });
1143
+ daemon.audit.log(permanent ? 'team.delete' : 'team.archive', { id: req.params.id, permanent });
1119
1144
  res.json({ ok: true });
1120
1145
  } catch (err) {
1121
1146
  res.status(400).json({ error: err.message });
@@ -8,6 +8,7 @@ export class FileWatcher {
8
8
  constructor(daemon) {
9
9
  this.daemon = daemon;
10
10
  this.watchers = new Map(); // relPath → { watcher, timer }
11
+ this.dirWatchers = new Map(); // relPath → { watcher, timer }
11
12
  }
12
13
 
13
14
  watch(relPath) {
@@ -51,9 +52,53 @@ export class FileWatcher {
51
52
  this.watchers.delete(relPath);
52
53
  }
53
54
 
55
+ watchDir(relPath) {
56
+ if (typeof relPath !== 'string') return;
57
+ if (relPath && relPath.includes('..')) return;
58
+ if (this.dirWatchers.has(relPath)) return;
59
+
60
+ const fullPath = relPath ? resolve(this.daemon.projectDir, relPath) : this.daemon.projectDir;
61
+
62
+ try {
63
+ const watcher = watch(fullPath, () => {
64
+ const entry = this.dirWatchers.get(relPath);
65
+ if (!entry) return;
66
+
67
+ if (entry.timer) clearTimeout(entry.timer);
68
+ entry.timer = setTimeout(() => {
69
+ this.daemon.broadcast({
70
+ type: 'file:tree-changed',
71
+ path: relPath,
72
+ timestamp: Date.now(),
73
+ });
74
+ }, 300);
75
+ });
76
+
77
+ watcher.on('error', () => {
78
+ this.unwatchDir(relPath);
79
+ });
80
+
81
+ this.dirWatchers.set(relPath, { watcher, timer: null });
82
+ } catch {
83
+ // Directory doesn't exist or not watchable — ignore
84
+ }
85
+ }
86
+
87
+ unwatchDir(relPath) {
88
+ const entry = this.dirWatchers.get(relPath);
89
+ if (!entry) return;
90
+
91
+ if (entry.timer) clearTimeout(entry.timer);
92
+ try { entry.watcher.close(); } catch { /* already closed */ }
93
+ this.dirWatchers.delete(relPath);
94
+ }
95
+
54
96
  unwatchAll() {
55
97
  for (const [relPath] of this.watchers) {
56
98
  this.unwatch(relPath);
57
99
  }
100
+ for (const [relPath] of this.dirWatchers) {
101
+ this.unwatchDir(relPath);
102
+ }
58
103
  }
59
104
  }
@@ -325,8 +325,9 @@ export class Daemon {
325
325
  data: enrichAgents(this.registry.getAll()),
326
326
  }));
327
327
 
328
- // Track which files this client is watching (for cleanup on disconnect)
328
+ // Track which files/dirs this client is watching (for cleanup on disconnect)
329
329
  const watchedFiles = new Set();
330
+ const watchedDirs = new Set();
330
331
 
331
332
  ws.on('message', (raw) => {
332
333
  try {
@@ -335,7 +336,7 @@ export class Daemon {
335
336
  // Validate message type against whitelist
336
337
  const VALID_WS_TYPES = new Set([
337
338
  'terminal:spawn', 'terminal:resize', 'terminal:input', 'terminal:close', 'terminal:kill', 'terminal:rename',
338
- 'editor:watch', 'editor:unwatch', 'editor:save',
339
+ 'editor:watch', 'editor:unwatch', 'editor:save', 'editor:watchdir', 'editor:unwatchdir',
339
340
  'ping'
340
341
  ]);
341
342
  if (!msg || typeof msg !== 'object' || !VALID_WS_TYPES.has(msg.type)) return;
@@ -351,6 +352,14 @@ export class Daemon {
351
352
  case 'editor:unwatch':
352
353
  if (msg.path) { this.fileWatcher.unwatch(msg.path); watchedFiles.delete(msg.path); }
353
354
  break;
355
+ case 'editor:watchdir':
356
+ if (typeof msg.path === 'string' && !msg.path.includes('..')) {
357
+ this.fileWatcher.watchDir(msg.path); watchedDirs.add(msg.path);
358
+ }
359
+ break;
360
+ case 'editor:unwatchdir':
361
+ if (typeof msg.path === 'string') { this.fileWatcher.unwatchDir(msg.path); watchedDirs.delete(msg.path); }
362
+ break;
354
363
  // Terminal
355
364
  case 'terminal:spawn': {
356
365
  if (msg.cwd !== undefined && (typeof msg.cwd !== 'string' || msg.cwd.includes('..'))) break;
@@ -389,6 +398,9 @@ export class Daemon {
389
398
  for (const path of watchedFiles) {
390
399
  this.fileWatcher.unwatch(path);
391
400
  }
401
+ for (const path of watchedDirs) {
402
+ this.fileWatcher.unwatchDir(path);
403
+ }
392
404
  this.terminalManager.cleanupClient(ws);
393
405
  });
394
406
  });