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.
- package/CHANGELOG.md +59 -0
- package/CLAUDE.md +24 -19
- package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
- package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
- package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
- package/node_modules/@groove-dev/daemon/src/api.js +346 -22
- package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
- package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
- package/node_modules/@groove-dev/daemon/src/index.js +28 -4
- package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
- package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
- package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
- package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
- package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
- package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +141 -9
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
- package/node_modules/@groove-dev/daemon/src/router.js +43 -0
- package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
- package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
- package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
- package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
- package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
- package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
- package/package.json +2 -8
- package/packages/cli/bin/groove.js +2 -0
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/nuke.js +16 -4
- package/packages/cli/src/commands/stop.js +17 -2
- package/packages/daemon/integrations-registry.json +681 -75
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/adaptive.js +23 -25
- package/packages/daemon/src/api.js +346 -22
- package/packages/daemon/src/classifier.js +53 -6
- package/packages/daemon/src/firstrun.js +14 -1
- package/packages/daemon/src/gateways/manager.js +2 -2
- package/packages/daemon/src/index.js +28 -4
- package/packages/daemon/src/integrations.js +215 -14
- package/packages/daemon/src/introducer.js +84 -11
- package/packages/daemon/src/journalist.js +43 -1
- package/packages/daemon/src/lockmanager.js +60 -0
- package/packages/daemon/src/mcp-manager.js +270 -0
- package/packages/daemon/src/memory.js +370 -0
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +141 -9
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/rotator.js +334 -31
- package/packages/daemon/src/router.js +43 -0
- package/packages/daemon/src/tokentracker.js +70 -18
- package/packages/daemon/src/validate.js +5 -13
- package/packages/daemon/templates/groove-slides.cjs +306 -0
- package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -4
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
- package/packages/gui/src/components/agents/agent-config.jsx +22 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
- package/packages/gui/src/components/agents/agent-node.jsx +132 -90
- package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
- package/packages/gui/src/components/layout/app-shell.jsx +24 -19
- package/packages/gui/src/components/layout/command-palette.jsx +2 -2
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/packages/gui/src/lib/format.js +0 -6
- package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/packages/gui/src/stores/groove.js +59 -9
- package/packages/gui/src/views/agents.jsx +84 -10
- package/packages/gui/src/views/dashboard.jsx +24 -21
- package/packages/gui/src/views/marketplace.jsx +153 -85
- package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
- package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
- package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
- package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
- package/node_modules/@radix-ui/react-popover/README.md +0 -3
- package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
- package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
- package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-popover/package.json +0 -82
- package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
- package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
- package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
- package/node_modules/@radix-ui/react-separator/package.json +0 -69
- package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
- 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
|
-
|
|
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'
|
|
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,
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.setCredential(integrationId, '
|
|
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
|
-
|
|
486
|
+
return { ok: true, integrationIds };
|
|
487
|
+
}
|
|
445
488
|
|
|
446
|
-
|
|
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),
|
|
144
|
-
lines.push(
|
|
145
|
-
lines.push(`
|
|
146
|
-
lines.push(
|
|
147
|
-
lines.push(`
|
|
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 —
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
}
|