groove-dev 0.17.8 → 0.18.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 (110) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
  3. package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
  4. package/node_modules/@groove-dev/daemon/src/api.js +103 -12
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
  6. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
  7. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
  8. package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
  9. package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
  10. package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
  11. package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
  12. package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
  13. package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
  14. package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
  15. package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
  16. package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
  17. package/{packages/gui/dist/assets/index-D5dtDQf0.js → node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js} +76 -50
  18. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  19. package/node_modules/@groove-dev/gui/src/App.jsx +27 -4
  20. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +1 -1
  21. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +839 -586
  22. package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
  23. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
  24. package/package.json +1 -2
  25. package/packages/daemon/integrations-registry.json +0 -40
  26. package/packages/daemon/src/api.js +103 -12
  27. package/packages/daemon/src/integrations.js +59 -20
  28. package/packages/daemon/src/providers/claude-code.js +4 -0
  29. package/{node_modules/@groove-dev/gui/dist/assets/index-D5dtDQf0.js → packages/gui/dist/assets/index-DXkccbmd.js} +76 -50
  30. package/packages/gui/dist/index.html +1 -1
  31. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
  32. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
  33. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
  34. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
  35. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
  36. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
  37. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
  38. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
  39. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
  40. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
  41. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
  42. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
  43. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
  44. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
  45. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
  46. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
  47. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
  48. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
  49. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
  50. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
  51. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
  52. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
  53. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
  54. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
  55. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
  56. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
  57. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
  58. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
  59. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
  60. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
  61. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
  62. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
  63. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
  64. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
  65. package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
  66. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
  67. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
  68. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
  69. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
  70. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
  71. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
  72. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
  73. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
  74. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
  75. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
  76. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
  77. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
  78. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  79. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  80. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
  81. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
  82. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
  83. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
  84. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
  85. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
  86. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
  87. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
  88. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
  89. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
  90. package/packages/gui/node_modules/.vite/deps/package.json +3 -0
  91. package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
  92. package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
  93. package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
  94. package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  95. package/packages/gui/node_modules/.vite/deps/react.js +5 -0
  96. package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
  97. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  98. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  99. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  100. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  101. package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
  102. package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
  103. package/packages/gui/src/App.jsx +27 -4
  104. package/packages/gui/src/components/AgentChat.jsx +1 -1
  105. package/packages/gui/src/components/SpawnPanel.jsx +839 -586
  106. package/packages/gui/src/views/FileEditor.jsx +85 -1
  107. package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
  108. package/docs/FILE-EDITOR-PLAN.md +0 -253
  109. package/docs/GUI_DESIGN_SPEC.md +0 -402
  110. package/docs/SKILLS-API-SPEC.md +0 -277
package/CLAUDE.md CHANGED
@@ -195,10 +195,3 @@ Fully functional multi-agent orchestration system. Tested end-to-end: planner
195
195
  - Remote access (--host 0.0.0.0 + token auth)
196
196
  - Semantic degradation detection
197
197
  - Distribution: demo video, HN launch, Twitter content
198
-
199
- <!-- GROOVE:START -->
200
- ## GROOVE Orchestration (auto-injected)
201
- Active agents: 0
202
- See AGENTS_REGISTRY.md for full agent state.
203
- **Memory policy:** Ignore auto-memory. Do not read or write MEMORY.md. GROOVE manages all context.
204
- <!-- GROOVE:END -->
@@ -0,0 +1,5 @@
1
+ {
2
+ "client_id": "",
3
+ "client_secret": "",
4
+ "_comment": "Fill in with your Google Cloud OAuth Desktop App credentials. One-time setup — all Google integrations (Gmail, Calendar, Drive) use these."
5
+ }
@@ -338,26 +338,6 @@
338
338
  "ratingCount": 0,
339
339
  "verified": "community"
340
340
  },
