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
@@ -0,0 +1,389 @@
1
+ // GROOVE — Integration Store (MCP Server Management)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
5
+ import { resolve, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { execFileSync } from 'child_process';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+
11
+ const INTEGRATIONS_API = 'https://docs.groovedev.ai/api/v1';
12
+
13
+ export class IntegrationStore {
14
+ constructor(daemon) {
15
+ this.daemon = daemon;
16
+ this.integrationsDir = resolve(daemon.grooveDir, 'integrations');
17
+ mkdirSync(this.integrationsDir, { recursive: true });
18
+
19
+ // Load bundled registry as fallback
20
+ this.registry = [];
21
+ try {
22
+ const regPath = resolve(__dirname, '../integrations-registry.json');
23
+ this.registry = JSON.parse(readFileSync(regPath, 'utf8'));
24
+ } catch { /* no registry file */ }
25
+
26
+ // Ensure the integrations directory has a package.json for npm installs
27
+ this._ensurePackageJson();
28
+
29
+ // Fetch live registry in background
30
+ this._refreshRegistry();
31
+ }
32
+
33
+ _ensurePackageJson() {
34
+ const pkgPath = resolve(this.integrationsDir, 'package.json');
35
+ if (!existsSync(pkgPath)) {
36
+ writeFileSync(pkgPath, JSON.stringify({
37
+ name: 'groove-integrations',
38
+ version: '1.0.0',
39
+ private: true,
40
+ description: 'MCP server packages managed by Groove',
41
+ }, null, 2));
42
+ }
43
+ }
44
+
45
+ async _refreshRegistry() {
46
+ try {
47
+ const res = await fetch(`${INTEGRATIONS_API}/integrations?limit=200`, { signal: AbortSignal.timeout(5000) });
48
+ if (res.ok) {
49
+ const data = await res.json();
50
+ this.registry = data.integrations || data;
51
+ }
52
+ } catch { /* offline — use bundled */ }
53
+ }
54
+
55
+ /**
56
+ * Get integrations from the registry with optional search/category filter.
57
+ */
58
+ async getRegistry(query) {
59
+ let items = this.registry.map((s) => ({
60
+ ...s,
61
+ installed: this._isInstalled(s.id),
62
+ configured: this._isConfigured(s),
63
+ }));
64
+
65
+ if (query?.search) {
66
+ const q = query.search.toLowerCase();
67
+ items = items.filter((s) =>
68
+ s.name.toLowerCase().includes(q)
69
+ || s.description.toLowerCase().includes(q)
70
+ || (s.tags || []).some((t) => t.includes(q))
71
+ );
72
+ }
73
+
74
+ if (query?.category && query.category !== 'all') {
75
+ items = items.filter((s) => s.category === query.category);
76
+ }
77
+
78
+ return items;
79
+ }
80
+
81
+ /**
82
+ * Get installed integrations only.
83
+ */
84
+ getInstalled() {
85
+ const installed = [];
86
+ const metaPath = resolve(this.integrationsDir, 'installed.json');
87
+ let installedMeta = {};
88
+ if (existsSync(metaPath)) {
89
+ try { installedMeta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* ignore */ }
90
+ }
91
+
92
+ for (const [id, meta] of Object.entries(installedMeta)) {
93
+ const regEntry = this.registry.find((r) => r.id === id);
94
+ if (!regEntry && !meta) continue;
95
+ installed.push({
96
+ ...(regEntry || {}),
97
+ ...meta,
98
+ id,
99
+ installed: true,
100
+ configured: this._isConfigured(regEntry || meta),
101
+ });
102
+ }
103
+
104
+ return installed;
105
+ }
106
+
107
+ /**
108
+ * Get available categories from the registry.
109
+ */
110
+ getCategories() {
111
+ const cats = new Map();
112
+ for (const item of this.registry) {
113
+ const count = cats.get(item.category) || 0;
114
+ cats.set(item.category, count + 1);
115
+ }
116
+ return Array.from(cats.entries())
117
+ .map(([id, count]) => ({ id, count }))
118
+ .sort((a, b) => b.count - a.count);
119
+ }
120
+
121
+ /**
122
+ * Install an integration (npm install the MCP server package).
123
+ */
124
+ async install(integrationId) {
125
+ const entry = this.registry.find((s) => s.id === integrationId);
126
+ if (!entry) throw new Error(`Integration not found: ${integrationId}`);
127
+ if (this._isInstalled(integrationId)) throw new Error(`Integration already installed: ${integrationId}`);
128
+
129
+ if (entry.npmPackage) {
130
+ try {
131
+ execFileSync('npm', ['install', entry.npmPackage], {
132
+ cwd: this.integrationsDir,
133
+ stdio: 'pipe',
134
+ timeout: 120_000,
135
+ });
136
+ } catch (err) {
137
+ throw new Error(`Failed to install ${entry.npmPackage}: ${err.message}`);
138
+ }
139
+ }
140
+
141
+ // Record installation metadata
142
+ const metaPath = resolve(this.integrationsDir, 'installed.json');
143
+ let installedMeta = {};
144
+ if (existsSync(metaPath)) {
145
+ try { installedMeta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* ignore */ }
146
+ }
147
+ installedMeta[integrationId] = {
148
+ name: entry.name,
149
+ installedAt: new Date().toISOString(),
150
+ npmPackage: entry.npmPackage,
151
+ };
152
+ writeFileSync(metaPath, JSON.stringify(installedMeta, null, 2));
153
+
154
+ this.daemon.audit.log('integration.install', { id: integrationId, name: entry.name });
155
+
156
+ return { id: integrationId, name: entry.name, installed: true };
157
+ }
158
+
159
+ /**
160
+ * Uninstall an integration.
161
+ */
162
+ async uninstall(integrationId) {
163
+ if (!this._isInstalled(integrationId)) throw new Error(`Integration not installed: ${integrationId}`);
164
+
165
+ const entry = this.registry.find((s) => s.id === integrationId);
166
+ if (entry?.npmPackage) {
167
+ try {
168
+ execFileSync('npm', ['uninstall', entry.npmPackage], {
169
+ cwd: this.integrationsDir,
170
+ stdio: 'pipe',
171
+ timeout: 60_000,
172
+ });
173
+ } catch { /* best effort */ }
174
+ }
175
+
176
+ // Remove from installed metadata
177
+ const metaPath = resolve(this.integrationsDir, 'installed.json');
178
+ let installedMeta = {};
179
+ if (existsSync(metaPath)) {
180
+ try { installedMeta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* ignore */ }
181
+ }
182
+ delete installedMeta[integrationId];
183
+ writeFileSync(metaPath, JSON.stringify(installedMeta, null, 2));
184
+
185
+ // Remove credentials for this integration
186
+ this._removeCredentials(integrationId);
187
+
188
+ this.daemon.audit.log('integration.uninstall', { id: integrationId });
189
+
190
+ return { id: integrationId, installed: false };
191
+ }
192
+
193
+ /**
194
+ * Set a credential for an integration.
195
+ */
196
+ setCredential(integrationId, key, value) {
197
+ const credKey = `integration:${integrationId}:${key}`;
198
+ this.daemon.credentials.setKey(credKey, value);
199
+ this.daemon.audit.log('integration.credential.set', { id: integrationId, key });
200
+ }
201
+
202
+ /**
203
+ * Get a credential for an integration.
204
+ */
205
+ getCredential(integrationId, key) {
206
+ const credKey = `integration:${integrationId}:${key}`;
207
+ return this.daemon.credentials.getKey(credKey);
208
+ }
209
+
210
+ /**
211
+ * Delete a credential for an integration.
212
+ */
213
+ deleteCredential(integrationId, key) {
214
+ const credKey = `integration:${integrationId}:${key}`;
215
+ this.daemon.credentials.deleteKey(credKey);
216
+ }
217
+
218
+ /**
219
+ * Get the status of an integration (installed, configured, credential keys).
220
+ */
221
+ getStatus(integrationId) {
222
+ const entry = this.registry.find((s) => s.id === integrationId);
223
+ if (!entry) return null;
224
+
225
+ const installed = this._isInstalled(integrationId);
226
+ const envKeys = (entry.envKeys || []).map((ek) => ({
227
+ ...ek,
228
+ set: !!this.getCredential(integrationId, ek.key),
229
+ }));
230
+ const configured = envKeys.length === 0 || envKeys.every((ek) => !ek.required || ek.set);
231
+
232
+ return { id: integrationId, installed, configured, envKeys };
233
+ }
234
+
235
+ /**
236
+ * Build MCP config object for a set of integration IDs.
237
+ * Returns the mcpServers object to merge into .mcp.json.
238
+ */
239
+ buildMcpConfig(integrationIds) {
240
+ const mcpServers = {};
241
+
242
+ for (const id of integrationIds) {
243
+ const entry = this.registry.find((s) => s.id === id);
244
+ if (!entry) continue;
245
+ if (!this._isInstalled(id)) continue;
246
+
247
+ // Build environment with credentials
248
+ const env = {};
249
+ for (const ek of (entry.envKeys || [])) {
250
+ const val = this.getCredential(id, ek.key);
251
+ if (val) env[ek.key] = val;
252
+ }
253
+
254
+ mcpServers[`groove-${id}`] = {
255
+ command: entry.command || 'npx',
256
+ args: entry.args || ['-y', entry.npmPackage],
257
+ env,
258
+ };
259
+ }
260
+
261
+ return mcpServers;
262
+ }
263
+
264
+ /**
265
+ * Write/merge MCP config into the project root .mcp.json.
266
+ * Only adds/updates groove-* entries, preserves user's own MCP configs.
267
+ */
268
+ writeMcpJson(integrationIds) {
269
+ const mcpJsonPath = resolve(this.daemon.projectDir, '.mcp.json');
270
+ let existing = {};
271
+
272
+ // Read existing .mcp.json if present
273
+ if (existsSync(mcpJsonPath)) {
274
+ try {
275
+ existing = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
276
+ } catch { /* start fresh */ }
277
+ }
278
+
279
+ // Build MCP config for requested integrations
280
+ const grooveServers = this.buildMcpConfig(integrationIds);
281
+
282
+ // Remove all existing groove-* entries first
283
+ if (existing.mcpServers) {
284
+ for (const key of Object.keys(existing.mcpServers)) {
285
+ if (key.startsWith('groove-')) {
286
+ delete existing.mcpServers[key];
287
+ }
288
+ }
289
+ }
290
+
291
+ // Merge groove entries
292
+ existing.mcpServers = { ...(existing.mcpServers || {}), ...grooveServers };
293
+
294
+ writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
295
+ }
296
+
297
+ /**
298
+ * Remove all groove-* entries from .mcp.json.
299
+ * If only groove entries remain, delete the file.
300
+ */
301
+ cleanupMcpJson() {
302
+ const mcpJsonPath = resolve(this.daemon.projectDir, '.mcp.json');
303
+ if (!existsSync(mcpJsonPath)) return;
304
+
305
+ try {
306
+ const config = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
307
+ if (!config.mcpServers) return;
308
+
309
+ // Remove groove-* entries
310
+ let hasUserEntries = false;
311
+ for (const key of Object.keys(config.mcpServers)) {
312
+ if (key.startsWith('groove-')) {
313
+ delete config.mcpServers[key];
314
+ } else {
315
+ hasUserEntries = true;
316
+ }
317
+ }
318
+
319
+ if (hasUserEntries) {
320
+ writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2));
321
+ } else if (Object.keys(config.mcpServers).length === 0) {
322
+ // If we created this file and it's now empty, remove it
323
+ // Only if no other top-level keys besides mcpServers
324
+ const otherKeys = Object.keys(config).filter((k) => k !== 'mcpServers');
325
+ if (otherKeys.length === 0) {
326
+ rmSync(mcpJsonPath);
327
+ } else {
328
+ writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2));
329
+ }
330
+ }
331
+ } catch { /* ignore cleanup errors */ }
332
+ }
333
+
334
+ /**
335
+ * Get the combined set of integration IDs from all running agents.
336
+ */
337
+ getActiveIntegrations() {
338
+ const agents = this.daemon.registry.getAll().filter(
339
+ (a) => a.status === 'running' || a.status === 'starting'
340
+ );
341
+ const ids = new Set();
342
+ for (const agent of agents) {
343
+ for (const id of (agent.integrations || [])) {
344
+ ids.add(id);
345
+ }
346
+ }
347
+ return Array.from(ids);
348
+ }
349
+
350
+ /**
351
+ * Refresh .mcp.json to reflect all currently running agents' integrations.
352
+ */
353
+ refreshMcpJson() {
354
+ const activeIntegrations = this.getActiveIntegrations();
355
+ if (activeIntegrations.length > 0) {
356
+ this.writeMcpJson(activeIntegrations);
357
+ } else {
358
+ this.cleanupMcpJson();
359
+ }
360
+ }
361
+
362
+ // --- Internal ---
363
+
364
+ _isInstalled(integrationId) {
365
+ const metaPath = resolve(this.integrationsDir, 'installed.json');
366
+ if (!existsSync(metaPath)) return false;
367
+ try {
368
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
369
+ return !!meta[integrationId];
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+
375
+ _isConfigured(entry) {
376
+ if (!entry || !entry.envKeys || entry.envKeys.length === 0) return true;
377
+ return entry.envKeys
378
+ .filter((ek) => ek.required)
379
+ .every((ek) => !!this.getCredential(entry.id, ek.key));
380
+ }
381
+
382
+ _removeCredentials(integrationId) {
383
+ const entry = this.registry.find((s) => s.id === integrationId);
384
+ if (!entry?.envKeys) return;
385
+ for (const ek of entry.envKeys) {
386
+ try { this.deleteCredential(integrationId, ek.key); } catch { /* ignore */ }
387
+ }
388
+ }
389
+ }
@@ -188,6 +188,29 @@ export class Introducer {
188
188
  }
189
189
  }
