groove-dev 0.16.3 → 0.17.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 (38) hide show
  1. package/README.md +18 -16
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +321 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +152 -0
  4. package/node_modules/@groove-dev/daemon/src/index.js +13 -1
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +389 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +23 -0
  7. package/node_modules/@groove-dev/daemon/src/process.js +59 -0
  8. package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
  9. package/node_modules/@groove-dev/daemon/src/scheduler.js +336 -0
  10. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +119 -54
  11. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-C5k-qSwi.js +153 -0
  13. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  14. package/node_modules/@groove-dev/gui/src/App.jsx +6 -0
  15. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +98 -7
  16. package/node_modules/@groove-dev/gui/src/components/Terminal.jsx +29 -12
  17. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +954 -0
  18. package/node_modules/@groove-dev/gui/src/views/ScheduleManager.jsx +614 -0
  19. package/package.json +2 -2
  20. package/packages/daemon/integrations-registry.json +321 -0
  21. package/packages/daemon/src/api.js +152 -0
  22. package/packages/daemon/src/index.js +13 -1
  23. package/packages/daemon/src/integrations.js +389 -0
  24. package/packages/daemon/src/introducer.js +23 -0
  25. package/packages/daemon/src/process.js +59 -0
  26. package/packages/daemon/src/registry.js +2 -1
  27. package/packages/daemon/src/scheduler.js +336 -0
  28. package/packages/daemon/src/terminal-pty.js +119 -54
  29. package/packages/daemon/src/validate.js +10 -0
  30. package/packages/gui/dist/assets/index-C5k-qSwi.js +153 -0
  31. package/packages/gui/dist/index.html +1 -1
  32. package/packages/gui/src/App.jsx +6 -0
  33. package/packages/gui/src/components/SpawnPanel.jsx +98 -7
  34. package/packages/gui/src/components/Terminal.jsx +29 -12
  35. package/packages/gui/src/views/IntegrationsStore.jsx +954 -0
  36. package/packages/gui/src/views/ScheduleManager.jsx +614 -0
  37. package/node_modules/@groove-dev/gui/dist/assets/index-CFeltwTB.js +0 -153
  38. package/packages/gui/dist/assets/index-CFeltwTB.js +0 -153
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>GROOVE</title>
7
- <script type="module" crossorigin src="/assets/index-CFeltwTB.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-C5k-qSwi.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BhjOFLBc.css">
9
9
  </head>
10
10
  <body>
@@ -12,13 +12,17 @@ import TeamSelector from './components/TeamSelector';
12
12
  import CommandCenter from './views/CommandCenter';
13
13
  import ApprovalQueue from './components/ApprovalQueue';
14
14
  import SkillsMarketplace from './views/SkillsMarketplace';
15
+ import IntegrationsStore from './views/IntegrationsStore';
16
+ import ScheduleManager from './views/ScheduleManager';
15
17
  import FileEditor from './views/FileEditor';
16
18
 
17
19
  const TABS = [
18
20
  { id: 'agents', label: 'Agents' },
19
21
  { id: 'editor', label: 'Editor' },
22
+ { id: 'integrations', label: 'Integrations' },
20
23
  { id: 'skills', label: 'Skills' },
21
24
  { id: 'stats', label: 'Stats' },
25
+ { id: 'schedules', label: 'Schedules' },
22
26
  { id: 'teams', label: 'Teams' },
23
27
  { id: 'approvals', label: 'Approvals' },
24
28
  ];
@@ -143,8 +147,10 @@ export default function App() {
143
147
  !hasAgents ? <EmptyState /> : <AgentTree />
144
148
  )}
145
149
  {activeTab === 'editor' && <FileEditor />}
150
+ {activeTab === 'integrations' && <IntegrationsStore />}
146
151
  {activeTab === 'skills' && <SkillsMarketplace />}
147
152
  {activeTab === 'stats' && <CommandCenter />}
153
+ {activeTab === 'schedules' && <ScheduleManager />}
148
154
  {activeTab === 'teams' && <TeamSelector />}
149
155
  {activeTab === 'approvals' && <ApprovalQueue />}
150
156
  </main>
@@ -6,13 +6,21 @@ import { useGrooveStore } from '../stores/groove';
6
6
  import DirPicker from './DirPicker';
7
7
 