341
- {
342
- "id": "filesystem",
343
- "name": "Filesystem",
344
- "description": "Read, write, search, and manage files on the local filesystem",
345
- "category": "developer",
346
- "icon": "folder",
347
- "tags": ["files", "filesystem", "local", "storage"],
348
- "roles": ["backend", "fullstack", "devops"],
349
- "npmPackage": "@modelcontextprotocol/server-filesystem",
350
- "transport": "stdio",
351
- "command": "npx",
352
- "args": ["-y", "@modelcontextprotocol/server-filesystem"],
353
- "authType": "none",
354
- "envKeys": [],
355
- "featured": false,
356
- "downloads": 0,
357
- "rating": 0,
358
- "ratingCount": 0,
359
- "verified": "mcp-official"
360
- },
361
341
  {
362
342
  "id": "google-maps",
363
343
  "name": "Google Maps",
@@ -385,25 +365,5 @@
385
365
  "rating": 0,
386
366
  "ratingCount": 0,
387
367
  "verified": "mcp-official"
388
- },
389
- {
390
- "id": "sqlite",
391
- "name": "SQLite",
392
- "description": "Query and manage SQLite databases, inspect schemas, run SQL",
393
- "category": "database",
394
- "icon": "database",
395
- "tags": ["sql", "database", "local", "lightweight"],
396
- "roles": ["analyst", "backend"],
397
- "npmPackage": "mcp-sqlite",
398
- "transport": "stdio",
399
- "command": "npx",
400
- "args": ["-y", "mcp-sqlite"],
401
- "authType": "none",
402
- "envKeys": [],
403
- "featured": false,
404
- "downloads": 0,
405
- "rating": 0,
406
- "ratingCount": 0,
407
- "verified": "mcp-official"
408
368
  }
409
369
  ]
@@ -5,8 +5,9 @@ import express from 'express';
5
5
  import { resolve, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
8
+ import { spawn as cpSpawn } from 'child_process';
8
9
  import { lookup as mimeLookup } from './mimetypes.js';
9
- import { listProviders } from './providers/index.js';
10
+ import { listProviders, getProvider } from './providers/index.js';
10
11
  import { validateAgentConfig } from './validate.js';
11
12
 
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -351,6 +352,67 @@ export function createApi(app, daemon) {
351
352
  });
352
353
  });
353
354
 
355
+ // Plan chat — direct API (fast, sub-second) when API key available, CLI fallback otherwise
356
+ const PLAN_SYSTEM = `You are the planning assistant built into Groove's spawn panel. The user is configuring an AI agent right now — they're looking at a form with role selection, file scope, skills, integrations, effort level, and a task prompt. Your conversation will be synthesized into the agent's task prompt when they click "Generate Prompt."
357
+
358
+ Your job: help them think through what the agent should do, then craft a clear plan. Be direct and practical. Don't ask how they'll feed input to agents or what tools to use — they're already inside Groove doing it. Focus on the TASK itself.
359
+
360
+ What you know about the system:
361
+ - The user is in the spawn panel, configuring an agent before launching it
362
+ - The left panel has: role picker, directory, permissions, effort, integrations, skills, schedule
363
+ - When done planning, "Generate Prompt" synthesizes this chat into the agent's task prompt
364
+ - Agents are Claude Code instances with full terminal/file access in the specified directory
365
+ - Agents can read/write files, run commands, use MCP integrations (Slack, GitHub, etc.)
366
+ - The journalist system prevents cold starts during context rotation — agents don't lose context
367
+
368
+ Keep responses concise. Help them think, don't lecture them about the system they built.`;
369
+
370
+ app.post('/api/journalist/query', async (req, res) => {
371
+ try {
372
+ const { prompt } = req.body || {};
373
+ if (!prompt) return res.status(400).json({ error: 'prompt is required' });
374
+
375
+ // Fast path: direct Anthropic API call (sub-second)
376
+ const apiKey = daemon.credentials.getKey('anthropic-api');
377
+ if (apiKey) {
378
+ const apiRes = await fetch('https://api.anthropic.com/v1/messages', {
379
+ method: 'POST',
380
+ headers: {
381
+ 'Content-Type': 'application/json',
382
+ 'x-api-key': apiKey,
383
+ 'anthropic-version': '2023-06-01',
384
+ },
385
+ body: JSON.stringify({
386
+ model: 'claude-haiku-4-5-20251001',
387
+ max_tokens: 1024,
388
+ system: PLAN_SYSTEM,
389
+ messages: [{ role: 'user', content: prompt }],
390
+ }),
391
+ });
392
+ const data = await apiRes.json();
393
+ if (data.content?.[0]?.text) {
394
+ return res.json({ response: data.content[0].text, mode: 'fast' });
395
+ }
396
+ if (data.error) {
397
+ return res.status(400).json({ error: data.error.message || 'API error' });
398
+ }
399
+ }
400
+
401
+ // Slow path: CLI fallback for subscription auth (~10s)
402
+ const fullPrompt = `${PLAN_SYSTEM}\n\n${prompt}`;
403
+ const response = await daemon.journalist.callHeadless(fullPrompt);
404
+ res.json({ response, mode: 'cli' });
405
+ } catch (err) {
406
+ res.status(500).json({ error: err.message });
407
+ }
408
+ });
409
+
410
+ // Check if Anthropic API key is configured
411
+ app.get('/api/anthropic-key/status', (req, res) => {
412
+ const hasKey = !!daemon.credentials.getKey('anthropic-api');
413
+ res.json({ configured: hasKey });
414
+ });
415
+
354
416
  // Trigger journalist cycle manually