190
190
 
191
+ // Integration context — list MCP tools available to this agent
192
+ if (newAgent.integrations && newAgent.integrations.length > 0 && this.daemon.integrations) {
193
+ const integrationSections = [];
194
+ for (const integrationId of newAgent.integrations) {
195
+ const entry = this.daemon.integrations.registry.find((s) => s.id === integrationId);
196
+ if (entry) {
197
+ const configured = this.daemon.integrations._isConfigured(entry);
198
+ const status = configured ? 'connected' : 'NOT CONFIGURED — credentials missing';
199
+ integrationSections.push(`- **${entry.name}** (${status}): ${entry.description}`);
200
+ }
201
+ }
202
+ if (integrationSections.length > 0) {
203
+ lines.push('');
204
+ lines.push(`## Integrations (${integrationSections.length} connected)`);
205
+ lines.push('');
206
+ lines.push('You have MCP tools available from these integrations. Use them to interact with external services:');
207
+ lines.push('');
208
+ lines.push(integrationSections.join('\n'));
209
+ lines.push('');
210
+ lines.push('Call these tools directly — they are available in your MCP tool list. Do not attempt to use curl or API calls for services that have an MCP integration attached.');
211
+ }
212
+ }
213
+
191
214
  return lines.join('\n');
192
215
  }
