groove-dev 0.17.0 → 0.17.2
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/node_modules/@groove-dev/daemon/integrations-registry.json +105 -9
- package/node_modules/@groove-dev/daemon/src/api.js +52 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +106 -8
- package/node_modules/@groove-dev/daemon/src/process.js +5 -2
- package/node_modules/@groove-dev/gui/dist/assets/{index-C5k-qSwi.js → index-CEf7nLM2.js} +38 -35
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +251 -34
- package/package.json +1 -1
- package/packages/daemon/integrations-registry.json +105 -9
- package/packages/daemon/src/api.js +52 -0
- package/packages/daemon/src/integrations.js +106 -8
- package/packages/daemon/src/process.js +5 -2
- package/packages/gui/dist/assets/{index-C5k-qSwi.js → index-CEf7nLM2.js} +38 -35
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/views/IntegrationsStore.jsx +251 -34
|
@@ -235,6 +235,8 @@ export class IntegrationStore {
|
|
|
235
235
|
/**
|
|
236
236
|
* Build MCP config object for a set of integration IDs.
|
|
237
237
|
* Returns the mcpServers object to merge into .mcp.json.
|
|
238
|
+
* SECURITY: credentials are NOT included in the config file.
|
|
239
|
+
* They are injected at spawn time via process environment only.
|
|
238
240
|
*/
|
|
239
241
|
buildMcpConfig(integrationIds) {
|
|
240
242
|
const mcpServers = {};
|
|
@@ -244,23 +246,33 @@ export class IntegrationStore {
|
|
|
244
246
|
if (!entry) continue;
|
|
245
247
|
if (!this._isInstalled(id)) continue;
|
|
246
248
|
|
|
247
|
-
//
|
|
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
|
-
|
|
249
|
+
// No env block — credentials stay out of .mcp.json
|
|
254
250
|
mcpServers[`groove-${id}`] = {
|
|
255
251
|
command: entry.command || 'npx',
|
|
256
252
|
args: entry.args || ['-y', entry.npmPackage],
|
|
257
|
-
env,
|
|
258
253
|
};
|
|
259
254
|
}
|
|
260
255
|
|
|
261
256
|
return mcpServers;
|
|
262
257
|
}
|
|
263
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Get environment variables with decrypted credentials for a set of integration IDs.
|
|
261
|
+
* These are passed to the agent process at spawn time (in-memory only, never written to disk).
|
|
262
|
+
*/
|
|
263
|
+
getSpawnEnv(integrationIds) {
|
|
264
|
+
const env = {};
|
|
265
|
+
for (const id of integrationIds) {
|
|
266
|
+
const entry = this.registry.find((s) => s.id === id);
|
|
267
|
+
if (!entry) continue;
|
|
268
|
+
for (const ek of (entry.envKeys || [])) {
|
|
269
|
+
const val = this.getCredential(id, ek.key);
|
|
270
|
+
if (val) env[ek.key] = val;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return env;
|
|
274
|
+
}
|
|
275
|
+
|
|
264
276
|
/**
|
|
265
277
|
* Write/merge MCP config into the project root .mcp.json.
|
|
266
278
|
* Only adds/updates groove-* entries, preserves user's own MCP configs.
|
|
@@ -359,6 +371,92 @@ export class IntegrationStore {
|
|
|
359
371
|
}
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Start an OAuth flow for a Google integration.
|
|
376
|
+
* Returns the authorization URL to open in a browser.
|
|
377
|
+
*/
|
|
378
|
+
getOAuthUrl(integrationId) {
|
|
379
|
+
const entry = this.registry.find((s) => s.id === integrationId);
|
|
380
|
+
if (!entry) throw new Error(`Integration not found: ${integrationId}`);
|
|
381
|
+
if (entry.authType !== 'oauth-google') throw new Error('Integration does not use OAuth');
|
|
382
|
+
|
|
383
|
+
// Check if user has provided their own Google OAuth client (stored globally)
|
|
384
|
+
const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
|
|
385
|
+
const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
|
|
386
|
+
if (!clientId || !clientSecret) {
|
|
387
|
+
throw new Error('Google OAuth not configured. Set up your Google Cloud project first.');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const port = this.daemon.port || 31415;
|
|
391
|
+
const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
|
|
392
|
+
const scopes = entry.oauthScopes || [];
|
|
393
|
+
|
|
394
|
+
const params = new URLSearchParams({
|
|
395
|
+
client_id: clientId,
|
|
396
|
+
redirect_uri: redirectUri,
|
|
397
|
+
response_type: 'code',
|
|
398
|
+
scope: scopes.join(' '),
|
|
399
|
+
access_type: 'offline',
|
|
400
|
+
prompt: 'consent',
|
|
401
|
+
state: integrationId,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Handle OAuth callback — exchange code for tokens.
|
|
409
|
+
*/
|
|
410
|
+
async handleOAuthCallback(code, integrationId) {
|
|
411
|
+
const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
|
|
412
|
+
const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
|
|
413
|
+
if (!clientId || !clientSecret) {
|
|
414
|
+
throw new Error('Google OAuth credentials not found');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const port = this.daemon.port || 31415;
|
|
418
|
+
const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
|
|
419
|
+
|
|
420
|
+
const res = await fetch('https://oauth2.googleapis.com/token', {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
423
|
+
body: new URLSearchParams({
|
|
424
|
+
code,
|
|
425
|
+
client_id: clientId,
|
|
426
|
+
client_secret: clientSecret,
|
|
427
|
+
redirect_uri: redirectUri,
|
|
428
|
+
grant_type: 'authorization_code',
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (!res.ok) {
|
|
433
|
+
const err = await res.json().catch(() => ({}));
|
|
434
|
+
throw new Error(`OAuth token exchange failed: ${err.error_description || err.error || 'unknown'}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const tokens = await res.json();
|
|
438
|
+
|
|
439
|
+
// Store the tokens for this integration
|
|
440
|
+
this.setCredential(integrationId, 'GOOGLE_CLIENT_ID', clientId);
|
|
441
|
+
this.setCredential(integrationId, 'GOOGLE_CLIENT_SECRET', clientSecret);
|
|
442
|
+
if (tokens.refresh_token) {
|
|
443
|
+
this.setCredential(integrationId, 'GOOGLE_REFRESH_TOKEN', tokens.refresh_token);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.daemon.audit.log('integration.oauth.complete', { id: integrationId });
|
|
447
|
+
|
|
448
|
+
return { ok: true, integrationId };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Check if Google OAuth is configured (user has set up their Cloud project).
|
|
453
|
+
*/
|
|
454
|
+
isGoogleOAuthConfigured() {
|
|
455
|
+
const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
|
|
456
|
+
const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
|
|
457
|
+
return !!(clientId && clientSecret);
|
|
458
|
+
}
|
|
459
|
+
|
|
362
460
|
// --- Internal ---
|
|
363
461
|
|
|
364
462
|
_isInstalled(integrationId) {
|
|
@@ -201,9 +201,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
// Write MCP config for agent integrations
|
|
204
|
+
// Write MCP config for agent integrations (command/args only, no secrets)
|
|
205
|
+
// Credentials are injected via process environment below
|
|
206
|
+
let integrationEnv = {};
|
|
205
207
|
if (config.integrations?.length > 0 && this.daemon.integrations) {
|
|
206
208
|
this.daemon.integrations.writeMcpJson(config.integrations);
|
|
209
|
+
integrationEnv = this.daemon.integrations.getSpawnEnv(config.integrations);
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
const { command, args, env } = provider.buildSpawnCommand(spawnConfig);
|
|
@@ -232,7 +235,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
232
235
|
// Spawn the process
|
|
233
236
|
const proc = cpSpawn(command, args, {
|
|
234
237
|
cwd: agent.workingDir || this.daemon.projectDir,
|
|
235
|
-
env: { ...process.env, ...env, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name },
|
|
238
|
+
env: { ...process.env, ...env, ...integrationEnv, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name },
|
|
236
239
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
237
240
|
// Don't let agent process prevent daemon from exiting
|
|
238
241
|
detached: false,
|