groove-dev 0.26.38 → 0.27.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 (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CLAUDE.md +24 -19
  3. package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
  6. package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
  7. package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
  8. package/node_modules/@groove-dev/daemon/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
  10. package/node_modules/@groove-dev/daemon/src/api.js +346 -22
  11. package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
  12. package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
  13. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
  14. package/node_modules/@groove-dev/daemon/src/index.js +28 -4
  15. package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
  17. package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
  18. package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
  19. package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
  20. package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
  21. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  22. package/node_modules/@groove-dev/daemon/src/process.js +141 -9
  23. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  24. package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
  25. package/node_modules/@groove-dev/daemon/src/router.js +43 -0
  26. package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
  27. package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
  28. package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
  30. package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
  31. package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
  32. package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
  33. package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
  34. package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
  35. package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
  37. package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
  38. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  39. package/node_modules/@groove-dev/gui/package.json +1 -4
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  49. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  50. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
  51. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
  52. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  53. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  54. package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
  55. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
  56. package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
  57. package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
  58. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
  59. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
  60. package/package.json +2 -8
  61. package/packages/cli/bin/groove.js +2 -0
  62. package/packages/cli/package.json +1 -1
  63. package/packages/cli/src/commands/nuke.js +16 -4
  64. package/packages/cli/src/commands/stop.js +17 -2
  65. package/packages/daemon/integrations-registry.json +681 -75
  66. package/packages/daemon/package.json +1 -1
  67. package/packages/daemon/src/adaptive.js +23 -25
  68. package/packages/daemon/src/api.js +346 -22
  69. package/packages/daemon/src/classifier.js +53 -6
  70. package/packages/daemon/src/firstrun.js +14 -1
  71. package/packages/daemon/src/gateways/manager.js +2 -2
  72. package/packages/daemon/src/index.js +28 -4
  73. package/packages/daemon/src/integrations.js +215 -14
  74. package/packages/daemon/src/introducer.js +84 -11
  75. package/packages/daemon/src/journalist.js +43 -1
  76. package/packages/daemon/src/lockmanager.js +60 -0
  77. package/packages/daemon/src/mcp-manager.js +270 -0
  78. package/packages/daemon/src/memory.js +370 -0
  79. package/packages/daemon/src/pm.js +1 -1
  80. package/packages/daemon/src/process.js +141 -9
  81. package/packages/daemon/src/registry.js +1 -1
  82. package/packages/daemon/src/rotator.js +334 -31
  83. package/packages/daemon/src/router.js +43 -0
  84. package/packages/daemon/src/tokentracker.js +70 -18
  85. package/packages/daemon/src/validate.js +5 -13
  86. package/packages/daemon/templates/groove-slides.cjs +306 -0
  87. package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
  88. package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
  89. package/packages/gui/dist/index.html +2 -2
  90. package/packages/gui/package.json +1 -4
  91. package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
  92. package/packages/gui/src/components/agents/agent-config.jsx +22 -1
  93. package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
  94. package/packages/gui/src/components/agents/agent-node.jsx +132 -90
  95. package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
  96. package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
  97. package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
  98. package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
  99. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  100. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  101. package/packages/gui/src/components/layout/app-shell.jsx +24 -19
  102. package/packages/gui/src/components/layout/command-palette.jsx +2 -2
  103. package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  104. package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  105. package/packages/gui/src/lib/format.js +0 -6
  106. package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
  107. package/packages/gui/src/stores/groove.js +59 -9
  108. package/packages/gui/src/views/agents.jsx +84 -10
  109. package/packages/gui/src/views/dashboard.jsx +24 -21
  110. package/packages/gui/src/views/marketplace.jsx +153 -85
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
  112. package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
  113. package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
  114. package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
  115. package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
  116. package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
  117. package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
  118. package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
  119. package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
  120. package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
  121. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
  122. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
  123. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
  124. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
  125. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
  126. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
  127. package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
  128. package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
  129. package/node_modules/@radix-ui/react-popover/README.md +0 -3
  130. package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
  131. package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
  132. package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
  133. package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
  134. package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
  135. package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
  136. package/node_modules/@radix-ui/react-popover/package.json +0 -82
  137. package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
  138. package/node_modules/@radix-ui/react-separator/README.md +0 -3
  139. package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
  140. package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
  141. package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
  142. package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
  143. package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
  144. package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
  145. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
  146. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
  147. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
  148. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
  149. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
  150. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
  151. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
  152. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
  153. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
  154. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
  155. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
  156. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
  157. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
  158. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
  159. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
  160. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
  161. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
  162. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
  163. package/node_modules/@radix-ui/react-separator/package.json +0 -69
  164. package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
  165. package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
  166. package/packages/gui/dist/groove-logo-short.png +0 -0
  167. package/packages/gui/dist/groove-logo.png +0 -0
  168. package/packages/gui/public/groove-logo-short.png +0 -0
  169. package/packages/gui/public/groove-logo.png +0 -0
  170. package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
  171. package/packages/gui/src/lib/hooks/use-media-query.js +0 -18
@@ -32,8 +32,10 @@ import { IntegrationStore } from './integrations.js';
32
32
  import { Scheduler } from './scheduler.js';
33
33
  import { FileWatcher } from './filewatcher.js';
34
34
  import { TimelineTracker } from './timeline.js';
35
+ import { MemoryStore } from './memory.js';
35
36
  import { TerminalManager } from './terminal-pty.js';
36
37
  import { GatewayManager } from './gateways/manager.js';
38
+ import { McpManager } from './mcp-manager.js';
37
39
  import { ModelManager } from './model-manager.js';
38
40
  import { LlamaServerManager } from './llama-server.js';
39
41
  import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
@@ -110,6 +112,7 @@ export class Daemon {
110
112
  this.registry = new Registry(this.state);
111
113
  this.locks = new LockManager(this.grooveDir);
112
114
  this.tokens = new TokenTracker(this.grooveDir);
115
+ this.memory = new MemoryStore(this.grooveDir);
113
116
  this.timeline = new TimelineTracker(this);
114
117
  this.processes = new ProcessManager(this);
115
118
  this.introducer = new Introducer(this);
@@ -119,7 +122,7 @@ export class Daemon {
119
122
  this.adaptive = new AdaptiveThresholds(this.grooveDir);
120
123
  this.teams = new Teams(this);
121
124
  this.credentials = new CredentialStore(this.grooveDir);
122
- this.classifier = new TaskClassifier();
125
+ this.classifier = new TaskClassifier(this);
123
126
  this.router = new ModelRouter(this);
124
127
  this.pm = new ProjectManager(this);
125
128
  this.indexer = new CodebaseIndexer(this);
@@ -133,6 +136,7 @@ export class Daemon {
133
136
  this.gateways = new GatewayManager(this);
134
137
  this.modelManager = new ModelManager(this);
135
138
  this.llamaServer = new LlamaServerManager(this);
139
+ this.mcpManager = new McpManager(this);
136
140
 
137
141
  // HTTP + WebSocket server
138
142
  this.app = express();
@@ -158,16 +162,35 @@ export class Daemon {
158
162
  // Wire up API routes
159
163
  createApi(this.app, this);
160
164
 
165
+ // Enrich agent list with live quality + efficiency scores for GUI
166
+ const enrichAgents = (agents) => agents.map((a) => {
167
+ const enriched = { ...a };
168
+ try {
169
+ const events = this.classifier.agentWindows[a.id] || [];
170
+ if (events.length >= 6) {
171
+ const signals = this.adaptive.extractSignals(events, a.scope);
172
+ const score = signals ? this.adaptive.scoreSession(signals) : null;
173
+ if (score != null) enriched.qualityScore = score;
174
+ }
175
+ } catch { /* classifier/adaptive may not be ready */ }
176
+ try {
177
+ const td = this.tokens.getAgent(a.id);
178
+ const total = (td.cacheReadTokens || 0) + (td.cacheCreationTokens || 0) + (td.inputTokens || 0);
179
+ if (total > 0) enriched.efficiency = Math.round(((td.cacheReadTokens || 0) / total) * 100);
180
+ } catch { /* token tracker may not have data */ }
181
+ return enriched;
182
+ });
183
+
161
184
  // Broadcast registry changes over WebSocket
162
185
  this.registry.on('change', () => {
163
- this.broadcast({ type: 'state', data: this.registry.getAll() });
186
+ this.broadcast({ type: 'state', data: enrichAgents(this.registry.getAll()) });
164
187
  });
165
188
 
166
189
  // Send full state to new WebSocket clients + handle editor messages
167
190
  this.wss.on('connection', (ws) => {
168
191
  ws.send(JSON.stringify({
169
192
  type: 'state',
170
- data: this.registry.getAll(),
193
+ data: enrichAgents(this.registry.getAll()),
171
194
  }));
172
195
 
173
196
  // Track which files this client is watching (for cleanup on disconnect)
@@ -427,8 +450,9 @@ export class Daemon {
427
450
  this.fileWatcher.unwatchAll();
428
451
  this.terminalManager.killAll();
429
452
 
430
- // Kill all agent processes and stop inference servers
453
+ // Kill all agent processes, stop MCP servers, and stop inference servers
431
454
  await this.processes.killAll();
455
+ this.mcpManager.stopAll();
432
456
  await this.llamaServer.stopAll();
433
457
 
434
458
  // Clean up PID and host files
@@ -2,7 +2,7 @@
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
5
- import { resolve, dirname } from 'path';
5
+ import { resolve, dirname, basename, extname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { execFileSync, spawn as cpSpawn } from 'child_process';
8
8
 
@@ -229,7 +229,30 @@ export class IntegrationStore {
229
229
  }));
230
230
  const configured = envKeys.length === 0 || envKeys.every((ek) => !ek.required || ek.set);
231
231
 
232
- return { id: integrationId, installed, configured, envKeys };
232
+ let authenticated = false;
233
+ if (entry.authType === 'google-autoauth' && entry.oauthKeysDir) {
234
+ const homedir = process.env.HOME || process.env.USERPROFILE || '~';
235
+ authenticated = existsSync(resolve(homedir, entry.oauthKeysDir, 'credentials.json'));
236
+ } else if (entry.authType === 'oauth-google') {
237
+ authenticated = !!this.getCredential(integrationId, 'GOOGLE_REFRESH_TOKEN');
238
+ } else if (entry.authType === 'api-key') {
239
+ authenticated = configured;
240
+ }
241
+
242
+ let needsReauth = false;
243
+ if (authenticated && entry.oauthScopes?.length) {
244
+ const raw = this.getCredential(integrationId, 'GOOGLE_AUTHORIZED_SCOPES');
245
+ if (raw) {
246
+ try {
247
+ const authorized = new Set(JSON.parse(raw));
248
+ needsReauth = entry.oauthScopes.some((s) => !authorized.has(s));
249
+ } catch {}
250
+ } else {
251
+ needsReauth = true;
252
+ }
253
+ }
254
+
255
+ return { id: integrationId, installed, configured, envKeys, authenticated, needsReauth };
233
256
  }
234
257
 
235
258
  /**
@@ -378,7 +401,9 @@ export class IntegrationStore {
378
401
  getOAuthUrl(integrationId) {
379
402
  const entry = this.registry.find((s) => s.id === integrationId);
380
403
  if (!entry) throw new Error(`Integration not found: ${integrationId}`);
381
- if (entry.authType !== 'oauth-google') throw new Error('Integration does not use OAuth');
404
+ if (entry.authType !== 'oauth-google' && entry.authType !== 'google-autoauth') {
405
+ throw new Error('Integration does not use Google OAuth');
406
+ }
382
407
 
383
408
  const creds = this._getGoogleOAuthCredentials();
384
409
  if (!creds) {
@@ -405,15 +430,17 @@ export class IntegrationStore {
405
430
  /**
406
431
  * Handle OAuth callback — exchange code for tokens.
407
432
  */
408
- async handleOAuthCallback(code, integrationId) {
433
+ async handleOAuthCallback(code, stateParam, redirectUri) {
409
434
  const creds = this._getGoogleOAuthCredentials();
410
435
  if (!creds) {
411
436
  throw new Error('Google OAuth credentials not found');
412
437
  }
413
438
  const { clientId, clientSecret } = creds;
414
439
 
415
- const port = this.daemon.port || 31415;
416
- const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
440
+ if (!redirectUri) {
441
+ const port = this.daemon.port || 31415;
442
+ redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
443
+ }
417
444
 
418
445
  const res = await fetch('https://oauth2.googleapis.com/token', {
419
446
  method: 'POST',
@@ -434,16 +461,63 @@ export class IntegrationStore {
434
461
 
435
462
  const tokens = await res.json();
436
463
 
437
- // Store the tokens for this integration
438
- this.setCredential(integrationId, 'GOOGLE_CLIENT_ID', clientId);
439
- this.setCredential(integrationId, 'GOOGLE_CLIENT_SECRET', clientSecret);
440
- if (tokens.refresh_token) {
441
- this.setCredential(integrationId, 'GOOGLE_REFRESH_TOKEN', tokens.refresh_token);
464
+ // State can be a single ID or comma-separated list for combined auth
465
+ const integrationIds = stateParam.split(',').filter(Boolean);
466
+
467
+ for (const integrationId of integrationIds) {
468
+ this.setCredential(integrationId, 'GOOGLE_CLIENT_ID', clientId);
469
+ this.setCredential(integrationId, 'GOOGLE_CLIENT_SECRET', clientSecret);
470
+ if (tokens.refresh_token) {
471
+ this.setCredential(integrationId, 'GOOGLE_REFRESH_TOKEN', tokens.refresh_token);
472
+ }
473
+
474
+ const entry = this.registry.find((s) => s.id === integrationId);
475
+ if (entry?.oauthScopes) {
476
+ this.setCredential(integrationId, 'GOOGLE_AUTHORIZED_SCOPES', JSON.stringify(entry.oauthScopes));
477
+ }
478
+
479
+ if (entry?.authType === 'google-autoauth' && entry.oauthKeysDir && tokens.refresh_token) {
480
+ this._writeAutoauthCredentials(entry, clientId, clientSecret, tokens.refresh_token);
481
+ }
482
+
483
+ this.daemon.audit.log('integration.oauth.complete', { id: integrationId });
442
484
  }
443
485
 
444
- this.daemon.audit.log('integration.oauth.complete', { id: integrationId });
486
+ return { ok: true, integrationIds };
487
+ }
445
488
 
446
- return { ok: true, integrationId };
489
+ /**
490
+ * Build a combined OAuth URL for multiple Google integrations at once.
491
+ * Aggregates scopes and uses comma-separated state.
492
+ */
493
+ getGoogleWorkspaceOAuthUrl(integrationIds) {
494
+ const creds = this._getGoogleOAuthCredentials();
495
+ if (!creds) {
496
+ throw new Error('Google OAuth not configured. Set up your Google Cloud project first.');
497
+ }
498
+
499
+ const allScopes = new Set();
500
+ for (const id of integrationIds) {
501
+ const entry = this.registry.find((s) => s.id === id);
502
+ if (entry?.oauthScopes) {
503
+ for (const scope of entry.oauthScopes) allScopes.add(scope);
504
+ }
505
+ }
506
+
507
+ const port = this.daemon.port || 31415;
508
+ const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
509
+
510
+ const params = new URLSearchParams({
511
+ client_id: creds.clientId,
512
+ redirect_uri: redirectUri,
513
+ response_type: 'code',
514
+ scope: Array.from(allScopes).join(' '),
515
+ access_type: 'offline',
516
+ prompt: 'consent',
517
+ state: integrationIds.join(','),
518
+ });
519
+
520
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
447
521
  }
448
522
 
449
523
  /**
@@ -595,7 +669,6 @@ export class IntegrationStore {
595
669
  },
596
670
  }, null, 2);
597
671
 
598
- // Write to the directory the MCP server expects (e.g., ~/.gmail-mcp/)
599
672
  const keysDir = entry.oauthKeysDir;
600
673
  if (keysDir) {
601
674
  const homedir = process.env.HOME || process.env.USERPROFILE || '~';
@@ -609,6 +682,134 @@ export class IntegrationStore {
609
682
  }
610
683
  }
611
684
 
685
+ /**
686
+ * Write credential files for google-autoauth MCP servers after OAuth completes.
687
+ * Writes both the keys file (client config) and credentials file (refresh token)
688
+ * so the MCP server finds valid auth at runtime without its own browser flow.
689
+ */
690
+ _writeAutoauthCredentials(entry, clientId, clientSecret, refreshToken) {
691
+ const homedir = process.env.HOME || process.env.USERPROFILE || '~';
692
+ const dirPath = resolve(homedir, entry.oauthKeysDir);
693
+ mkdirSync(dirPath, { recursive: true });
694
+
695
+ // Write the OAuth client config (gcp-oauth.keys.json)
696
+ const keysPath = resolve(dirPath, 'gcp-oauth.keys.json');
697
+ writeFileSync(keysPath, JSON.stringify({
698
+ installed: {
699
+ client_id: clientId,
700
+ client_secret: clientSecret,
701
+ auth_uri: 'https://accounts.google.com/o/oauth2/auth',
702
+ token_uri: 'https://oauth2.googleapis.com/token',
703
+ redirect_uris: ['http://localhost'],
704
+ },
705
+ }, null, 2), { mode: 0o600 });
706
+
707
+ // Write the user credentials (credentials.json) in Google authorized_user format
708
+ const credPath = resolve(dirPath, 'credentials.json');
709
+ writeFileSync(credPath, JSON.stringify({
710
+ type: 'authorized_user',
711
+ client_id: clientId,
712
+ client_secret: clientSecret,
713
+ refresh_token: refreshToken,
714
+ }, null, 2), { mode: 0o600 });
715
+
716
+ console.log(`[Groove:Integrations] Wrote OAuth keys + credentials to: ${dirPath}`);
717
+ }
718
+
719
+ // --- Google Drive Upload ---
720
+
721
+ static CONVERT_MAP = {
722
+ '.pptx': { source: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', target: 'application/vnd.google-apps.presentation' },
723
+ '.docx': { source: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', target: 'application/vnd.google-apps.document' },
724
+ '.xlsx': { source: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', target: 'application/vnd.google-apps.spreadsheet' },
725
+ '.csv': { source: 'text/csv', target: 'application/vnd.google-apps.spreadsheet' },
726
+ '.txt': { source: 'text/plain', target: 'application/vnd.google-apps.document' },
727
+ '.html': { source: 'text/html', target: 'application/vnd.google-apps.document' },
728
+ '.pdf': { source: 'application/pdf', target: null },
729
+ };
730
+
731
+ async getGoogleAccessToken() {
732
+ const creds = this._getGoogleOAuthCredentials();
733
+ if (!creds) throw new Error('Google OAuth not configured');
734
+
735
+ const googleIds = ['google-drive', 'google-docs', 'google-sheets', 'google-slides', 'google-calendar', 'gmail'];
736
+ let refreshToken = null;
737
+ for (const id of googleIds) {
738
+ refreshToken = this.getCredential(id, 'GOOGLE_REFRESH_TOKEN');
739
+ if (refreshToken) break;
740
+ }
741
+ if (!refreshToken) throw new Error('No Google refresh token found. Authenticate a Google integration first.');
742
+
743
+ const res = await fetch('https://oauth2.googleapis.com/token', {
744
+ method: 'POST',
745
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
746
+ body: new URLSearchParams({
747
+ grant_type: 'refresh_token',
748
+ refresh_token: refreshToken,
749
+ client_id: creds.clientId,
750
+ client_secret: creds.clientSecret,
751
+ }),
752
+ });
753
+
754
+ if (!res.ok) {
755
+ const err = await res.json().catch(() => ({}));
756
+ throw new Error(`Google token refresh failed: ${err.error_description || err.error || 'unknown'}`);
757
+ }
758
+
759
+ const data = await res.json();
760
+ return data.access_token;
761
+ }
762
+
763
+ async uploadToGoogleDrive(filePath, options = {}) {
764
+ const { name, folderId, convert = true } = options;
765
+
766
+ if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
767
+
768
+ const accessToken = await this.getGoogleAccessToken();
769
+
770
+ const ext = extname(filePath).toLowerCase();
771
+ const fileName = name || basename(filePath);
772
+ const mapping = IntegrationStore.CONVERT_MAP[ext];
773
+ const contentType = mapping?.source || 'application/octet-stream';
774
+
775
+ const metadata = { name: fileName };
776
+ if (convert && mapping?.target) metadata.mimeType = mapping.target;
777
+ if (folderId) metadata.parents = [folderId];
778
+
779
+ const fileContent = readFileSync(filePath);
780
+ const boundary = `groove_upload_${Date.now()}`;
781
+ const metadataJson = JSON.stringify(metadata);
782
+
783
+ const header = Buffer.from(
784
+ `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadataJson}\r\n--${boundary}\r\nContent-Type: ${contentType}\r\n\r\n`
785
+ );
786
+ const footer = Buffer.from(`\r\n--${boundary}--`);
787
+ const body = Buffer.concat([header, fileContent, footer]);
788
+
789
+ const res = await fetch(
790
+ 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,webViewLink,mimeType',
791
+ {
792
+ method: 'POST',
793
+ headers: {
794
+ Authorization: `Bearer ${accessToken}`,
795
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
796
+ },
797
+ body,
798
+ },
799
+ );
800
+
801
+ if (!res.ok) {
802
+ const err = await res.json().catch(() => ({}));
803
+ const msg = err.error?.message || res.statusText;
804
+ if (res.status === 403 && msg.includes('insufficient')) {
805
+ throw new Error(`Google Drive upload failed: insufficient permissions. Re-authenticate Google Drive with write access.`);
806
+ }
807
+ throw new Error(`Google Drive upload failed: ${msg}`);
808
+ }
809
+
810
+ return res.json();
811
+ }
812
+
612
813
  // --- Internal ---
613
814
 
614
815
  _isInstalled(integrationId) {
@@ -84,6 +84,15 @@ export class Introducer {
84
84
  lines.push(` - Expected behavior after the change`);
85
85
  lines.push(` GROOVE will automatically wake the target agent and deliver your request.`);
86
86
  lines.push(`- Check AGENTS_REGISTRY.md for the latest team state.`);
87
+ lines.push('');
88
+ lines.push(`## Daemon Safety (NEVER VIOLATE)`);
89
+ lines.push('');
90
+ lines.push(`You are running inside the Groove daemon. Other agents in other teams are running in parallel. Restarting or killing the daemon destroys ALL of their work.`);
91
+ lines.push(`- NEVER run "groove stop", "groove start", "groove restart", or "groove nuke"`);
92
+ lines.push(`- NEVER kill the daemon process ("kill <pid>", "pkill groove", "killall node")`);
93
+ lines.push(`- NEVER run "./promote.sh", "./promote-local.sh", or any publish/deploy script`);
94
+ lines.push(`- NEVER start long-running dev servers that block process exit (vite dev, npm start, next dev)`);
95
+ lines.push(`If code changes require a daemon restart to take effect, state that in your output so the user can restart manually. Do NOT restart it yourself.`);
87
96
 
88
97
  // User feedback from previous tasks — critical context about what the user
89
98
  // observed and what needs to change. Prevents agents from repeating mistakes.
@@ -98,6 +107,47 @@ export class Introducer {
98
107
  }
99
108
  }
100
109
 
110
+ // Project memory (Layer 7) — accumulated wisdom across all prior rotations.
111
+ // Constraints, recent role handoffs, known error→fix patterns. Total cap ~12K chars.
112
+ if (this.daemon.memory) {
113
+ const constraints = this.daemon.memory.getConstraintsMarkdown(4000);
114
+ const recentChain = this.daemon.memory.getRecentHandoffMarkdown(newAgent.role, 3, 4000);
115
+ const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 20, 4000);
116
+
117
+ if (constraints || recentChain || discoveries) {
118
+ lines.push('');
119
+ lines.push(`## Project Memory`);
120
+ lines.push('');
121
+ lines.push(`This is accumulated knowledge from prior agents working on this project. Read carefully — it will save you from rediscovering what others already learned.`);
122
+
123
+ if (constraints) {
124
+ lines.push('');
125
+ lines.push(`### Constraints`);
126
+ lines.push('');
127
+ lines.push(constraints);
128
+ }
129
+
130
+ if (recentChain) {
131
+ lines.push('');
132
+ lines.push(`### Recent ${newAgent.role} handoffs`);
133
+ lines.push('');
134
+ lines.push(recentChain);
135
+ }
136
+
137
+ if (discoveries) {
138
+ lines.push('');
139
+ lines.push(`### Known patterns (from prior ${newAgent.role} agents)`);
140
+ lines.push('');
141
+ lines.push(discoveries);
142
+ }
143
+
144
+ lines.push('');
145
+ lines.push(`You can contribute to this memory via:`);
146
+ lines.push(`- \`POST /api/memory/discoveries\` — share an error→fix you found`);
147
+ lines.push(`- \`POST /api/memory/constraints\` — declare a project rule you discovered`);
148
+ }
149
+ }
150
+
101
151
  // Project files section — tell the new agent what exists and what to read
102
152
  if (allTeamFiles.length > 0) {
103
153
  lines.push('');
@@ -140,11 +190,21 @@ export class Introducer {
140
190
  lines.push('');
141
191
  lines.push(`## Coordination Protocol`);
142
192
  lines.push('');
143
- lines.push(`Before performing shared/destructive actions (restart server, npm install/build, modify package.json, modify shared config), coordinate with your team:`);
144
- lines.push(`1. Read \`.groove/coordination.md\` to check for active operations`);
145
- lines.push(`2. Write your intent to \`.groove/coordination.md\` (e.g., "backend-1: restarting server")`);
146
- lines.push(`3. Proceed only if no conflicting operations are active`);
147
- lines.push(`4. Clear your entry from \`.groove/coordination.md\` when done`);
193
+ lines.push(`Before performing shared/destructive actions (restart server, npm install/build, modify package.json, modify shared config), declare intent via the GROOVE daemon. Another agent holding the same resource will cause a 423 response — wait and retry.`);
194
+ lines.push('');
195
+ lines.push(`Declare:`);
196
+ lines.push('```');
197
+ lines.push(`POST http://127.0.0.1:31415/api/coordination/declare`);
198
+ lines.push(`{ "agentId": "${newAgent.id}", "operation": "npm install", "resources": ["package.json", "node_modules"] }`);
199
+ lines.push('```');
200
+ lines.push('');
201
+ lines.push(`Complete (always call this when done, even on failure):`);
202
+ lines.push('```');
203
+ lines.push(`POST http://127.0.0.1:31415/api/coordination/complete`);
204
+ lines.push(`{ "agentId": "${newAgent.id}" }`);
205
+ lines.push('```');
206
+ lines.push('');
207
+ lines.push(`Operations auto-expire after 10 minutes to prevent deadlock.`);
148
208
  }
149
209
 
150
210
  // File safety — prevent agents from deleting files they didn't create
@@ -220,26 +280,39 @@ export class Introducer {
220
280
  }
221
281
  }
222
282
 
223
- // Integration context — list MCP tools available to this agent
283
+ // Integration context — inject playbooks for GROOVE exec API
224
284
  if (newAgent.integrations && newAgent.integrations.length > 0 && this.daemon.integrations) {
225
285
  const integrationSections = [];
226
286
  for (const integrationId of newAgent.integrations) {
227
287
  const entry = this.daemon.integrations.registry.find((s) => s.id === integrationId);
228
288
  if (entry) {
229
289
  const configured = this.daemon.integrations._isConfigured(entry);
230
- const status = configured ? 'connected' : 'NOT CONFIGURED — credentials missing';
231
- integrationSections.push(`- **${entry.name}** (${status}): ${entry.description}`);
290
+ if (!configured) {
291
+ integrationSections.push(`- **${entry.name}** — NOT CONFIGURED (credentials missing)`);
292
+ } else if (entry.agentInstructions) {
293
+ integrationSections.push(entry.agentInstructions);
294
+ } else {
295
+ integrationSections.push(`- **${entry.name}**: ${entry.description}\n Exec: \`POST http://localhost:31415/api/integrations/${entry.id}/exec\` with \`{"tool": "...", "params": {...}}\``);
296
+ }
232
297
  }
233
298
  }
234
299
  if (integrationSections.length > 0) {
235
300
  lines.push('');
236
301
  lines.push(`## Integrations (${integrationSections.length} connected)`);
237
302
  lines.push('');
238
- lines.push('You have MCP tools available from these integrations. Use them to interact with external services:');
303
+ lines.push('You have integrations connected via GROOVE. To use them, make HTTP POST requests:');
304
+ lines.push('```');
305
+ lines.push('POST http://localhost:31415/api/integrations/{id}/exec');
306
+ lines.push('Body: {"tool": "tool_name", "params": {...}}');
307
+ lines.push('```');
308
+ lines.push('To discover available tools: `GET http://localhost:31415/api/integrations/{id}/tools`');
239
309
  lines.push('');
240
- lines.push(integrationSections.join('\n'));
310
+ lines.push('**Approval gates:** Some tools require human approval (e.g., sending emails, creating charges).');
311
+ lines.push('If you get a `requiresApproval: true` response with an `approvalId`, tell the user the action');
312
+ lines.push('needs approval in the GROOVE GUI. Do NOT retry until the user confirms it has been approved.');
313
+ lines.push('To retry: include `"approvalId": "<id>"` in your next exec request body.');
241
314
  lines.push('');
242
- 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.');
315
+ lines.push(integrationSections.join('\n\n'));
243
316
  }
244
317
  }
245
318
 
@@ -355,7 +355,40 @@ export class Journalist {
355
355
  }
356
356
  }
357
357
 
358
- async callHeadless(prompt) {
358
+ // Extract usage from stream-json stdout and record to token tracker.
359
+ // Reserved agent IDs (__journalist__, __pm__, etc.) capture overhead
360
+ // separately from user-facing agents. Silent no-op if provider doesn't
361
+ // emit usage data.
362
+ _recordHeadlessUsage(stdout, trackAs, modelId) {
363
+ if (!this.daemon?.tokens || !stdout) return;
364
+ const lines = stdout.split('\n');
365
+ for (const line of lines) {
366
+ try {
367
+ const data = JSON.parse(line);
368
+ if (data.type !== 'result') continue;
369
+ const usage = data.usage;
370
+ if (!usage) continue;
371
+ const inputTokens = usage.input_tokens || 0;
372
+ const outputTokens = usage.output_tokens || 0;
373
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
374
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
375
+ const total = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
376
+ if (total === 0) continue;
377
+ this.daemon.tokens.record(trackAs, {
378
+ tokens: total,
379
+ inputTokens,
380
+ outputTokens,
381
+ cacheReadTokens,
382
+ cacheCreationTokens,
383
+ model: modelId,
384
+ estimatedCostUsd: data.total_cost_usd || 0,
385
+ });
386
+ return;
387
+ } catch { /* skip non-JSON lines */ }
388
+ }
389
+ }
390
+
391
+ async callHeadless(prompt, { trackAs = '__journalist__' } = {}) {
359
392
  // Find the best available provider for headless synthesis
360
393
  // Priority: claude-code (cheapest via Haiku) > gemini > codex > ollama
361
394
  const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
@@ -391,6 +424,7 @@ export class Journalist {
391
424
  proc.on('exit', (code) => {
392
425
  clearTimeout(timer);
393
426
  if (code !== 0) return reject(new Error(`Headless exited with code ${code}`));
427
+ this._recordHeadlessUsage(stdout, trackAs, modelId);
394
428
  // Process stdout same as execFile path below
395
429
  const lines = stdout.split('\n');
396
430
  for (const line of lines) {
@@ -413,6 +447,8 @@ export class Journalist {
413
447
  }, (err, stdout, stderr) => {
414
448
  if (err) return reject(err);
415
449
 
450
+ this._recordHeadlessUsage(stdout, trackAs, modelId);
451
+
416
452
  // Parse stream-json output to extract the result text
417
453
  const lines = stdout.split('\n');
418
454
  for (const line of lines) {
@@ -648,12 +684,18 @@ export class Journalist {
648
684
  .slice(-3)
649
685
  .join('\n');
650
686
 
687
+ // Pull recent rotation history from persistent memory (Layer 7).
688
+ // Gives the new agent causal continuity: what the last 3 agents struggled
689
+ // with, decided, and solved — not just what the current session did.
690
+ const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000) || '';
691
+
651
692
  return [
652
693
  `# Agent Handoff Brief`,
653
694
  ``,
654
695
  `You are continuing the work of **${agent.name}** (role: ${agent.role}).`,
655
696
  `This is a context rotation — the previous session is being replaced to keep context fresh.`,
656
697
  ``,
698
+ recentChain ? `## Rotation History (recent)\n\n${recentChain}\n` : '',
657
699
  `## Your Identity`,
658
700
  `- Role: ${agent.role}`,
659
701
  `- Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
@@ -1,14 +1,25 @@
1
1
  // GROOVE — File Lock Manager
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
+ //
4
+ // Two lock namespaces:
5
+ // 1. File-scope locks (register/release/check) — per-agent glob patterns
6
+ // registered at spawn time to enforce scope ownership
7
+ // 2. Operation locks (declareOperation/completeOperation) — short-lived
8
+ // resource claims for coordinated actions (npm install, server restarts,
9
+ // shared config writes). Auto-expire to prevent deadlock if an agent
10
+ // crashes mid-operation.
3
11
 
4
12
  import { readFileSync, writeFileSync, existsSync } from 'fs';
5
13
  import { resolve } from 'path';
6
14
  import { minimatch } from 'minimatch';
7
15
 
16
+ const DEFAULT_OPERATION_TTL_MS = 10 * 60 * 1000; // 10 minutes
17
+
8
18
  export class LockManager {
9
19
  constructor(grooveDir) {
10
20
  this.path = resolve(grooveDir, 'locks.json');
11
21
  this.locks = new Map(); // agentId -> glob patterns[]
22
+ this.operations = new Map(); // agentId -> { name, resources, acquiredAt, expiresAt }
12
23
  this.load();
13
24
  }
14
25
 
@@ -37,6 +48,7 @@ export class LockManager {
37
48
 
38
49
  release(agentId) {
39
50
  this.locks.delete(agentId);
51
+ this.operations.delete(agentId);
40
52
  this.save();
41
53
  }
42
54
 
@@ -55,4 +67,52 @@ export class LockManager {
55
67
  getAll() {
56
68
  return Object.fromEntries(this.locks);
57
69
  }
70
+
71
+ // --- Operation locks (coordination protocol) ---
72
+
73
+ _expireOperations() {
74
+ const now = Date.now();
75
+ for (const [id, op] of this.operations) {
76
+ if (op.expiresAt <= now) this.operations.delete(id);
77
+ }
78
+ }
79
+
80
+ declareOperation(agentId, operation, resources, ttlMs = DEFAULT_OPERATION_TTL_MS) {
81
+ if (!agentId || !operation || !Array.isArray(resources) || resources.length === 0) {
82
+ return { conflict: false, error: 'agentId, operation, and resources[] required' };
83
+ }
84
+ this._expireOperations();
85
+
86
+ for (const [holderId, op] of this.operations) {
87
+ if (holderId === agentId) continue;
88
+ const overlap = op.resources.find((r) => resources.includes(r));
89
+ if (overlap) {
90
+ return {
91
+ conflict: true,
92
+ owner: holderId,
93
+ operation: op.name,
94
+ resource: overlap,
95
+ expiresAt: op.expiresAt,
96
+ };
97
+ }
98
+ }
99
+
100
+ const now = Date.now();
101
+ this.operations.set(agentId, {
102
+ name: operation,
103
+ resources,
104
+ acquiredAt: now,
105
+ expiresAt: now + ttlMs,
106
+ });
107
+ return { conflict: false };
108
+ }
109
+
110
+ completeOperation(agentId) {
111
+ return this.operations.delete(agentId);
112
+ }
113
+
114
+ getOperations() {
115
+ this._expireOperations();
116
+ return Object.fromEntries(this.operations);
117
+ }
58
118
  }