groove-dev 0.17.7 → 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 (112) 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 +10 -48
  4. package/node_modules/@groove-dev/daemon/src/api.js +103 -12
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +94 -16
  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/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +182 -0
  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 +157 -42
  24. package/package.json +1 -2
  25. package/packages/daemon/integrations-registry.json +10 -48
  26. package/packages/daemon/src/api.js +103 -12
  27. package/packages/daemon/src/integrations.js +94 -16
  28. package/packages/daemon/src/providers/claude-code.js +4 -0
  29. package/packages/gui/dist/assets/index-DXkccbmd.js +182 -0
  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 +157 -42
  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
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CsymvgNh.js +0 -156
  112. package/packages/gui/dist/assets/index-CsymvgNh.js +0 -156
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
+ }
@@ -99,12 +99,13 @@
99
99
  "transport": "stdio",
100
100
  "command": "npx",
101
101
  "args": ["-y", "@gongrzhe/server-calendar-autoauth-mcp"],
102
- "authType": "none",
102
+ "authType": "google-autoauth",
103
+ "oauthKeysDir": ".calendar-mcp",
103
104
  "envKeys": [],
104
105
  "setupSteps": [
105
- "Click Install, then click 'Sign in with Google'",
106
- "A browser window will openauthorize Groove",
107
- "Done — no API keys or tokens needed"
106
+ "One-time: link your Google Cloud OAuth app (shared across all Google integrations)",
107
+ "Click 'Sign in with Google'a browser opens for authorization",
108
+ "Done — the MCP server handles token refresh automatically"
108
109
  ],
109
110
  "featured": false,
110
111
  "downloads": 0,
@@ -124,12 +125,13 @@
124
125
  "transport": "stdio",
125
126
  "command": "npx",
126
127
  "args": ["-y", "@gongrzhe/server-gmail-autoauth-mcp"],
127
- "authType": "none",
128
+ "authType": "google-autoauth",
129
+ "oauthKeysDir": ".gmail-mcp",
128
130
  "envKeys": [],
129
131
  "setupSteps": [
130
- "Click Install, then click 'Sign in with Google'",
131
- "A browser window will openauthorize Groove",
132
- "Done — no API keys or tokens needed"
132
+ "One-time: link your Google Cloud OAuth app (shared across all Google integrations)",
133
+ "Click 'Sign in with Google'a browser opens for authorization",
134
+ "Done — the MCP server handles token refresh automatically"
133
135
  ],
134
136
  "featured": false,
135
137
  "downloads": 0,
@@ -336,26 +338,6 @@
336
338
  "ratingCount": 0,
337
339
  "verified": "community"
338
340
  },
339
- {
340
- "id": "filesystem",
341
- "name": "Filesystem",
342
- "description": "Read, write, search, and manage files on the local filesystem",
343
- "category": "developer",
344
- "icon": "folder",
345
- "tags": ["files", "filesystem", "local", "storage"],
346
- "roles": ["backend", "fullstack", "devops"],
347
- "npmPackage": "@modelcontextprotocol/server-filesystem",
348
- "transport": "stdio",
349
- "command": "npx",
350
- "args": ["-y", "@modelcontextprotocol/server-filesystem"],
351
- "authType": "none",
352
- "envKeys": [],
353
- "featured": false,
354
- "downloads": 0,
355
- "rating": 0,
356
- "ratingCount": 0,
357
- "verified": "mcp-official"
358
- },
359
341
  {
360
342
  "id": "google-maps",
361
343
  "name": "Google Maps",
@@ -383,25 +365,5 @@
383
365
  "rating": 0,
384
366
  "ratingCount": 0,
385
367
  "verified": "mcp-official"
386
- },
387
- {
388
- "id": "sqlite",
389
- "name": "SQLite",
390
- "description": "Query and manage SQLite databases, inspect schemas, run SQL",
391
- "category": "database",
392
- "icon": "database",
393
- "tags": ["sql", "database", "local", "lightweight"],
394
- "roles": ["analyst", "backend"],
395
- "npmPackage": "mcp-sqlite",
396
- "transport": "stdio",
397
- "command": "npx",
398
- "args": ["-y", "mcp-sqlite"],
399
- "authType": "none",
400
- "envKeys": [],
401
- "featured": false,
402
- "downloads": 0,
403
- "rating": 0,
404
- "ratingCount": 0,
405
- "verified": "mcp-official"
406
368
  }
