groove-dev 0.17.5 → 0.17.7

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.
@@ -102,9 +102,9 @@
102
102
  "authType": "none",
103
103
  "envKeys": [],
104
104
  "setupSteps": [
105
- "Just install the server handles Google sign-in automatically",
106
- "On first use, a browser window will open for you to authorize",
107
- "That's it — no API keys or tokens needed"
105
+ "Click Install, then click 'Sign in with Google'",
106
+ "A browser window will open authorize Groove",
107
+ "Done — no API keys or tokens needed"
108
108
  ],
109
109
  "featured": false,
110
110
  "downloads": 0,
@@ -127,9 +127,9 @@
127
127
  "authType": "none",
128
128
  "envKeys": [],
129
129
  "setupSteps": [
130
- "Just install the server handles Google sign-in automatically",
131
- "On first use, a browser window will open for you to authorize",
132
- "That's it — no API keys or tokens needed"
130
+ "Click Install, then click 'Sign in with Google'",
131
+ "A browser window will open authorize Groove",
132
+ "Done — no API keys or tokens needed"
133
133
  ],
134
134
  "featured": false,
135
135
  "downloads": 0,
@@ -506,6 +506,16 @@ export function createApi(app, daemon) {
506
506
 
507
507
  // Parameterized :id routes (after specific routes above)
508
508
 
509
+ app.post('/api/integrations/:id/authenticate', (req, res) => {
510
+ try {
511
+ const handle = daemon.integrations.authenticate(req.params.id);
512
+ res.json({ ok: true, pid: handle.pid });
513
+ // Auto-cleanup tracked by the handle timeout
514
+ } catch (err) {
515
+ res.status(400).json({ error: err.message });
516
+ }
517
+ });
518
+
509
519
  app.post('/api/integrations/:id/install', async (req, res) => {
510
520
  try {
511
521
  const result = await daemon.integrations.install(req.params.id);
@@ -4,7 +4,7 @@
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
5
5
  import { resolve, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
- import { execFileSync } from 'child_process';
7
+ import { execFileSync, spawn as cpSpawn } from 'child_process';
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
 
@@ -457,6 +457,80 @@ export class IntegrationStore {
457
457
  return !!(clientId && clientSecret);
458
458
  }
459
459
 
460
+ /**
461
+ * Pre-authenticate an auto-auth integration by running its MCP server
462
+ * and sending the MCP handshake (initialize + tools/list). This triggers
463
+ * the server's built-in OAuth flow which opens a browser for sign-in.
464
+ * Returns a handle to track the auth process.
465
+ */
466
+ authenticate(integrationId) {
467
+ const entry = this.registry.find((s) => s.id === integrationId);
468
+ if (!entry) throw new Error(`Integration not found: ${integrationId}`);
469
+
470
+ const command = entry.command || 'npx';
471
+ const args = entry.args || ['-y', entry.npmPackage];
472
+
473
+ // Build env with any configured credentials
474
+ const env = {};
475
+ for (const ek of (entry.envKeys || [])) {
476
+ const val = this.getCredential(integrationId, ek.key);
477
+ if (val) env[ek.key] = val;
478
+ }
479
+
480
+ // Spawn the MCP server with stdin/stdout for JSON-RPC,
481
+ // stderr inherited so it can open browsers and show auth prompts
482
+ const proc = cpSpawn(command, args, {
483
+ env: { ...process.env, ...env },
484
+ stdio: ['pipe', 'pipe', 'inherit'],
485
+ detached: false,
486
+ });
487
+
488
+ // Send MCP handshake to initialize the server — this triggers auth
489
+ const initMsg = JSON.stringify({
490
+ jsonrpc: '2.0', id: 1, method: 'initialize',
491
+ params: {
492
+ protocolVersion: '2024-11-05',
493
+ capabilities: {},
494
+ clientInfo: { name: 'groove', version: '1.0.0' },
495
+ },
496
+ });
497
+ const listToolsMsg = JSON.stringify({
498
+ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {},
499
+ });
500
+ const initializedNotif = JSON.stringify({
501
+ jsonrpc: '2.0', method: 'notifications/initialized',
502
+ });
503
+
504
+ // Wait a moment for npx to download + start, then send handshake
505
+ proc.stdout.on('data', (chunk) => {
506
+ const text = chunk.toString();
507
+ // After initialize response, send initialized notification + tools/list
508
+ if (text.includes('"id":1') || text.includes('"id": 1')) {
509
+ proc.stdin.write(initializedNotif + '\n');
510
+ setTimeout(() => proc.stdin.write(listToolsMsg + '\n'), 500);
511
+ }
512
+ });
513
+
514
+ // Send initialize after a brief delay for npx startup
515
+ setTimeout(() => {
516
+ try { proc.stdin.write(initMsg + '\n'); } catch { /* process may have exited */ }
517
+ }, 3000);
518
+
519
+ // Auto-kill after 2 minutes (auth should complete well before that)
520
+ const timeout = setTimeout(() => {
521
+ try { proc.kill('SIGTERM'); } catch { /* ignore */ }
522
+ }, 120_000);
523
+
524
+ proc.on('exit', () => clearTimeout(timeout));
525
+
526
+ this.daemon.audit.log('integration.authenticate', { id: integrationId });
527
+
528
+ return {
529
+ pid: proc.pid,
530
+ kill: () => { clearTimeout(timeout); try { proc.kill('SIGTERM'); } catch { /* ignore */ } },
531
+ };
532
+ }
533
+
460
534
  // --- Internal ---
461
535
 
462
536
  _isInstalled(integrationId) {