355
417
  app.post('/api/journalist/cycle', async (req, res) => {
356
418
  try {
@@ -507,11 +569,13 @@ export function createApi(app, daemon) {
507
569
  // Parameterized :id routes (after specific routes above)
508
570
 
509
571
  app.post('/api/integrations/:id/authenticate', (req, res) => {
572
+ console.log(`[Groove:API] POST /api/integrations/${req.params.id}/authenticate`);
510
573
  try {
511
574
  const handle = daemon.integrations.authenticate(req.params.id);
575
+ console.log(`[Groove:API] Authenticate started, PID: ${handle.pid}`);
512
576
  res.json({ ok: true, pid: handle.pid });
513
- // Auto-cleanup tracked by the handle timeout
514
577
  } catch (err) {
578
+ console.log(`[Groove:API] Authenticate error: ${err.message}`);
515
579
  res.status(400).json({ error: err.message });
516
580
  }
517
581
  });
@@ -740,6 +804,11 @@ export function createApi(app, daemon) {
740
804
 
741
805
  const IGNORED_NAMES = new Set(['.git', 'node_modules', '.DS_Store', '.groove', '__pycache__', '.next', '.cache', 'dist', 'coverage']);
742
806
 
807
+ // Editor root directory — defaults to projectDir but can be changed at runtime
808
+ let editorRootDir = daemon.projectDir;
809
+
810
+ function getEditorRoot() { return editorRootDir; }
811
+
743
812
  function validateFilePath(relPath, projectDir) {
744
813
  if (!relPath || typeof relPath !== 'string') return { error: 'path is required' };
745
814
  if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
@@ -750,6 +819,27 @@ export function createApi(app, daemon) {
750
819
  return { fullPath };
751
820
  }
752
821
 
822
+ // Get/set the editor working directory
823
+ app.get('/api/files/root', (req, res) => {
824
+ res.json({ root: editorRootDir });
825
+ });
826
+
827
+ app.post('/api/files/root', (req, res) => {
828
+ const { root } = req.body || {};
829
+ if (!root || typeof root !== 'string') return res.status(400).json({ error: 'root path is required' });
830
+ // Must be absolute and exist
831
+ if (!root.startsWith('/')) return res.status(400).json({ error: 'root must be an absolute path' });
832
+ if (root.includes('\0') || root.includes('..')) return res.status(400).json({ error: 'Invalid path' });
833
+ if (!existsSync(root)) return res.status(404).json({ error: 'Directory not found' });
834
+ try {
835
+ const stat = statSync(root);
836
+ if (!stat.isDirectory()) return res.status(400).json({ error: 'Path is not a directory' });
837
+ } catch { return res.status(400).json({ error: 'Cannot access directory' }); }
838
+ editorRootDir = root;
839
+ daemon.audit.log('editor.root.set', { root });
840
+ res.json({ ok: true, root: editorRootDir });
841
+ });
842
+
753
843
  // File tree — returns dirs + files for a given path
754
844
  app.get('/api/files/tree', (req, res) => {
755
845
  const relPath = req.query.path || '';
@@ -759,8 +849,9 @@ export function createApi(app, daemon) {
759
849
  return res.status(400).json({ error: 'Invalid path' });
760
850
  }
761
851
 
762
- const fullPath = relPath ? resolve(daemon.projectDir, relPath) : daemon.projectDir;
763
- if (!fullPath.startsWith(daemon.projectDir)) {
852
+ const rootDir = getEditorRoot();
853
+ const fullPath = relPath ? resolve(rootDir, relPath) : rootDir;
854
+ if (!fullPath.startsWith(rootDir)) {
764
855
  return res.status(400).json({ error: 'Path outside project' });
765
856
  }
766
857
  if (!existsSync(fullPath)) {
@@ -810,7 +901,7 @@ export function createApi(app, daemon) {
810
901
 
811
902
  // Read file contents
812
903
  app.get('/api/files/read', (req, res) => {
813
- const result = validateFilePath(req.query.path, daemon.projectDir);
904
+ const result = validateFilePath(req.query.path, getEditorRoot());
814
905
  if (result.error) return res.status(400).json({ error: result.error });
815
906
 
816
907
  if (!existsSync(result.fullPath)) {
@@ -846,7 +937,7 @@ export function createApi(app, daemon) {
846
937
  // Write file contents
847
938
  app.post('/api/files/write', (req, res) => {
848
939
  const { path: relPath, content } = req.body;
849
- const result = validateFilePath(relPath, daemon.projectDir);
940
+ const result = validateFilePath(relPath, getEditorRoot());
850
941
  if (result.error) return res.status(400).json({ error: result.error });
851
942
 
852
943
  if (typeof content !== 'string') {
@@ -868,7 +959,7 @@ export function createApi(app, daemon) {
868
959
  // Create a new file
869
960
  app.post('/api/files/create', (req, res) => {
870
961
  const { path: relPath, content = '' } = req.body;
871
- const result = validateFilePath(relPath, daemon.projectDir);
962
+ const result = validateFilePath(relPath, getEditorRoot());
872
963
  if (result.error) return res.status(400).json({ error: result.error });
873
964
 
874
965
  if (existsSync(result.fullPath)) {
@@ -893,7 +984,7 @@ export function createApi(app, daemon) {
893
984
  // Create a new directory
894
985
  app.post('/api/files/mkdir', (req, res) => {
895
986
  const { path: relPath } = req.body;
896
- const result = validateFilePath(relPath, daemon.projectDir);
987
+ const result = validateFilePath(relPath, getEditorRoot());
897
988
  if (result.error) return res.status(400).json({ error: result.error });
898
989
 
899
990
  if (existsSync(result.fullPath)) {
@@ -912,7 +1003,7 @@ export function createApi(app, daemon) {
912
1003
  // Delete a file or directory
913
1004
  app.delete('/api/files/delete', (req, res) => {
914
1005
  const relPath = req.query.path || req.body?.path;
915
- const result = validateFilePath(relPath, daemon.projectDir);
1006
+ const result = validateFilePath(relPath, getEditorRoot());
916
1007
  if (result.error) return res.status(400).json({ error: result.error });
917
1008
 
918
1009
  if (!existsSync(result.fullPath)) {
@@ -936,9 +1027,9 @@ export function createApi(app, daemon) {
936
1027
  // Rename / move a file or directory
937
1028
  app.post('/api/files/rename', (req, res) => {
938
1029
  const { oldPath, newPath } = req.body;
939
- const oldResult = validateFilePath(oldPath, daemon.projectDir);
1030
+ const oldResult = validateFilePath(oldPath, getEditorRoot());
940
1031
  if (oldResult.error) return res.status(400).json({ error: oldResult.error });
941
- const newResult = validateFilePath(newPath, daemon.projectDir);
1032
+ const newResult = validateFilePath(newPath, getEditorRoot());
942
1033
  if (newResult.error) return res.status(400).json({ error: newResult.error });
943
1034
 
944
1035
  if (!existsSync(oldResult.fullPath)) {
@@ -962,7 +1053,7 @@ export function createApi(app, daemon) {
962
1053
 
963
1054
  // Serve raw file (images, video, etc.)
964
1055
  app.get('/api/files/raw', (req, res) => {
965
- const result = validateFilePath(req.query.path, daemon.projectDir);
1056
+ const result = validateFilePath(req.query.path, getEditorRoot());
966
1057
  if (result.error) return res.status(400).json({ error: result.error });
967
1058
 
968
1059
  if (!existsSync(result.fullPath)) {
@@ -380,10 +380,8 @@ export class IntegrationStore {
380
380
  if (!entry) throw new Error(`Integration not found: ${integrationId}`);
381
381
  if (entry.authType !== 'oauth-google') throw new Error('Integration does not use OAuth');
382
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) {
383
+ const creds = this._getGoogleOAuthCredentials();
384
+ if (!creds) {
387
385
  throw new Error('Google OAuth not configured. Set up your Google Cloud project first.');
388
386
  }
389
387
 
@@ -392,7 +390,7 @@ export class IntegrationStore {
392
390
  const scopes = entry.oauthScopes || [];
393
391
 
394
392
  const params = new URLSearchParams({
395
- client_id: clientId,
393
+ client_id: creds.clientId,
396
394
  redirect_uri: redirectUri,
397
395
  response_type: 'code',
398
396
  scope: scopes.join(' '),
@@ -408,11 +406,11 @@ export class IntegrationStore {
408
406
  * Handle OAuth callback — exchange code for tokens.
409
407
  */
410
408
  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) {
409
+ const creds = this._getGoogleOAuthCredentials();
410
+ if (!creds) {
414
411
  throw new Error('Google OAuth credentials not found');
415
412
  }
413
+ const { clientId, clientSecret } = creds;
416
414
 
417
415
  const port = this.daemon.port || 31415;
418
416
  const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
@@ -451,10 +449,32 @@ export class IntegrationStore {
451
449
  /**
452
450
  * Check if Google OAuth is configured (user has set up their Cloud project).
453
451
  */
452
+ /**
453
+ * Get Google OAuth credentials from user config OR bundled defaults.
454
+ * User-configured credentials take priority over bundled defaults.
455
+ */
456
+ _getGoogleOAuthCredentials() {
457
+ // 1. Check user-configured credentials (encrypted store)
458
+ let clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
459
+ let clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
460
+ if (clientId && clientSecret) return { clientId, clientSecret, source: 'user' };
461
+
462
+ // 2. Check bundled defaults (shipped with Groove)
463
+ try {
464
+ const defaultsPath = resolve(__dirname, '../google-oauth.json');
465
+ if (existsSync(defaultsPath)) {
466
+ const defaults = JSON.parse(readFileSync(defaultsPath, 'utf8'));
467
+ if (defaults.client_id && defaults.client_secret) {
468
+ return { clientId: defaults.client_id, clientSecret: defaults.client_secret, source: 'bundled' };
469
+ }
470
+ }
471
+ } catch { /* no bundled defaults */ }
472
+
473
+ return null;
474
+ }
475
+
454
476
  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);
477
+ return !!this._getGoogleOAuthCredentials();
458
478
  }
459
479
 
460
480
  /**
@@ -467,14 +487,18 @@ export class IntegrationStore {
467
487
  const entry = this.registry.find((s) => s.id === integrationId);
468
488
  if (!entry) throw new Error(`Integration not found: ${integrationId}`);
469
489
 
490
+ console.log(`[Groove:Integrations] authenticate(${integrationId}) — authType: ${entry.authType}`);
491
+
470
492
  // For google-autoauth integrations, write the gcp-oauth.keys.json file
471
493
  // that the MCP server expects before it can start the OAuth browser flow
472
494
  if (entry.authType === 'google-autoauth') {
495
+ console.log(`[Groove:Integrations] Writing gcp-oauth.keys.json for ${integrationId}`);
473
496
  this._writeGoogleOAuthKeys(entry);
474
497
  }
475
498
 
476
499
  const command = entry.command || 'npx';
477
500
  const args = entry.args || ['-y', entry.npmPackage];
501
+ console.log(`[Groove:Integrations] Spawning: ${command} ${args.join(' ')}`);
478
502
 
479
503
  // Build env with any configured credentials
480
504
  const env = {};
@@ -491,6 +515,8 @@ export class IntegrationStore {
491
515
  detached: false,
492
516
  });
493
517
 
518
+ console.log(`[Groove:Integrations] Process spawned, PID: ${proc.pid}`);
519
+
494
520
  // Send MCP handshake to initialize the server — this triggers auth
495
521
  const initMsg = JSON.stringify({
496
522
  jsonrpc: '2.0', id: 1, method: 'initialize',
@@ -507,28 +533,38 @@ export class IntegrationStore {
507
533
  jsonrpc: '2.0', method: 'notifications/initialized',
508
534
  });
509
535
 
510
- // Wait a moment for npx to download + start, then send handshake
511
536
  proc.stdout.on('data', (chunk) => {
512
537
  const text = chunk.toString();
538
+ console.log(`[Groove:Integrations] MCP stdout: ${text.slice(0, 200)}`);
513
539
  // After initialize response, send initialized notification + tools/list
514
540
  if (text.includes('"id":1') || text.includes('"id": 1')) {
541
+ console.log('[Groove:Integrations] Got initialize response, sending initialized + tools/list');
515
542
  proc.stdin.write(initializedNotif + '\n');
516
543
  setTimeout(() => proc.stdin.write(listToolsMsg + '\n'), 500);
517
544
  }
518
545
  });
519
546
 
547
+ proc.on('error', (err) => {
548
+ console.log(`[Groove:Integrations] Process error: ${err.message}`);
549
+ });
550
+
551
+ proc.on('exit', (code, signal) => {
552
+ console.log(`[Groove:Integrations] Process exited: code=${code} signal=${signal}`);
553
+ clearTimeout(timeout);
554
+ });
555
+
520
556
  // Send initialize after a brief delay for npx startup
521
557
  setTimeout(() => {
522
- try { proc.stdin.write(initMsg + '\n'); } catch { /* process may have exited */ }
558
+ console.log('[Groove:Integrations] Sending MCP initialize message');
559
+ try { proc.stdin.write(initMsg + '\n'); } catch (e) { console.log('[Groove:Integrations] stdin write failed:', e.message); }
523
560
  }, 3000);
524
561
 
525
- // Auto-kill after 2 minutes (auth should complete well before that)
562
+ // Auto-kill after 2 minutes
526
563
  const timeout = setTimeout(() => {
564
+ console.log('[Groove:Integrations] Auth timeout — killing process');
527
565
  try { proc.kill('SIGTERM'); } catch { /* ignore */ }
528
566
  }, 120_000);
529
567
 
530
- proc.on('exit', () => clearTimeout(timeout));
531
-
532
568
  this.daemon.audit.log('integration.authenticate', { id: integrationId });
533
569
 
534
570
  return {
@@ -543,11 +579,11 @@ export class IntegrationStore {
543
579
  * before they can open the browser for user consent.
544
580
  */
545
581
  _writeGoogleOAuthKeys(entry) {
546
- const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
547
- const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
548
- if (!clientId || !clientSecret) {
549
- throw new Error('Google OAuth not configured. Click "Sign in with Google" to set up your Google Cloud credentials first.');
582
+ const creds = this._getGoogleOAuthCredentials();
583
+ if (!creds) {
584
+ throw new Error('Google OAuth not configured. Set up your Google Cloud credentials first.');
550
585
  }
586
+ const { clientId, clientSecret } = creds;
551
587
 
552
588
  const keysContent = JSON.stringify({
553
589
  installed: {
@@ -567,6 +603,9 @@ export class IntegrationStore {
567
603
  mkdirSync(dirPath, { recursive: true });
568
604
  const keysPath = resolve(dirPath, 'gcp-oauth.keys.json');
569
605
  writeFileSync(keysPath, keysContent, { mode: 0o600 });
606
+ console.log(`[Groove:Integrations] Wrote OAuth keys to: ${keysPath}`);
607
+ } else {
608
+ console.log(`[Groove:Integrations] WARNING: No oauthKeysDir for ${entry.id}`);
570
609
  }
571
610
  }
572
611
 
@@ -48,6 +48,10 @@ export class ClaudeCodeProvider extends Provider {
48
48
  args.push('--model', agent.model);
49
49
  }
50
50
 
51
+ if (agent.effort) {
52
+ args.push('--effort', agent.effort);
53
+ }
54
+
51
55
  // Pass the initial prompt as positional arg (includes GROOVE context)
52
56
  const fullPrompt = this.buildFullPrompt(agent);
53
57
  if (fullPrompt) {
@@ -0,0 +1,64 @@
1
+ {
2
+ "projectName": "gui",
3
+ "scannedAt": "2026-04-08T01:26:36.485Z",
4
+ "stats": {
5
+ "totalFiles": 32,
6
+ "totalDirs": 5,
7
+ "treeDepth": 4
8
+ },
9
+ "workspaces": [],
10
+ "keyFiles": [
11
+ "package.json"
12
+ ],
13
+ "tree": [
14
+ {
15
+ "path": ".",
16
+ "depth": 0,
17
+ "dirs": 2,
18
+ "files": 3,
19
+ "children": [
20
+ "public",
21
+ "src"
22
+ ]
23
+ },
24
+ {
25
+ "path": "public",
26
+ "depth": 1,
27
+ "dirs": 0,
28
+ "files": 3,
29
+ "children": []
30
+ },
31
+ {
32
+ "path": "src",
33
+ "depth": 1,
34
+ "dirs": 3,
35
+ "files": 3,
36
+ "children": [
37
+ "components",
38
+ "stores",
39
+ "views"
40
+ ]
41
+ },
42
+ {
43
+ "path": "src/components",
44
+ "depth": 2,
45
+ "dirs": 0,
46
+ "files": 15,
47
+ "children": []
48
+ },
49
+ {
50
+ "path": "src/stores",
51
+ "depth": 2,
52
+ "dirs": 0,
53
+ "files": 1,
54
+ "children": []
55
+ },
56
+ {
57
+ "path": "src/views",
58
+ "depth": 2,
59
+ "dirs": 0,
60
+ "files": 7,
61
+ "children": []
62
+ }
63
+ ]
64
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "port": 31415,
4
+ "journalistInterval": 120,
5
+ "rotationThreshold": 0.75,
6
+ "autoRotation": true,
7
+ "qcThreshold": 4,
8
+ "maxAgents": 10,
9
+ "defaultProvider": "claude-code"
10
+ }
@@ -0,0 +1,5 @@
1
+ # GROOVE Coordination
2
+
3
+ *Agents write their intent here before shared/destructive actions.*
4
+
5
+ <!-- No active operations -->
@@ -0,0 +1 @@
1
+ 127.0.0.1
@@ -0,0 +1,3 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MC4CAQAwBQYDK2VwBCIEIDgv9qJFjRft4Jh4sUoIe1GtmtckzOSsVvbi8WNRdSwf
3
+ -----END PRIVATE KEY-----
@@ -0,0 +1,3 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MCowBQYDK2VwAyEAHZ3D0IChsPmavZorg3hwhdMZN4W3Q0geH9TR5NVMnI0=
3
+ -----END PUBLIC KEY-----
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "groove-integrations",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "MCP server packages managed by Groove"
6
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "agents": []
3
+ }