407
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,8 +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
+
492
+ // For google-autoauth integrations, write the gcp-oauth.keys.json file
493
+ // that the MCP server expects before it can start the OAuth browser flow
494
+ if (entry.authType === 'google-autoauth') {
495
+ console.log(`[Groove:Integrations] Writing gcp-oauth.keys.json for ${integrationId}`);
496
+ this._writeGoogleOAuthKeys(entry);
497
+ }
498
+
470
499
  const command = entry.command || 'npx';
471
500
  const args = entry.args || ['-y', entry.npmPackage];
501
+ console.log(`[Groove:Integrations] Spawning: ${command} ${args.join(' ')}`);
472
502
 
473
503
  // Build env with any configured credentials
474
504
  const env = {};
@@ -485,6 +515,8 @@ export class IntegrationStore {
485
515
  detached: false,
486
516
  });
487
517
 
518
+ console.log(`[Groove:Integrations] Process spawned, PID: ${proc.pid}`);
519
+
488
520
  // Send MCP handshake to initialize the server — this triggers auth
489
521
  const initMsg = JSON.stringify({
490
522
  jsonrpc: '2.0', id: 1, method: 'initialize',
@@ -501,28 +533,38 @@ export class IntegrationStore {
501
533
  jsonrpc: '2.0', method: 'notifications/initialized',
502
534
  });
503
535
 
504
- // Wait a moment for npx to download + start, then send handshake
505
536
  proc.stdout.on('data', (chunk) => {
506
537
  const text = chunk.toString();
538
+ console.log(`[Groove:Integrations] MCP stdout: ${text.slice(0, 200)}`);
507
539
  // After initialize response, send initialized notification + tools/list
508
540
  if (text.includes('"id":1') || text.includes('"id": 1')) {
541
+ console.log('[Groove:Integrations] Got initialize response, sending initialized + tools/list');
509
542
  proc.stdin.write(initializedNotif + '\n');
510
543
  setTimeout(() => proc.stdin.write(listToolsMsg + '\n'), 500);
511
544
  }
512
545
  });
513
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
+
514
556
  // Send initialize after a brief delay for npx startup
515
557
  setTimeout(() => {
516
- 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); }
517
560
  }, 3000);
518
561
 
519
- // Auto-kill after 2 minutes (auth should complete well before that)
562
+ // Auto-kill after 2 minutes
520
563
  const timeout = setTimeout(() => {
564
+ console.log('[Groove:Integrations] Auth timeout — killing process');
521
565
  try { proc.kill('SIGTERM'); } catch { /* ignore */ }
522
566
  }, 120_000);
523
567
 
524
- proc.on('exit', () => clearTimeout(timeout));
525
-
526
568
  this.daemon.audit.log('integration.authenticate', { id: integrationId });
527
569
 
528
570
  return {
@@ -531,6 +573,42 @@ export class IntegrationStore {
531
573
  };
532
574
  }
533
575
 
576
+ /**
577
+ * Write gcp-oauth.keys.json for Google auto-auth MCP servers.
578
+ * These servers need a Google Cloud OAuth client file at a specific path
579
+ * before they can open the browser for user consent.
580
+ */
581
+ _writeGoogleOAuthKeys(entry) {
582
+ const creds = this._getGoogleOAuthCredentials();
583
+ if (!creds) {
584
+ throw new Error('Google OAuth not configured. Set up your Google Cloud credentials first.');
585
+ }
586
+ const { clientId, clientSecret } = creds;
587
+
588
+ const keysContent = JSON.stringify({
589
+ installed: {
590
+ client_id: clientId,
591
+ client_secret: clientSecret,
592
+ auth_uri: 'https://accounts.google.com/o/oauth2/auth',
593
+ token_uri: 'https://oauth2.googleapis.com/token',
594
+ redirect_uris: ['http://localhost'],
595
+ },
596
+ }, null, 2);
597
+
598
+ // Write to the directory the MCP server expects (e.g., ~/.gmail-mcp/)
599
+ const keysDir = entry.oauthKeysDir;
600
+ if (keysDir) {
601
+ const homedir = process.env.HOME || process.env.USERPROFILE || '~';
602
+ const dirPath = resolve(homedir, keysDir);
603
+ mkdirSync(dirPath, { recursive: true });
604
+ const keysPath = resolve(dirPath, 'gcp-oauth.keys.json');
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}`);
609
+ }
610
+ }
611
+
534
612
  // --- Internal ---
535
613
 
536
614
  _isInstalled(integrationId) {
@@ -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
+ }