193
216
 
@@ -9,6 +9,55 @@ import { getProvider } from './providers/index.js';
9
9
  // Role-specific prompt prefixes — applied during spawn regardless of entry point
10
10
  // (SpawnPanel, chat continue, CLI, API) for consistency
11
11
  const ROLE_PROMPTS = {
12
+ // Business roles — use MCP tools, not code
13
+ cmo: `You are a Chief Marketing Officer agent. You have MCP integrations for communication and research. Focus on:
14
+ - Drafting and reviewing marketing content, social media posts, and campaigns
15
+ - Analyzing market trends and competitive positioning
16
+ - Managing team communications and status updates via Slack
17
+ - Researching topics using web search tools
18
+ Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
19
+
20
+ `,
21
+ cfo: `You are a Chief Financial Officer agent. You have MCP integrations for financial data and reporting. Focus on:
22
+ - Reviewing revenue, subscriptions, and payment data via Stripe
23
+ - Creating financial summaries and reports
24
+ - Analyzing spending patterns and forecasting
25
+ - Managing financial documents and spreadsheets
26
+ Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
27
+
28
+ `,
29
+ ea: `You are an Executive Assistant agent. You have MCP integrations for email, calendar, and communication. Focus on:
30
+ - Managing calendar events and scheduling meetings
31
+ - Drafting and sending emails
32
+ - Coordinating team communications via Slack
33
+ - Organizing tasks, reminders, and follow-ups
34
+ Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
35
+
36
+ `,
37
+ support: `You are a Customer Support agent. You have MCP integrations for communication channels. Focus on:
38
+ - Responding to customer inquiries and tickets
39
+ - Triaging and categorizing support requests
40
+ - Drafting helpful responses and knowledge base articles
41
+ - Escalating critical issues with clear summaries
42
+ Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
43
+
44
+ `,
45
+ analyst: `You are a Data Analyst agent. You have MCP integrations for databases and data tools. Focus on:
46
+ - Querying databases to extract insights and trends
47
+ - Creating data summaries and reports
48
+ - Identifying patterns, anomalies, and opportunities
49
+ - Presenting findings in clear, actionable format
50
+ Do NOT write code unless explicitly asked. Use your MCP tools (database queries, spreadsheets) to analyze data.
51
+
52
+ `,
53
+ home: `You are a Smart Home automation agent. You have MCP integrations for Home Assistant. Focus on:
54
+ - Monitoring and controlling smart home devices
55
+ - Setting up automations and routines
56
+ - Reporting on device status and energy usage
57
+ - Troubleshooting connectivity and configuration issues
58
+ Do NOT write code unless explicitly asked. Use your MCP tools to interact with Home Assistant.
59
+
60
+ `,
12
61
  planner: `You are a planning and architecture agent. Research, analyze, and create plans — do NOT implement code unless explicitly asked. Focus on:
13
62
  - Understanding requirements
14
63
  - Exploring the codebase
@@ -152,6 +201,11 @@ For normal file edits within your scope, proceed without review.
152
201
  }
153
202
  }
154
203
 
204
+ // Write MCP config for agent integrations
205
+ if (config.integrations?.length > 0 && this.daemon.integrations) {
206
+ this.daemon.integrations.writeMcpJson(config.integrations);
207
+ }
208
+
155
209
  const { command, args, env } = provider.buildSpawnCommand(spawnConfig);
156
210
 
157
211
  // Set up log capture
@@ -258,6 +312,11 @@ For normal file edits within your scope, proceed without review.
258
312
  status: finalStatus,
259
313
  });
260
314
 
315
+ // Refresh MCP config — remove integrations no longer needed by running agents
316
+ if (this.daemon.integrations) {
317
+ this.daemon.integrations.refreshMcpJson();
318
+ }
319
+
261
320
  // Trigger journalist synthesis immediately on completion so the project
262
321
  // map is fresh for the next agent that spawns (don't wait for 120s cycle)
263
322
  if (finalStatus === 'completed' && this.daemon.journalist) {
@@ -23,6 +23,7 @@ export class Registry extends EventEmitter {
23
23
  permission: config.permission || 'full',
24
24
  workingDir: config.workingDir || process.cwd(),
25
25
  skills: config.skills || [],
26
+ integrations: config.integrations || [],
26
27
  status: 'starting',
27
28
  pid: null,
28
29
  spawnedAt: new Date().toISOString(),
@@ -49,7 +50,7 @@ export class Registry extends EventEmitter {
49
50
  if (!agent) return null;
50
51
 
51
52
  // Only allow known fields to prevent prototype pollution
52
- const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills'];
53
+ const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations'];
53
54
  for (const key of Object.keys(updates)) {
54
55
  if (SAFE_FIELDS.includes(key)) {
55
56
  agent[key] = updates[key];