8
8
  const ROLE_PRESETS = [
9
- { id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'] },
10
- { id: 'frontend', label: 'Frontend', desc: 'UI components, views, styles', scope: ['src/components/**', 'src/views/**', 'src/pages/**', 'src/styles/**'] },
11
- { id: 'fullstack', label: 'Fullstack', desc: 'Full codebase access', scope: [] },
12
- { id: 'planner', label: 'Planner', desc: 'Architecture, research, planning', scope: [] },
13
- { id: 'testing', label: 'Testing', desc: 'Tests, specs, coverage', scope: ['tests/**', 'test/**', '**/*.test.*', '**/*.spec.*'] },
14
- { id: 'devops', label: 'DevOps', desc: 'Docker, CI/CD, infra', scope: ['Dockerfile*', 'docker-compose*', '.github/**', 'infra/**'] },
15
- { id: 'docs', label: 'Docs', desc: 'Documentation, READMEs', scope: ['docs/**', '*.md'] },
9
+ // Coding roles
10
+ { id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'], category: 'coding' },
11
+ { id: 'frontend', label: 'Frontend', desc: 'UI components, views, styles', scope: ['src/components/**', 'src/views/**', 'src/pages/**', 'src/styles/**'], category: 'coding' },
12
+ { id: 'fullstack', label: 'Fullstack', desc: 'Full codebase access', scope: [], category: 'coding' },
13
+ { id: 'planner', label: 'Planner', desc: 'Architecture, research, planning', scope: [], category: 'coding' },
14
+ { id: 'testing', label: 'Testing', desc: 'Tests, specs, coverage', scope: ['tests/**', 'test/**', '**/*.test.*', '**/*.spec.*'], category: 'coding' },
15
+ { id: 'devops', label: 'DevOps', desc: 'Docker, CI/CD, infra', scope: ['Dockerfile*', 'docker-compose*', '.github/**', 'infra/**'], category: 'coding' },
16
+ { id: 'docs', label: 'Docs', desc: 'Documentation, READMEs', scope: ['docs/**', '*.md'], category: 'coding' },
17
+ // Business roles
18
+ { id: 'cmo', label: 'CMO', desc: 'Marketing, social media, content', scope: [], category: 'business', integrations: ['slack', 'brave-search'] },
19
+ { id: 'cfo', label: 'CFO', desc: 'Finance, billing, revenue', scope: [], category: 'business', integrations: ['stripe', 'google-drive'] },
20
+ { id: 'ea', label: 'EA', desc: 'Scheduling, email, comms', scope: [], category: 'business', integrations: ['gmail', 'google-calendar', 'slack'] },
21
+ { id: 'support', label: 'Support', desc: 'Customer support, triage', scope: [], category: 'business', integrations: ['slack', 'discord'] },
22
+ { id: 'analyst', label: 'Analyst', desc: 'Data analysis, reporting', scope: [], category: 'business', integrations: ['postgres', 'google-drive'] },
23
+ { id: 'home', label: 'Home', desc: 'Smart home automation', scope: [], category: 'business', integrations: ['home-assistant'] },
16
24
  ];
17
25
 
18
26
  const PERMISSION_LEVELS = [
@@ -43,11 +51,14 @@ export default function SpawnPanel() {
43
51
  const [showDirPicker, setShowDirPicker] = useState(false);
44
52
  const [installedSkills, setInstalledSkills] = useState([]);
45
53
  const [selectedSkills, setSelectedSkills] = useState([]);
54
+ const [installedIntegrations, setInstalledIntegrations] = useState([]);
55
+ const [selectedIntegrations, setSelectedIntegrations] = useState([]);
46
56
 
47
57
  useEffect(() => {
48
58
  fetchProviders();
49
59
  fetchWorkspaces();
50
60
  fetchInstalledSkills();
61
+ fetchInstalledIntegrations();
51
62
  }, []);
52
63
 
53
64
  async function fetchProviders() {
@@ -72,12 +83,36 @@ export default function SpawnPanel() {
72
83
  } catch { /* ignore */ }
73
84
  }
74
85
 
86
+ async function fetchInstalledIntegrations() {
87
+ try {
88
+ const res = await fetch('/api/integrations/installed');
89
+ setInstalledIntegrations(await res.json());
90
+ } catch { /* ignore */ }
91
+ }
92
+
75
93
  function toggleSkill(skillId) {
76
94
  setSelectedSkills((prev) =>
77
95
  prev.includes(skillId) ? prev.filter((s) => s !== skillId) : [...prev, skillId]
78
96
  );
79
97
  }
80
98
 
99
+ function toggleIntegration(integrationId) {
100
+ setSelectedIntegrations((prev) =>
101
+ prev.includes(integrationId) ? prev.filter((s) => s !== integrationId) : [...prev, integrationId]
102
+ );
103
+ }
104
+
105
+ // Auto-select integrations when a business role is chosen
106
+ useEffect(() => {
107
+ const preset = ROLE_PRESETS.find((p) => p.id === role);
108
+ if (preset?.integrations && installedIntegrations.length > 0) {
109
+ const autoSelect = preset.integrations.filter((id) =>
110
+ installedIntegrations.some((i) => i.id === id && i.configured)
111
+ );
112
+ setSelectedIntegrations(autoSelect);
113
+ }
114
+ }, [role, installedIntegrations]);
115
+
81
116
  const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
82
117
  const effectiveScope = role === 'custom'
83
118
  ? scope
@@ -144,6 +179,7 @@ export default function SpawnPanel() {
144
179
  permission,
145
180
  ...(workingDir.trim() ? { workingDir: workingDir.trim() } : {}),
146
181
  ...(selectedSkills.length > 0 ? { skills: selectedSkills } : {}),
182
+ ...(selectedIntegrations.length > 0 ? { integrations: selectedIntegrations } : {}),
147
183
  });
148
184
  closeDetail();
149
185
  } catch (err) {
@@ -298,6 +334,61 @@ export default function SpawnPanel() {
298
334
  ))}
299
335
  </div>
300
336
 
337
+ {/* Integrations picker */}
338
+ {installedIntegrations.length > 0 && (
339
+ <>
340
+ <div style={styles.label}>INTEGRATIONS</div>
341
+ <div style={styles.skillsGrid}>
342
+ {installedIntegrations.map((item) => {
343
+ const active = selectedIntegrations.includes(item.id);
344
+ const ready = item.configured;
345
+ return (
346
+ <button
347
+ key={item.id}
348
+ type="button"
349
+ onClick={() => ready && toggleIntegration(item.id)}
350
+ title={ready ? item.description : 'Configure credentials first'}
351
+ style={{
352
+ ...styles.skillBtn,
353
+ borderColor: active ? 'var(--accent)' : !ready ? 'var(--amber)' : 'var(--border)',
354
+ background: active ? 'rgba(51, 175, 188, 0.08)' : 'var(--bg-surface)',
355
+ opacity: ready ? 1 : 0.5,
356
+ cursor: ready ? 'pointer' : 'not-allowed',
357
+ }}
358
+ >
359
+ <span style={{
360
+ ...styles.skillIcon,
361
+ background: active ? 'var(--accent)' : !ready ? 'var(--amber)' : 'var(--bg-active)',
362
+ color: active ? 'var(--bg-base)' : 'var(--text-dim)',
363
+ }}>
364
+ {(item.name || '?').charAt(0)}
365
+ </span>
366
+ <div style={{ flex: 1, minWidth: 0 }}>
367
+ <div style={{
368
+ fontSize: 11, fontWeight: 600,
369
+ color: active ? 'var(--text-bright)' : 'var(--text-primary)',
370
+ }}>
371
+ {item.name}
372
+ </div>
373
+ <div style={{ fontSize: 9, color: ready ? 'var(--green)' : 'var(--amber)' }}>
374
+ {ready ? 'connected' : 'needs setup'}
375
+ </div>
376
+ </div>
377
+ {active && (
378
+ <span style={{ fontSize: 10, color: 'var(--accent)', flexShrink: 0 }}>{'\u2713'}</span>
379
+ )}
380
+ </button>
381
+ );
382
+ })}
383
+ </div>
384
+ {selectedIntegrations.length > 0 && (
385
+ <div style={styles.hint}>
386
+ {selectedIntegrations.length} integration{selectedIntegrations.length !== 1 ? 's' : ''} will provide MCP tools to this agent
387
+ </div>
388
+ )}
389
+ </>
390
+ )}
391
+
301
392
  {/* Skills picker */}
302
393
  {installedSkills.length > 0 && (
303
394
  <>
@@ -1,7 +1,7 @@
1
1
  // GROOVE GUI — Embedded Terminal (xterm.js)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React, { useRef, useEffect, useCallback } from 'react';
4
+ import React, { useRef, useEffect } from 'react';
5
5
  import { Terminal as XTerm } from '@xterm/xterm';
6
6
  import { FitAddon } from '@xterm/addon-fit';
7
7
  import { WebLinksAddon } from '@xterm/addon-web-links';
@@ -13,9 +13,10 @@ export default function Terminal({ visible }) {
13
13
  const termRef = useRef(null);
14
14
  const fitAddonRef = useRef(null);
15
15
  const sessionIdRef = useRef(null);
16
+ const spawnedRef = useRef(false);
16
17
  const ws = useGrooveStore((s) => s.ws);
17
18
 
18
- // Create terminal on mount
19
+ // Create xterm instance on mount
19
20
  useEffect(() => {
20
21
  if (!containerRef.current) return;
21
22
 
@@ -55,7 +56,11 @@ export default function Terminal({ visible }) {
55
56
  term.loadAddon(webLinksAddon);
56
57
 
57
58
  term.open(containerRef.current);
58
- fitAddon.fit();
59
+
60
+ // Fit after a frame so container has final dimensions
61
+ requestAnimationFrame(() => {
62
+ try { fitAddon.fit(); } catch { /* ignore */ }
63
+ });
59
64
 
60
65
  termRef.current = term;
61
66
  fitAddonRef.current = fitAddon;
@@ -64,19 +69,24 @@ export default function Terminal({ visible }) {
64
69
  term.dispose();
65
70
  termRef.current = null;
66
71
  fitAddonRef.current = null;
72
+ spawnedRef.current = false;
67
73
  };
68
74
  }, []);
69
75
 
70
- // Spawn shell session when ws is ready
76
+ // Spawn shell session when ws + term are ready
71
77
  useEffect(() => {
72
- if (!ws || ws.readyState !== 1 || !termRef.current) return;
78
+ if (!ws || ws.readyState !== 1 || !termRef.current || spawnedRef.current) return;
73
79
 
74
80
  const term = termRef.current;
81
+ spawnedRef.current = true;
75
82
 
76
- // Request a shell session
77
- ws.send(JSON.stringify({ type: 'terminal:spawn' }));
83
+ // Send spawn with initial dimensions
84
+ ws.send(JSON.stringify({
85
+ type: 'terminal:spawn',
86
+ cols: term.cols || 120,
87
+ rows: term.rows || 30,
88
+ }));
78
89
 
79
- // Listen for terminal messages
80
90
  const handler = (event) => {
81
91
  try {
82
92
  const msg = JSON.parse(event.data);
@@ -93,31 +103,38 @@ export default function Terminal({ visible }) {
93
103
 
94
104
  ws.addEventListener('message', handler);
95
105
 
96
- // Forward keystrokes to daemon
106
+ // Forward keystrokes
97
107
  const inputDisposable = term.onData((data) => {
98
108
  if (sessionIdRef.current && ws.readyState === 1) {
99
109
  ws.send(JSON.stringify({ type: 'terminal:input', id: sessionIdRef.current, data }));
100
110
  }
101
111
  });
102
112
 
113
+ // Forward resize events
114
+ const resizeDisposable = term.onResize(({ cols, rows }) => {
115
+ if (sessionIdRef.current && ws.readyState === 1) {
116
+ ws.send(JSON.stringify({ type: 'terminal:resize', id: sessionIdRef.current, cols, rows }));
117
+ }
118
+ });
119
+
103
120
  return () => {
104
121
  ws.removeEventListener('message', handler);
105
122
  inputDisposable.dispose();
106
- // Kill session on unmount
123
+ resizeDisposable.dispose();
107
124
  if (sessionIdRef.current && ws.readyState === 1) {
108
125
  ws.send(JSON.stringify({ type: 'terminal:kill', id: sessionIdRef.current }));
109
126
  }
110
127
  sessionIdRef.current = null;
128
+ spawnedRef.current = false;
111
129
  };
112
130
  }, [ws]);
113
131
 
114
132
  // Refit on visibility change
115
133
  useEffect(() => {
116
134
  if (visible && fitAddonRef.current) {
117
- // Small delay so container has its final size
118
135
  const timer = setTimeout(() => {
119
136
  try { fitAddonRef.current.fit(); } catch { /* ignore */ }
120
- }, 50);
137
+ }, 80);
121
138
  return () => clearTimeout(timer);
122
139
  }
123
140
  }, [visible]);