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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// GROOVE — MCP Manager (Provider-Agnostic Integration Execution)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { spawn as cpSpawn } from 'child_process';
|
|
5
|
+
|
|
6
|
+
const IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
7
|
+
const MAX_RETRIES = 3;
|
|
8
|
+
|
|
9
|
+
export class McpManager {
|
|
10
|
+
constructor(daemon) {
|
|
11
|
+
this.daemon = daemon;
|
|
12
|
+
this.servers = new Map();
|
|
13
|
+
this._crashCounts = new Map();
|
|
14
|
+
this._nextId = 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async startServer(integrationId) {
|
|
18
|
+
if (this.servers.has(integrationId)) {
|
|
19
|
+
const existing = this.servers.get(integrationId);
|
|
20
|
+
if (existing.proc && !existing.proc.killed) {
|
|
21
|
+
return existing.tools;
|
|
22
|
+
}
|
|
23
|
+
this._cleanup(integrationId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const entry = this.daemon.integrations.registry.find((s) => s.id === integrationId);
|
|
27
|
+
if (!entry) throw new Error(`Integration not found: ${integrationId}`);
|
|
28
|
+
|
|
29
|
+
if (!this.daemon.integrations._isInstalled(integrationId)) {
|
|
30
|
+
throw new Error(`Integration not installed: ${integrationId}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ((this._crashCounts.get(integrationId) || 0) >= MAX_RETRIES) {
|
|
34
|
+
throw new Error(`Integration ${integrationId} crashed ${MAX_RETRIES} times — not restarting`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const command = entry.command || 'npx';
|
|
38
|
+
const args = entry.args || ['-y', entry.npmPackage];
|
|
39
|
+
|
|
40
|
+
const env = { ...process.env };
|
|
41
|
+
const spawnEnv = this.daemon.integrations.getSpawnEnv([integrationId]);
|
|
42
|
+
Object.assign(env, spawnEnv);
|
|
43
|
+
|
|
44
|
+
const proc = cpSpawn(command, args, {
|
|
45
|
+
env,
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
detached: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const server = {
|
|
51
|
+
proc,
|
|
52
|
+
integrationId,
|
|
53
|
+
tools: [],
|
|
54
|
+
pending: new Map(),
|
|
55
|
+
buffer: '',
|
|
56
|
+
lastCall: Date.now(),
|
|
57
|
+
idleTimer: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.servers.set(integrationId, server);
|
|
61
|
+
|
|
62
|
+
proc.stdout.on('data', (chunk) => {
|
|
63
|
+
server.buffer += chunk.toString();
|
|
64
|
+
this._processBuffer(server);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
proc.stderr.on('data', (chunk) => {
|
|
68
|
+
console.log(`[Groove:MCP:${integrationId}] stderr: ${chunk.toString().slice(0, 200)}`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
proc.on('error', (err) => {
|
|
72
|
+
console.log(`[Groove:MCP:${integrationId}] Process error: ${err.message}`);
|
|
73
|
+
this._handleCrash(integrationId);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
proc.on('exit', (code, signal) => {
|
|
77
|
+
console.log(`[Groove:MCP:${integrationId}] Process exited: code=${code} signal=${signal}`);
|
|
78
|
+
if ((code !== 0 && code !== null) || (code === null && signal)) {
|
|
79
|
+
this._handleCrash(integrationId);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const tools = await this._initialize(server);
|
|
84
|
+
server.tools = tools;
|
|
85
|
+
this._crashCounts.delete(integrationId);
|
|
86
|
+
this._resetIdleTimer(server);
|
|
87
|
+
|
|
88
|
+
console.log(`[Groove:MCP:${integrationId}] Started — ${tools.length} tools available`);
|
|
89
|
+
return tools;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
stopServer(integrationId) {
|
|
93
|
+
const server = this.servers.get(integrationId);
|
|
94
|
+
if (!server) return;
|
|
95
|
+
this._cleanup(integrationId);
|
|
96
|
+
console.log(`[Groove:MCP:${integrationId}] Stopped`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async execTool(integrationId, toolName, params) {
|
|
100
|
+
let server = this.servers.get(integrationId);
|
|
101
|
+
if (!server || server.proc.killed) {
|
|
102
|
+
await this.startServer(integrationId);
|
|
103
|
+
server = this.servers.get(integrationId);
|
|
104
|
+
}
|
|
105
|
+
if (!server) throw new Error(`Failed to start MCP server for ${integrationId}`);
|
|
106
|
+
|
|
107
|
+
server.lastCall = Date.now();
|
|
108
|
+
this._resetIdleTimer(server);
|
|
109
|
+
|
|
110
|
+
const id = this._nextId++;
|
|
111
|
+
const msg = {
|
|
112
|
+
jsonrpc: '2.0',
|
|
113
|
+
id,
|
|
114
|
+
method: 'tools/call',
|
|
115
|
+
params: { name: toolName, arguments: params || {} },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const result = await this._sendRequest(server, id, msg);
|
|
119
|
+
|
|
120
|
+
if (result.error) {
|
|
121
|
+
throw new Error(result.error.message || JSON.stringify(result.error));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result.result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async listTools(integrationId) {
|
|
128
|
+
const server = this.servers.get(integrationId);
|
|
129
|
+
if (server && !server.proc.killed && server.tools.length > 0) {
|
|
130
|
+
return server.tools;
|
|
131
|
+
}
|
|
132
|
+
return this.startServer(integrationId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
stopAll() {
|
|
136
|
+
for (const integrationId of this.servers.keys()) {
|
|
137
|
+
this._cleanup(integrationId);
|
|
138
|
+
}
|
|
139
|
+
console.log('[Groove:MCP] All servers stopped');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_processBuffer(server) {
|
|
143
|
+
const lines = server.buffer.split('\n');
|
|
144
|
+
server.buffer = lines.pop() || '';
|
|
145
|
+
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const trimmed = line.trim();
|
|
148
|
+
if (!trimmed) continue;
|
|
149
|
+
try {
|
|
150
|
+
const msg = JSON.parse(trimmed);
|
|
151
|
+
if (msg.id !== undefined && server.pending.has(msg.id)) {
|
|
152
|
+
const { resolve } = server.pending.get(msg.id);
|
|
153
|
+
server.pending.delete(msg.id);
|
|
154
|
+
resolve(msg);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Not JSON — ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_sendRequest(server, id, msg) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const timeout = setTimeout(() => {
|
|
165
|
+
server.pending.delete(id);
|
|
166
|
+
reject(new Error(`MCP request timed out (id=${id}, method=${msg.method})`));
|
|
167
|
+
}, 30_000);
|
|
168
|
+
|
|
169
|
+
server.pending.set(id, {
|
|
170
|
+
resolve: (result) => {
|
|
171
|
+
clearTimeout(timeout);
|
|
172
|
+
resolve(result);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
server.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
178
|
+
} catch (err) {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
server.pending.delete(id);
|
|
181
|
+
reject(new Error(`Failed to write to MCP server: ${err.message}`));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async _initialize(server) {
|
|
187
|
+
const initId = this._nextId++;
|
|
188
|
+
const initMsg = {
|
|
189
|
+
jsonrpc: '2.0',
|
|
190
|
+
id: initId,
|
|
191
|
+
method: 'initialize',
|
|
192
|
+
params: {
|
|
193
|
+
protocolVersion: '2024-11-05',
|
|
194
|
+
capabilities: {},
|
|
195
|
+
clientInfo: { name: 'groove', version: '1.0.0' },
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const initResp = await this._sendRequest(server, initId, initMsg);
|
|
200
|
+
if (initResp.error) {
|
|
201
|
+
throw new Error(initResp.error.message || 'MCP initialize failed');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const notif = { jsonrpc: '2.0', method: 'notifications/initialized' };
|
|
205
|
+
try {
|
|
206
|
+
server.proc.stdin.write(JSON.stringify(notif) + '\n');
|
|
207
|
+
} catch {
|
|
208
|
+
throw new Error('MCP server died during initialization');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const listId = this._nextId++;
|
|
212
|
+
const listMsg = {
|
|
213
|
+
jsonrpc: '2.0',
|
|
214
|
+
id: listId,
|
|
215
|
+
method: 'tools/list',
|
|
216
|
+
params: {},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const listResp = await this._sendRequest(server, listId, listMsg);
|
|
220
|
+
return listResp.result?.tools || [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_handleCrash(integrationId) {
|
|
224
|
+
const server = this.servers.get(integrationId);
|
|
225
|
+
if (!server) return;
|
|
226
|
+
|
|
227
|
+
for (const [, { resolve }] of server.pending) {
|
|
228
|
+
resolve({ error: { message: 'MCP server crashed' } });
|
|
229
|
+
}
|
|
230
|
+
server.pending.clear();
|
|
231
|
+
|
|
232
|
+
const crashes = (this._crashCounts.get(integrationId) || 0) + 1;
|
|
233
|
+
this._crashCounts.set(integrationId, crashes);
|
|
234
|
+
|
|
235
|
+
if (crashes >= MAX_RETRIES) {
|
|
236
|
+
console.log(`[Groove:MCP:${integrationId}] Max retries reached (${crashes}/${MAX_RETRIES}) — giving up`);
|
|
237
|
+
} else {
|
|
238
|
+
console.log(`[Groove:MCP:${integrationId}] Crash ${crashes}/${MAX_RETRIES} — will restart on next call`);
|
|
239
|
+
}
|
|
240
|
+
this._cleanup(integrationId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_resetIdleTimer(server) {
|
|
244
|
+
if (server.idleTimer) clearTimeout(server.idleTimer);
|
|
245
|
+
server.idleTimer = setTimeout(() => {
|
|
246
|
+
console.log(`[Groove:MCP:${server.integrationId}] Idle timeout — stopping`);
|
|
247
|
+
this.stopServer(server.integrationId);
|
|
248
|
+
}, IDLE_TIMEOUT_MS);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_cleanup(integrationId) {
|
|
252
|
+
const server = this.servers.get(integrationId);
|
|
253
|
+
if (!server) return;
|
|
254
|
+
|
|
255
|
+
if (server.idleTimer) clearTimeout(server.idleTimer);
|
|
256
|
+
|
|
257
|
+
for (const [, { resolve }] of server.pending) {
|
|
258
|
+
resolve({ error: { message: 'MCP server stopped' } });
|
|
259
|
+
}
|
|
260
|
+
server.pending.clear();
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
if (server.proc && !server.proc.killed) {
|
|
264
|
+
server.proc.kill('SIGTERM');
|
|
265
|
+
}
|
|
266
|
+
} catch { /* ignore */ }
|
|
267
|
+
|
|
268
|
+
this.servers.delete(integrationId);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// GROOVE — Persistent Agent Memory (Layer 7)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
//
|
|
4
|
+
// Four file types, all in .groove/memory/:
|
|
5
|
+
// - project-constraints.md Discovered project rules, do-not-touch, required patterns
|
|
6
|
+
// - handoff-chain/<role>.md Cumulative rotation briefs (newest first, last 10 kept)
|
|
7
|
+
// - agent-discoveries.jsonl Error→fix pairs (only successes stored)
|
|
8
|
+
// - agent-specializations.json Per-agent and per-project-role quality profiles
|
|
9
|
+
//
|
|
10
|
+
// Read by the introducer on every spawn so agent #50 knows what agent #1 learned.
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync, statSync } from 'fs';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
|
|
16
|
+
const MAX_CONSTRAINTS = 50;
|
|
17
|
+
const MAX_HANDOFF_ROTATIONS = 10;
|
|
18
|
+
const MAX_DISCOVERIES = 1000;
|
|
19
|
+
const HANDOFF_BRIEF_MAX_CHARS = 4000;
|
|
20
|
+
|
|
21
|
+
function hashText(text) {
|
|
22
|
+
return createHash('sha1').update(text.trim().toLowerCase()).digest('hex').slice(0, 12);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeName(role) {
|
|
26
|
+
return (role || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function truncate(text, max) {
|
|
30
|
+
if (!text || text.length <= max) return text || '';
|
|
31
|
+
return text.slice(0, max - 3) + '...';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class MemoryStore {
|
|
35
|
+
constructor(grooveDir) {
|
|
36
|
+
this.memDir = resolve(grooveDir, 'memory');
|
|
37
|
+
this.constraintsPath = resolve(this.memDir, 'project-constraints.md');
|
|
38
|
+
this.handoffDir = resolve(this.memDir, 'handoff-chain');
|
|
39
|
+
this.discoveriesPath = resolve(this.memDir, 'agent-discoveries.jsonl');
|
|
40
|
+
this.specializationsPath = resolve(this.memDir, 'agent-specializations.json');
|
|
41
|
+
this._ensureDirs();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_ensureDirs() {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(this.memDir, { recursive: true });
|
|
47
|
+
mkdirSync(this.handoffDir, { recursive: true });
|
|
48
|
+
} catch { /* best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Project Constraints ---
|
|
52
|
+
|
|
53
|
+
listConstraints() {
|
|
54
|
+
if (!existsSync(this.constraintsPath)) return [];
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(this.constraintsPath, 'utf8');
|
|
57
|
+
const constraints = [];
|
|
58
|
+
const blocks = content.split(/\n(?=- )/); // each constraint starts with "- "
|
|
59
|
+
for (const block of blocks) {
|
|
60
|
+
const m = block.match(/^- \[([a-f0-9]+)\] \*([^*]+)\* (.+)$/s);
|
|
61
|
+
if (m) {
|
|
62
|
+
constraints.push({
|
|
63
|
+
hash: m[1],
|
|
64
|
+
category: m[2].trim(),
|
|
65
|
+
text: m[3].trim(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return constraints;
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
addConstraint({ text, category = 'general' }) {
|
|
76
|
+
if (!text || typeof text !== 'string') return { added: false, error: 'text required' };
|
|
77
|
+
const trimmed = text.trim();
|
|
78
|
+
if (trimmed.length < 3) return { added: false, error: 'text too short' };
|
|
79
|
+
if (trimmed.length > 500) return { added: false, error: 'text too long (max 500 chars)' };
|
|
80
|
+
|
|
81
|
+
const hash = hashText(trimmed);
|
|
82
|
+
const existing = this.listConstraints();
|
|
83
|
+
if (existing.some((c) => c.hash === hash)) {
|
|
84
|
+
return { added: false, hash, reason: 'duplicate' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
existing.push({ hash, category, text: trimmed });
|
|
88
|
+
// Keep most recent MAX_CONSTRAINTS
|
|
89
|
+
const pruned = existing.slice(-MAX_CONSTRAINTS);
|
|
90
|
+
this._writeConstraints(pruned);
|
|
91
|
+
return { added: true, hash };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
removeConstraint(hash) {
|
|
95
|
+
const existing = this.listConstraints();
|
|
96
|
+
const filtered = existing.filter((c) => c.hash !== hash);
|
|
97
|
+
if (filtered.length === existing.length) return false;
|
|
98
|
+
this._writeConstraints(filtered);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_writeConstraints(constraints) {
|
|
103
|
+
const lines = [
|
|
104
|
+
'# Project Constraints',
|
|
105
|
+
`*Auto-managed by GROOVE memory (Layer 7). Last updated: ${new Date().toISOString()}*`,
|
|
106
|
+
'',
|
|
107
|
+
];
|
|
108
|
+
for (const c of constraints) {
|
|
109
|
+
lines.push(`- [${c.hash}] *${c.category}* ${c.text}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(this.constraintsPath, lines.join('\n'));
|
|
114
|
+
} catch { /* best-effort */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getConstraintsMarkdown(maxChars = 4000) {
|
|
118
|
+
const constraints = this.listConstraints();
|
|
119
|
+
if (constraints.length === 0) return '';
|
|
120
|
+
const byCategory = {};
|
|
121
|
+
for (const c of constraints) {
|
|
122
|
+
byCategory[c.category] = byCategory[c.category] || [];
|
|
123
|
+
byCategory[c.category].push(c.text);
|
|
124
|
+
}
|
|
125
|
+
const lines = [];
|
|
126
|
+
for (const [cat, items] of Object.entries(byCategory)) {
|
|
127
|
+
lines.push(`**${cat}:**`);
|
|
128
|
+
for (const item of items) lines.push(`- ${item}`);
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
return truncate(lines.join('\n').trim(), maxChars);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Handoff Chain ---
|
|
135
|
+
|
|
136
|
+
_chainPath(role) {
|
|
137
|
+
return resolve(this.handoffDir, `${safeName(role)}.md`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getHandoffChain(role) {
|
|
141
|
+
const path = this._chainPath(role);
|
|
142
|
+
if (!existsSync(path)) return [];
|
|
143
|
+
try {
|
|
144
|
+
const content = readFileSync(path, 'utf8');
|
|
145
|
+
const entries = [];
|
|
146
|
+
// Parse entries — each starts with "## Rotation N —"
|
|
147
|
+
// Body includes the header + all content up to (but not including) the
|
|
148
|
+
// trailing --- separator and the next entry.
|
|
149
|
+
const blocks = content.split(/\n(?=## Rotation )/);
|
|
150
|
+
for (const block of blocks) {
|
|
151
|
+
const headerMatch = block.match(/^## Rotation (\d+) —/);
|
|
152
|
+
if (!headerMatch) continue;
|
|
153
|
+
const body = block.replace(/\n---\s*$/, '').trim();
|
|
154
|
+
entries.push({
|
|
155
|
+
rotationN: parseInt(headerMatch[1], 10),
|
|
156
|
+
body,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return entries;
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
appendHandoffBrief(role, entry) {
|
|
166
|
+
if (!role || !entry) return false;
|
|
167
|
+
const chain = this.getHandoffChain(role);
|
|
168
|
+
const nextN = (chain[0]?.rotationN || 0) + 1;
|
|
169
|
+
|
|
170
|
+
const block = [
|
|
171
|
+
`## Rotation ${nextN} — ${entry.timestamp || new Date().toISOString()} (${entry.agentId || '?'} → ${entry.newAgentId || '?'})`,
|
|
172
|
+
`**Reason:** ${entry.reason || 'unknown'}`,
|
|
173
|
+
entry.oldTokens != null ? `**Tokens carried:** ${entry.oldTokens.toLocaleString()}` : '',
|
|
174
|
+
entry.contextUsage != null ? `**Context at rotation:** ${Math.round(entry.contextUsage * 100)}%` : '',
|
|
175
|
+
'',
|
|
176
|
+
'**Brief summary:**',
|
|
177
|
+
truncate(entry.brief || '(no brief)', HANDOFF_BRIEF_MAX_CHARS),
|
|
178
|
+
'',
|
|
179
|
+
].filter(Boolean).join('\n');
|
|
180
|
+
|
|
181
|
+
// Prepend new entry (newest first), keep last N
|
|
182
|
+
const newChain = [{ rotationN: nextN, body: block }, ...chain].slice(0, MAX_HANDOFF_ROTATIONS);
|
|
183
|
+
|
|
184
|
+
const lines = [
|
|
185
|
+
`# ${role[0].toUpperCase() + role.slice(1)} Handoff Chain`,
|
|
186
|
+
`*Cumulative rotation briefs. Newest first. Last ${MAX_HANDOFF_ROTATIONS} kept.*`,
|
|
187
|
+
'',
|
|
188
|
+
];
|
|
189
|
+
for (const e of newChain) {
|
|
190
|
+
lines.push(e.body || '');
|
|
191
|
+
lines.push('---');
|
|
192
|
+
lines.push('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(this._chainPath(role), lines.join('\n'));
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getRecentHandoffMarkdown(role, count = 3, maxChars = 4000) {
|
|
204
|
+
const chain = this.getHandoffChain(role);
|
|
205
|
+
if (chain.length === 0) return '';
|
|
206
|
+
const recent = chain.slice(0, count);
|
|
207
|
+
const out = recent.map((e) => e.body || '').join('\n\n---\n\n');
|
|
208
|
+
return truncate(out, maxChars);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
listHandoffRoles() {
|
|
212
|
+
if (!existsSync(this.handoffDir)) return [];
|
|
213
|
+
try {
|
|
214
|
+
return readdirSync(this.handoffDir)
|
|
215
|
+
.filter((f) => f.endsWith('.md'))
|
|
216
|
+
.map((f) => f.replace(/\.md$/, ''));
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- Discoveries (error → fix pairs) ---
|
|
223
|
+
|
|
224
|
+
addDiscovery({ agentId, role, trigger, fix, outcome = 'success' }) {
|
|
225
|
+
if (!trigger || !fix) return { added: false, error: 'trigger and fix required' };
|
|
226
|
+
if (outcome !== 'success') return { added: false, reason: 'only successes stored' };
|
|
227
|
+
|
|
228
|
+
const entry = {
|
|
229
|
+
ts: new Date().toISOString(),
|
|
230
|
+
agentId: agentId || null,
|
|
231
|
+
role: role || 'unknown',
|
|
232
|
+
trigger: truncate(String(trigger).trim(), 300),
|
|
233
|
+
fix: truncate(String(fix).trim(), 500),
|
|
234
|
+
outcome,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Dedup: same trigger+fix = skip
|
|
238
|
+
const existing = this.listDiscoveries({ limit: 200 });
|
|
239
|
+
const key = hashText(entry.trigger + '||' + entry.fix);
|
|
240
|
+
if (existing.some((d) => hashText(d.trigger + '||' + d.fix) === key)) {
|
|
241
|
+
return { added: false, reason: 'duplicate' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
appendFileSync(this.discoveriesPath, JSON.stringify(entry) + '\n');
|
|
246
|
+
this._pruneDiscoveries();
|
|
247
|
+
return { added: true };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
return { added: false, error: err.message };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
listDiscoveries({ role, limit = 100 } = {}) {
|
|
254
|
+
if (!existsSync(this.discoveriesPath)) return [];
|
|
255
|
+
try {
|
|
256
|
+
const lines = readFileSync(this.discoveriesPath, 'utf8').split('\n').filter(Boolean);
|
|
257
|
+
const entries = [];
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
try {
|
|
260
|
+
const e = JSON.parse(line);
|
|
261
|
+
if (!role || e.role === role) entries.push(e);
|
|
262
|
+
} catch { /* skip malformed */ }
|
|
263
|
+
}
|
|
264
|
+
return entries.slice(-limit).reverse(); // newest first
|
|
265
|
+
} catch {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_pruneDiscoveries() {
|
|
271
|
+
if (!existsSync(this.discoveriesPath)) return;
|
|
272
|
+
try {
|
|
273
|
+
const stat = statSync(this.discoveriesPath);
|
|
274
|
+
// Only prune on larger files to avoid thrashing
|
|
275
|
+
if (stat.size < 50_000) return;
|
|
276
|
+
const lines = readFileSync(this.discoveriesPath, 'utf8').split('\n').filter(Boolean);
|
|
277
|
+
if (lines.length <= MAX_DISCOVERIES) return;
|
|
278
|
+
const kept = lines.slice(-MAX_DISCOVERIES);
|
|
279
|
+
writeFileSync(this.discoveriesPath, kept.join('\n') + '\n');
|
|
280
|
+
} catch { /* best-effort */ }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getDiscoveriesMarkdown(role, limit = 20, maxChars = 4000) {
|
|
284
|
+
const entries = this.listDiscoveries({ role, limit });
|
|
285
|
+
if (entries.length === 0) return '';
|
|
286
|
+
const lines = entries.map((d) => `- When \`${d.trigger}\` → fix: ${d.fix}`);
|
|
287
|
+
return truncate(lines.join('\n'), maxChars);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Specializations ---
|
|
291
|
+
|
|
292
|
+
_loadSpecializations() {
|
|
293
|
+
if (!existsSync(this.specializationsPath)) {
|
|
294
|
+
return { perAgent: {}, perProjectRole: {} };
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const data = JSON.parse(readFileSync(this.specializationsPath, 'utf8'));
|
|
298
|
+
return {
|
|
299
|
+
perAgent: data.perAgent || {},
|
|
300
|
+
perProjectRole: data.perProjectRole || {},
|
|
301
|
+
};
|
|
302
|
+
} catch {
|
|
303
|
+
return { perAgent: {}, perProjectRole: {} };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_saveSpecializations(data) {
|
|
308
|
+
try {
|
|
309
|
+
writeFileSync(this.specializationsPath, JSON.stringify(data, null, 2));
|
|
310
|
+
} catch { /* best-effort */ }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
updateSpecialization(agentId, { role, qualityScore, filesTouched, signals, threshold }) {
|
|
314
|
+
if (!agentId) return false;
|
|
315
|
+
const data = this._loadSpecializations();
|
|
316
|
+
|
|
317
|
+
const agentEntry = data.perAgent[agentId] || {
|
|
318
|
+
role: role || 'unknown',
|
|
319
|
+
sessionCount: 0,
|
|
320
|
+
avgQualityScore: 0,
|
|
321
|
+
qualityTotal: 0,
|
|
322
|
+
fileTouches: {},
|
|
323
|
+
signatureErrors: [],
|
|
324
|
+
};
|
|
325
|
+
agentEntry.sessionCount += 1;
|
|
326
|
+
if (typeof qualityScore === 'number') {
|
|
327
|
+
agentEntry.qualityTotal += qualityScore;
|
|
328
|
+
agentEntry.avgQualityScore = Math.round(agentEntry.qualityTotal / agentEntry.sessionCount);
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(filesTouched)) {
|
|
331
|
+
for (const f of filesTouched) {
|
|
332
|
+
agentEntry.fileTouches[f] = (agentEntry.fileTouches[f] || 0) + 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (role) agentEntry.role = role;
|
|
336
|
+
if (threshold != null) agentEntry.preferredThreshold = threshold;
|
|
337
|
+
data.perAgent[agentId] = agentEntry;
|
|
338
|
+
|
|
339
|
+
if (role) {
|
|
340
|
+
const roleEntry = data.perProjectRole[role] || {
|
|
341
|
+
sessionCount: 0,
|
|
342
|
+
avgQualityScore: 0,
|
|
343
|
+
qualityTotal: 0,
|
|
344
|
+
topFileChurn: {},
|
|
345
|
+
};
|
|
346
|
+
roleEntry.sessionCount += 1;
|
|
347
|
+
if (typeof qualityScore === 'number') {
|
|
348
|
+
roleEntry.qualityTotal += qualityScore;
|
|
349
|
+
roleEntry.avgQualityScore = Math.round(roleEntry.qualityTotal / roleEntry.sessionCount);
|
|
350
|
+
}
|
|
351
|
+
if (Array.isArray(filesTouched)) {
|
|
352
|
+
for (const f of filesTouched) {
|
|
353
|
+
roleEntry.topFileChurn[f] = (roleEntry.topFileChurn[f] || 0) + 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
data.perProjectRole[role] = roleEntry;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this._saveSpecializations(data);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
getSpecialization(agentId) {
|
|
364
|
+
return this._loadSpecializations().perAgent[agentId] || null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getAllSpecializations() {
|
|
368
|
+
return this._loadSpecializations();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -54,7 +54,7 @@ Review: Is this within scope? Conflicts with other agents? Aligns with project?
|
|
|
54
54
|
Respond in ONE line: APPROVED: <reason> or REJECTED: <reason>`;
|
|
55
55
|
|
|
56
56
|
try {
|
|
57
|
-
const result = await this.daemon.journalist.callHeadless(prompt);
|
|
57
|
+
const result = await this.daemon.journalist.callHeadless(prompt, { trackAs: '__pm__' });
|
|
58
58
|
const text = (result || '').trim();
|
|
59
59
|
const approved = !text.toUpperCase().startsWith('REJECTED');
|
|
60
60
|
const reason = text.replace(/^(APPROVED|REJECTED):?\s*/i, '').trim();
|