groove-dev 0.16.3 → 0.17.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/README.md +18 -16
- package/node_modules/@groove-dev/daemon/integrations-registry.json +321 -0
- package/node_modules/@groove-dev/daemon/src/api.js +152 -0
- package/node_modules/@groove-dev/daemon/src/index.js +13 -1
- package/node_modules/@groove-dev/daemon/src/integrations.js +389 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +23 -0
- package/node_modules/@groove-dev/daemon/src/process.js +59 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
- package/node_modules/@groove-dev/daemon/src/scheduler.js +336 -0
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +119 -54
- package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C5k-qSwi.js +153 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +98 -7
- package/node_modules/@groove-dev/gui/src/components/Terminal.jsx +29 -12
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +954 -0
- package/node_modules/@groove-dev/gui/src/views/ScheduleManager.jsx +614 -0
- package/package.json +2 -2
- package/packages/daemon/integrations-registry.json +321 -0
- package/packages/daemon/src/api.js +152 -0
- package/packages/daemon/src/index.js +13 -1
- package/packages/daemon/src/integrations.js +389 -0
- package/packages/daemon/src/introducer.js +23 -0
- package/packages/daemon/src/process.js +59 -0
- package/packages/daemon/src/registry.js +2 -1
- package/packages/daemon/src/scheduler.js +336 -0
- package/packages/daemon/src/terminal-pty.js +119 -54
- package/packages/daemon/src/validate.js +10 -0
- package/packages/gui/dist/assets/index-C5k-qSwi.js +153 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/App.jsx +6 -0
- package/packages/gui/src/components/SpawnPanel.jsx +98 -7
- package/packages/gui/src/components/Terminal.jsx +29 -12
- package/packages/gui/src/views/IntegrationsStore.jsx +954 -0
- package/packages/gui/src/views/ScheduleManager.jsx +614 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CFeltwTB.js +0 -153
- package/packages/gui/dist/assets/index-CFeltwTB.js +0 -153
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// GROOVE — Integration Store (MCP Server Management)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
const INTEGRATIONS_API = 'https://docs.groovedev.ai/api/v1';
|
|
12
|
+
|
|
13
|
+
export class IntegrationStore {
|
|
14
|
+
constructor(daemon) {
|
|
15
|
+
this.daemon = daemon;
|
|
16
|
+
this.integrationsDir = resolve(daemon.grooveDir, 'integrations');
|
|
17
|
+
mkdirSync(this.integrationsDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
// Load bundled registry as fallback
|
|
20
|
+
this.registry = [];
|
|
21
|
+
try {
|
|
22
|
+
const regPath = resolve(__dirname, '../integrations-registry.json');
|
|
23
|
+
this.registry = JSON.parse(readFileSync(regPath, 'utf8'));
|
|
24
|
+
} catch { /* no registry file */ }
|
|
25
|
+
|
|
26
|
+
// Ensure the integrations directory has a package.json for npm installs
|
|
27
|
+
this._ensurePackageJson();
|
|
28
|
+
|
|
29
|
+
// Fetch live registry in background
|
|
30
|
+
this._refreshRegistry();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_ensurePackageJson() {
|
|
34
|
+
const pkgPath = resolve(this.integrationsDir, 'package.json');
|
|
35
|
+
if (!existsSync(pkgPath)) {
|
|
36
|
+
writeFileSync(pkgPath, JSON.stringify({
|
|
37
|
+
name: 'groove-integrations',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
private: true,
|
|
40
|
+
description: 'MCP server packages managed by Groove',
|
|
41
|
+
}, null, 2));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async _refreshRegistry() {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${INTEGRATIONS_API}/integrations?limit=200`, { signal: AbortSignal.timeout(5000) });
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
this.registry = data.integrations || data;
|
|
51
|
+
}
|
|
52
|
+
} catch { /* offline — use bundled */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get integrations from the registry with optional search/category filter.
|
|
57
|
+
*/
|
|
58
|
+
async getRegistry(query) {
|
|
59
|
+
let items = this.registry.map((s) => ({
|
|
60
|
+
...s,
|
|
61
|
+
installed: this._isInstalled(s.id),
|
|
62
|
+
configured: this._isConfigured(s),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
if (query?.search) {
|
|
66
|
+
const q = query.search.toLowerCase();
|
|
67
|
+
items = items.filter((s) =>
|
|
68
|
+
s.name.toLowerCase().includes(q)
|
|
69
|
+
|| s.description.toLowerCase().includes(q)
|
|
70
|
+
|| (s.tags || []).some((t) => t.includes(q))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (query?.category && query.category !== 'all') {
|
|
75
|
+
items = items.filter((s) => s.category === query.category);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return items;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get installed integrations only.
|
|
83
|
+
*/
|
|
84
|
+
getInstalled() {
|
|
85
|
+
const installed = [];
|
|
86
|
+
const metaPath = resolve(this.integrationsDir, 'installed.json');
|
|
87
|
+
let installedMeta = {};
|
|
88
|
+
if (existsSync(metaPath)) {
|
|
89
|
+
try { installedMeta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* ignore */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [id, meta] of Object.entries(installedMeta)) {
|
|
93
|
+
const regEntry = this.registry.find((r) => r.id === id);
|
|
94
|
+
if (!regEntry && !meta) continue;
|
|
95
|
+
installed.push({
|
|
96
|
+
...(regEntry || {}),
|
|
97
|
+
...meta,
|
|
98
|
+
id,
|
|
99
|
+
installed: true,
|
|
100
|
+
configured: this._isConfigured(regEntry || meta),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return installed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get available categories from the registry.
|
|
109
|
+
*/
|
|
110
|
+
getCategories() {
|
|
111
|
+
const cats = new Map();
|
|
112
|
+
for (const item of this.registry) {
|
|
113
|
+
const count = cats.get(item.category) || 0;
|
|
114
|
+
cats.set(item.category, count + 1);
|
|
115
|
+
}
|
|
116
|
+
return Array.from(cats.entries())
|
|
117
|
+
.map(([id, count]) => ({ id, count }))
|
|
118
|
+
.sort((a, b) => b.count - a.count);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Install an integration (npm install the MCP server package).
|
|
123
|
+
*/
|
|
124
|
+
async install(integrationId) {
|
|
125
|
+
const entry = this.registry.find((s) => s.id === integrationId);
|
|
126
|
+
if (!entry) throw new Error(`Integration not found: ${integrationId}`);
|
|
127
|
+
if (this._isInstalled(integrationId)) throw new Error(`Integration already installed: ${integrationId}`);
|
|
128
|
+
|
|
129
|
+
if (entry.npmPackage) {
|
|
130
|
+
try {
|
|
131
|
+
execFileSync('npm', ['install', entry.npmPackage], {
|
|
132
|
+
cwd: this.integrationsDir,
|
|
133
|
+
stdio: 'pipe',
|
|
134
|
+
timeout: 120_000,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new Error(`Failed to install ${entry.npmPackage}: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Record installation metadata
|
|
142
|
+
const metaPath = resolve(this.integrationsDir, 'installed.json');
|
|
143
|
+
let installedMeta = {};
|
|
144
|
+
if (existsSync(metaPath)) {
|
|
145
|
+
try { installedMeta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* ignore */ }
|
|
146
|
+
}
|
|
147
|
+
installedMeta[integrationId] = {
|
|
148
|
+
name: entry.name,
|
|
149
|
+
installedAt: new Date().toISOString(),
|
|
150
|
+
npmPackage: entry.npmPackage,
|
|
151
|
+
};
|
|
152
|
+
writeFileSync(metaPath, JSON.stringify(installedMeta, null, 2));
|
|
153
|
+
|
|
154
|
+
this.daemon.audit.log('integration.install', { id: integrationId, name: entry.name });
|
|
155
|
+
|
|
156
|
+
return { id: integrationId, name: entry.name, installed: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Uninstall an integration.
|
|
161
|
+
*/
|
|
162
|
+
async uninstall(integrationId) {
|
|
163
|
+
if (!this._isInstalled(integrationId)) throw new Error(`Integration not installed: ${integrationId}`);
|
|
164
|
+
|
|
165
|
+
const entry = this.registry.find((s) => s.id === integrationId);
|
|
166
|
+
if (entry?.npmPackage) {
|
|
167
|
+
try {
|
|
168
|
+
execFileSync('npm', ['uninstall', entry.npmPackage], {
|
|
169
|
+
cwd: this.integrationsDir,
|
|
170
|
+
stdio: 'pipe',
|
|
171
|
+
timeout: 60_000,
|
|
172
|
+
});
|
|
173
|
+
} catch { /* best effort */ }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Remove from installed metadata
|
|
177
|
+
const metaPath = resolve(this.integrationsDir, 'installed.json');
|
|
178
|
+
let installedMeta = {};
|
|
179
|
+
if (existsSync(metaPath)) {
|
|
180
|
+
try { installedMeta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* ignore */ }
|
|
181
|
+
}
|
|
182
|
+
delete installedMeta[integrationId];
|
|
183
|
+
writeFileSync(metaPath, JSON.stringify(installedMeta, null, 2));
|
|
184
|
+
|
|
185
|
+
// Remove credentials for this integration
|
|
186
|
+
this._removeCredentials(integrationId);
|
|
187
|
+
|
|
188
|
+
this.daemon.audit.log('integration.uninstall', { id: integrationId });
|
|
189
|
+
|
|
190
|
+
return { id: integrationId, installed: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Set a credential for an integration.
|
|
195
|
+
*/
|
|
196
|
+
setCredential(integrationId, key, value) {
|
|
197
|
+
const credKey = `integration:${integrationId}:${key}`;
|
|
198
|
+
this.daemon.credentials.setKey(credKey, value);
|
|
199
|
+
this.daemon.audit.log('integration.credential.set', { id: integrationId, key });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get a credential for an integration.
|
|
204
|
+
*/
|
|
205
|
+
getCredential(integrationId, key) {
|
|
206
|
+
const credKey = `integration:${integrationId}:${key}`;
|
|
207
|
+
return this.daemon.credentials.getKey(credKey);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Delete a credential for an integration.
|
|
212
|
+
*/
|
|
213
|
+
deleteCredential(integrationId, key) {
|
|
214
|
+
const credKey = `integration:${integrationId}:${key}`;
|
|
215
|
+
this.daemon.credentials.deleteKey(credKey);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get the status of an integration (installed, configured, credential keys).
|
|
220
|
+
*/
|
|
221
|
+
getStatus(integrationId) {
|
|
222
|
+
const entry = this.registry.find((s) => s.id === integrationId);
|
|
223
|
+
if (!entry) return null;
|
|
224
|
+
|
|
225
|
+
const installed = this._isInstalled(integrationId);
|
|
226
|
+
const envKeys = (entry.envKeys || []).map((ek) => ({
|
|
227
|
+
...ek,
|
|
228
|
+
set: !!this.getCredential(integrationId, ek.key),
|
|
229
|
+
}));
|
|
230
|
+
const configured = envKeys.length === 0 || envKeys.every((ek) => !ek.required || ek.set);
|
|
231
|
+
|
|
232
|
+
return { id: integrationId, installed, configured, envKeys };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build MCP config object for a set of integration IDs.
|
|
237
|
+
* Returns the mcpServers object to merge into .mcp.json.
|
|
238
|
+
*/
|
|
239
|
+
buildMcpConfig(integrationIds) {
|
|
240
|
+
const mcpServers = {};
|
|
241
|
+
|
|
242
|
+
for (const id of integrationIds) {
|
|
243
|
+
const entry = this.registry.find((s) => s.id === id);
|
|
244
|
+
if (!entry) continue;
|
|
245
|
+
if (!this._isInstalled(id)) continue;
|
|
246
|
+
|
|
247
|
+
// Build environment with credentials
|
|
248
|
+
const env = {};
|
|
249
|
+
for (const ek of (entry.envKeys || [])) {
|
|
250
|
+
const val = this.getCredential(id, ek.key);
|
|
251
|
+
if (val) env[ek.key] = val;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
mcpServers[`groove-${id}`] = {
|
|
255
|
+
command: entry.command || 'npx',
|
|
256
|
+
args: entry.args || ['-y', entry.npmPackage],
|
|
257
|
+
env,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return mcpServers;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Write/merge MCP config into the project root .mcp.json.
|
|
266
|
+
* Only adds/updates groove-* entries, preserves user's own MCP configs.
|
|
267
|
+
*/
|
|
268
|
+
writeMcpJson(integrationIds) {
|
|
269
|
+
const mcpJsonPath = resolve(this.daemon.projectDir, '.mcp.json');
|
|
270
|
+
let existing = {};
|
|
271
|
+
|
|
272
|
+
// Read existing .mcp.json if present
|
|
273
|
+
if (existsSync(mcpJsonPath)) {
|
|
274
|
+
try {
|
|
275
|
+
existing = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
|
|
276
|
+
} catch { /* start fresh */ }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Build MCP config for requested integrations
|
|
280
|
+
const grooveServers = this.buildMcpConfig(integrationIds);
|
|
281
|
+
|
|
282
|
+
// Remove all existing groove-* entries first
|
|
283
|
+
if (existing.mcpServers) {
|
|
284
|
+
for (const key of Object.keys(existing.mcpServers)) {
|
|
285
|
+
if (key.startsWith('groove-')) {
|
|
286
|
+
delete existing.mcpServers[key];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Merge groove entries
|
|
292
|
+
existing.mcpServers = { ...(existing.mcpServers || {}), ...grooveServers };
|
|
293
|
+
|
|
294
|
+
writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Remove all groove-* entries from .mcp.json.
|
|
299
|
+
* If only groove entries remain, delete the file.
|
|
300
|
+
*/
|
|
301
|
+
cleanupMcpJson() {
|
|
302
|
+
const mcpJsonPath = resolve(this.daemon.projectDir, '.mcp.json');
|
|
303
|
+
if (!existsSync(mcpJsonPath)) return;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const config = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
|
|
307
|
+
if (!config.mcpServers) return;
|
|
308
|
+
|
|
309
|
+
// Remove groove-* entries
|
|
310
|
+
let hasUserEntries = false;
|
|
311
|
+
for (const key of Object.keys(config.mcpServers)) {
|
|
312
|
+
if (key.startsWith('groove-')) {
|
|
313
|
+
delete config.mcpServers[key];
|
|
314
|
+
} else {
|
|
315
|
+
hasUserEntries = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (hasUserEntries) {
|
|
320
|
+
writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2));
|
|
321
|
+
} else if (Object.keys(config.mcpServers).length === 0) {
|
|
322
|
+
// If we created this file and it's now empty, remove it
|
|
323
|
+
// Only if no other top-level keys besides mcpServers
|
|
324
|
+
const otherKeys = Object.keys(config).filter((k) => k !== 'mcpServers');
|
|
325
|
+
if (otherKeys.length === 0) {
|
|
326
|
+
rmSync(mcpJsonPath);
|
|
327
|
+
} else {
|
|
328
|
+
writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch { /* ignore cleanup errors */ }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get the combined set of integration IDs from all running agents.
|
|
336
|
+
*/
|
|
337
|
+
getActiveIntegrations() {
|
|
338
|
+
const agents = this.daemon.registry.getAll().filter(
|
|
339
|
+
(a) => a.status === 'running' || a.status === 'starting'
|
|
340
|
+
);
|
|
341
|
+
const ids = new Set();
|
|
342
|
+
for (const agent of agents) {
|
|
343
|
+
for (const id of (agent.integrations || [])) {
|
|
344
|
+
ids.add(id);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return Array.from(ids);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Refresh .mcp.json to reflect all currently running agents' integrations.
|
|
352
|
+
*/
|
|
353
|
+
refreshMcpJson() {
|
|
354
|
+
const activeIntegrations = this.getActiveIntegrations();
|
|
355
|
+
if (activeIntegrations.length > 0) {
|
|
356
|
+
this.writeMcpJson(activeIntegrations);
|
|
357
|
+
} else {
|
|
358
|
+
this.cleanupMcpJson();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --- Internal ---
|
|
363
|
+
|
|
364
|
+
_isInstalled(integrationId) {
|
|
365
|
+
const metaPath = resolve(this.integrationsDir, 'installed.json');
|
|
366
|
+
if (!existsSync(metaPath)) return false;
|
|
367
|
+
try {
|
|
368
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
369
|
+
return !!meta[integrationId];
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_isConfigured(entry) {
|
|
376
|
+
if (!entry || !entry.envKeys || entry.envKeys.length === 0) return true;
|
|
377
|
+
return entry.envKeys
|
|
378
|
+
.filter((ek) => ek.required)
|
|
379
|
+
.every((ek) => !!this.getCredential(entry.id, ek.key));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_removeCredentials(integrationId) {
|
|
383
|
+
const entry = this.registry.find((s) => s.id === integrationId);
|
|
384
|
+
if (!entry?.envKeys) return;
|
|
385
|
+
for (const ek of entry.envKeys) {
|
|
386
|
+
try { this.deleteCredential(integrationId, ek.key); } catch { /* ignore */ }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -188,6 +188,29 @@ export class Introducer {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// Integration context — list MCP tools available to this agent
|
|
192
|
+
if (newAgent.integrations && newAgent.integrations.length > 0 && this.daemon.integrations) {
|
|
193
|
+
const integrationSections = [];
|
|
194
|
+
for (const integrationId of newAgent.integrations) {
|
|
195
|
+
const entry = this.daemon.integrations.registry.find((s) => s.id === integrationId);
|
|
196
|
+
if (entry) {
|
|
197
|
+
const configured = this.daemon.integrations._isConfigured(entry);
|
|
198
|
+
const status = configured ? 'connected' : 'NOT CONFIGURED — credentials missing';
|
|
199
|
+
integrationSections.push(`- **${entry.name}** (${status}): ${entry.description}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (integrationSections.length > 0) {
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(`## Integrations (${integrationSections.length} connected)`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push('You have MCP tools available from these integrations. Use them to interact with external services:');
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(integrationSections.join('\n'));
|
|
209
|
+
lines.push('');
|
|
210
|
+
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.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
return lines.join('\n');
|
|
192
215
|
}
|
|
193
216
|
|
|
@@ -9,6 +9,55 @@ import { getProvider } from './providers/index.js';
|
|
|
9
9
|
// Role-specific prompt prefixes — applied during spawn regardless of entry point
|
|
10
10
|
// (SpawnPanel, chat continue, CLI, API) for consistency
|
|
11
11
|
const ROLE_PROMPTS = {
|
|
12
|
+
// Business roles — use MCP tools, not code
|
|
13
|
+
cmo: `You are a Chief Marketing Officer agent. You have MCP integrations for communication and research. Focus on:
|
|
14
|
+
- Drafting and reviewing marketing content, social media posts, and campaigns
|
|
15
|
+
- Analyzing market trends and competitive positioning
|
|
16
|
+
- Managing team communications and status updates via Slack
|
|
17
|
+
- Researching topics using web search tools
|
|
18
|
+
Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
|
|
19
|
+
|
|
20
|
+
`,
|
|
21
|
+
cfo: `You are a Chief Financial Officer agent. You have MCP integrations for financial data and reporting. Focus on:
|
|
22
|
+
- Reviewing revenue, subscriptions, and payment data via Stripe
|
|
23
|
+
- Creating financial summaries and reports
|
|
24
|
+
- Analyzing spending patterns and forecasting
|
|
25
|
+
- Managing financial documents and spreadsheets
|
|
26
|
+
Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
|
|
27
|
+
|
|
28
|
+
`,
|
|
29
|
+
ea: `You are an Executive Assistant agent. You have MCP integrations for email, calendar, and communication. Focus on:
|
|
30
|
+
- Managing calendar events and scheduling meetings
|
|
31
|
+
- Drafting and sending emails
|
|
32
|
+
- Coordinating team communications via Slack
|
|
33
|
+
- Organizing tasks, reminders, and follow-ups
|
|
34
|
+
Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
|
|
35
|
+
|
|
36
|
+
`,
|
|
37
|
+
support: `You are a Customer Support agent. You have MCP integrations for communication channels. Focus on:
|
|
38
|
+
- Responding to customer inquiries and tickets
|
|
39
|
+
- Triaging and categorizing support requests
|
|
40
|
+
- Drafting helpful responses and knowledge base articles
|
|
41
|
+
- Escalating critical issues with clear summaries
|
|
42
|
+
Do NOT write code unless explicitly asked. Use your MCP tools to interact with external services.
|
|
43
|
+
|
|
44
|
+
`,
|
|
45
|
+
analyst: `You are a Data Analyst agent. You have MCP integrations for databases and data tools. Focus on:
|
|
46
|
+
- Querying databases to extract insights and trends
|
|
47
|
+
- Creating data summaries and reports
|
|
48
|
+
- Identifying patterns, anomalies, and opportunities
|
|
49
|
+
- Presenting findings in clear, actionable format
|
|
50
|
+
Do NOT write code unless explicitly asked. Use your MCP tools (database queries, spreadsheets) to analyze data.
|
|
51
|
+
|
|
52
|
+
`,
|
|
53
|
+
home: `You are a Smart Home automation agent. You have MCP integrations for Home Assistant. Focus on:
|
|
54
|
+
- Monitoring and controlling smart home devices
|
|
55
|
+
- Setting up automations and routines
|
|
56
|
+
- Reporting on device status and energy usage
|
|
57
|
+
- Troubleshooting connectivity and configuration issues
|
|
58
|
+
Do NOT write code unless explicitly asked. Use your MCP tools to interact with Home Assistant.
|
|
59
|
+
|
|
60
|
+
`,
|
|
12
61
|
planner: `You are a planning and architecture agent. Research, analyze, and create plans — do NOT implement code unless explicitly asked. Focus on:
|
|
13
62
|
- Understanding requirements
|
|
14
63
|
- Exploring the codebase
|
|
@@ -152,6 +201,11 @@ For normal file edits within your scope, proceed without review.
|
|
|
152
201
|
}
|
|
153
202
|
}
|
|
154
203
|
|
|
204
|
+
// Write MCP config for agent integrations
|
|
205
|
+
if (config.integrations?.length > 0 && this.daemon.integrations) {
|
|
206
|
+
this.daemon.integrations.writeMcpJson(config.integrations);
|
|
207
|
+
}
|
|
208
|
+
|
|
155
209
|
const { command, args, env } = provider.buildSpawnCommand(spawnConfig);
|
|
156
210
|
|
|
157
211
|
// Set up log capture
|
|
@@ -258,6 +312,11 @@ For normal file edits within your scope, proceed without review.
|
|
|
258
312
|
status: finalStatus,
|
|
259
313
|
});
|
|
260
314
|
|
|
315
|
+
// Refresh MCP config — remove integrations no longer needed by running agents
|
|
316
|
+
if (this.daemon.integrations) {
|
|
317
|
+
this.daemon.integrations.refreshMcpJson();
|
|
318
|
+
}
|
|
319
|
+
|
|
261
320
|
// Trigger journalist synthesis immediately on completion so the project
|
|
262
321
|
// map is fresh for the next agent that spawns (don't wait for 120s cycle)
|
|
263
322
|
if (finalStatus === 'completed' && this.daemon.journalist) {
|
|
@@ -23,6 +23,7 @@ export class Registry extends EventEmitter {
|
|
|
23
23
|
permission: config.permission || 'full',
|
|
24
24
|
workingDir: config.workingDir || process.cwd(),
|
|
25
25
|
skills: config.skills || [],
|
|
26
|
+
integrations: config.integrations || [],
|
|
26
27
|
status: 'starting',
|
|
27
28
|
pid: null,
|
|
28
29
|
spawnedAt: new Date().toISOString(),
|
|
@@ -49,7 +50,7 @@ export class Registry extends EventEmitter {
|
|
|
49
50
|
if (!agent) return null;
|
|
50
51
|
|
|
51
52
|
// Only allow known fields to prevent prototype pollution
|
|
52
|
-
const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills'];
|
|
53
|
+
const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations'];
|
|
53
54
|
for (const key of Object.keys(updates)) {
|
|
54
55
|
if (SAFE_FIELDS.includes(key)) {
|
|
55
56
|
agent[key] = updates[key];
|