groove-dev 0.27.14 → 0.27.17
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 +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +587 -68
- package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
- package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
- package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
- package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
- package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
- package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
- package/node_modules/@groove-dev/daemon/src/index.js +172 -19
- package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
- package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
- package/node_modules/@groove-dev/daemon/src/process.js +140 -23
- package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
- package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
- package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
- package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
- package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -2
- package/node_modules/@groove-dev/gui/index.html +1 -0
- package/node_modules/@groove-dev/gui/src/app.css +7 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
- package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
- package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
- package/package.json +1 -1
- package/packages/daemon/src/api.js +587 -68
- package/packages/daemon/src/classifier.js +24 -0
- package/packages/daemon/src/credentials.js +12 -2
- package/packages/daemon/src/federation/ambassador.js +204 -0
- package/packages/daemon/src/federation/connection.js +359 -0
- package/packages/daemon/src/federation/contracts.js +112 -0
- package/packages/daemon/src/federation/whitelist.js +190 -0
- package/packages/daemon/src/federation.js +166 -7
- package/packages/daemon/src/index.js +172 -19
- package/packages/daemon/src/introducer.js +52 -7
- package/packages/daemon/src/journalist.js +46 -1
- package/packages/daemon/src/memory.js +36 -16
- package/packages/daemon/src/process.js +140 -23
- package/packages/daemon/src/providers/base.js +1 -0
- package/packages/daemon/src/providers/claude-code.js +1 -0
- package/packages/daemon/src/providers/codex.js +124 -28
- package/packages/daemon/src/providers/gemini.js +104 -17
- package/packages/daemon/src/providers/index.js +17 -0
- package/packages/daemon/src/registry.js +10 -1
- package/packages/daemon/src/rotator.js +93 -30
- package/packages/daemon/src/skills.js +33 -3
- package/packages/daemon/src/terminal-pty.js +9 -1
- package/packages/daemon/src/tool-executor.js +11 -5
- package/packages/daemon/src/toys.js +69 -0
- package/packages/daemon/src/tunnel-manager.js +24 -5
- package/packages/daemon/templates/toys-catalog.json +242 -0
- package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/packages/gui/dist/index.html +3 -2
- package/packages/gui/index.html +1 -0
- package/packages/gui/src/app.css +7 -0
- package/packages/gui/src/app.jsx +37 -10
- package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
- package/packages/gui/src/components/agents/agent-config.jsx +11 -6
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/packages/gui/src/components/editor/code-editor.jsx +33 -2
- package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/packages/gui/src/components/editor/goto-line.jsx +35 -0
- package/packages/gui/src/components/editor/terminal.jsx +12 -6
- package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
- package/packages/gui/src/components/layout/app-shell.jsx +0 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/packages/gui/src/components/layout/command-palette.jsx +6 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
- package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
- package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
- package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
- package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
- package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/packages/gui/src/components/settings/server-detail.jsx +310 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
- package/packages/gui/src/components/settings/server-list.jsx +59 -0
- package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/packages/gui/src/components/toys/toy-card.jsx +78 -0
- package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
- package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/packages/gui/src/components/ui/toast.jsx +2 -2
- package/packages/gui/src/lib/electron.js +15 -0
- package/packages/gui/src/lib/format.js +1 -0
- package/packages/gui/src/stores/groove.js +373 -58
- package/packages/gui/src/views/agents.jsx +148 -42
- package/packages/gui/src/views/editor.jsx +92 -2
- package/packages/gui/src/views/federation.jsx +37 -0
- package/packages/gui/src/views/marketplace.jsx +2 -42
- package/packages/gui/src/views/settings.jsx +32 -132
- package/packages/gui/src/views/subscription-panel.jsx +327 -0
- package/packages/gui/src/views/teams.jsx +3 -3
- package/packages/gui/src/views/toys.jsx +162 -0
- package/plans/chat-persistence-refactor.md +154 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
- package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
|
@@ -40,6 +40,7 @@ import { TunnelManager } from './tunnel-manager.js';
|
|
|
40
40
|
import { ModelManager } from './model-manager.js';
|
|
41
41
|
import { LlamaServerManager } from './llama-server.js';
|
|
42
42
|
import { RepoImporter } from './repo-import.js';
|
|
43
|
+
import { Toys } from './toys.js';
|
|
43
44
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
44
45
|
|
|
45
46
|
const DEFAULT_PORT = 31415;
|
|
@@ -141,26 +142,48 @@ export class Daemon {
|
|
|
141
142
|
this.mcpManager = new McpManager(this);
|
|
142
143
|
this.tunnelManager = new TunnelManager(this);
|
|
143
144
|
this.repoImporter = new RepoImporter(this);
|
|
145
|
+
this.toys = new Toys(this);
|
|
146
|
+
|
|
147
|
+
// Subscription state (populated by Electron IPC or direct auth)
|
|
148
|
+
this.authToken = null;
|
|
149
|
+
this.subscriptionCache = { plan: 'community', status: 'none', features: [], active: false, validatedAt: 0 };
|
|
144
150
|
|
|
145
151
|
// HTTP + WebSocket server
|
|
146
152
|
this.app = express();
|
|
147
153
|
this.server = createHttpServer(this.app);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
|
|
155
|
+
const verifyOrigin = (req) => {
|
|
156
|
+
const origin = req.headers.origin;
|
|
157
|
+
if (!origin) return true;
|
|
158
|
+
try {
|
|
159
|
+
const url = new URL(origin);
|
|
160
|
+
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') return true;
|
|
161
|
+
if (this.host !== DEFAULT_HOST && url.hostname === this.host) return true;
|
|
162
|
+
} catch { /* invalid origin */ }
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
this.wss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
|
|
167
|
+
this.federationWss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
|
|
168
|
+
|
|
169
|
+
this.server.on('upgrade', (req, socket, head) => {
|
|
170
|
+
if (!verifyOrigin(req) && !req.url?.startsWith('/ws/federation')) {
|
|
171
|
+
socket.destroy();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (req.url?.startsWith('/ws/federation')) {
|
|
175
|
+
const daemonId = req.headers['x-groove-daemonid'];
|
|
176
|
+
const signatureHeader = req.headers['x-groove-signature'] || '';
|
|
177
|
+
const callerIp = req.socket?.remoteAddress?.replace('::ffff:', '') || '';
|
|
178
|
+
if (!daemonId) { socket.destroy(); return; }
|
|
179
|
+
this.federationWss.handleUpgrade(req, socket, head, (ws) => {
|
|
180
|
+
this.federation.handleWsUpgrade(ws, daemonId, callerIp, signatureHeader);
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
184
|
+
this.wss.emit('connection', ws, req);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
164
187
|
});
|
|
165
188
|
|
|
166
189
|
// Wire up API routes
|
|
@@ -203,18 +226,38 @@ export class Daemon {
|
|
|
203
226
|
ws.on('message', (raw) => {
|
|
204
227
|
try {
|
|
205
228
|
const msg = JSON.parse(raw);
|
|
229
|
+
|
|
230
|
+
// Validate message type against whitelist
|
|
231
|
+
const VALID_WS_TYPES = new Set([
|
|
232
|
+
'terminal:spawn', 'terminal:resize', 'terminal:input', 'terminal:close', 'terminal:kill', 'terminal:rename',
|
|
233
|
+
'editor:watch', 'editor:unwatch', 'editor:save',
|
|
234
|
+
'ping'
|
|
235
|
+
]);
|
|
236
|
+
if (!msg || typeof msg !== 'object' || !VALID_WS_TYPES.has(msg.type)) return;
|
|
237
|
+
if (Object.hasOwn(msg, '__proto__') || Object.hasOwn(msg, 'constructor')) return;
|
|
238
|
+
|
|
206
239
|
switch (msg.type) {
|
|
207
240
|
// File editor
|
|
208
241
|
case 'editor:watch':
|
|
209
|
-
if (msg.path
|
|
242
|
+
if (msg.path && typeof msg.path === 'string' && !msg.path.includes('..')) {
|
|
243
|
+
this.fileWatcher.watch(msg.path); watchedFiles.add(msg.path);
|
|
244
|
+
}
|
|
210
245
|
break;
|
|
211
246
|
case 'editor:unwatch':
|
|
212
247
|
if (msg.path) { this.fileWatcher.unwatch(msg.path); watchedFiles.delete(msg.path); }
|
|
213
248
|
break;
|
|
214
249
|
// Terminal
|
|
215
250
|
case 'terminal:spawn': {
|
|
216
|
-
|
|
217
|
-
|
|
251
|
+
if (msg.cwd !== undefined && (typeof msg.cwd !== 'string' || msg.cwd.includes('..'))) break;
|
|
252
|
+
if (msg.cols !== undefined && (typeof msg.cols !== 'number' || msg.cols < 1 || msg.cols > 500)) break;
|
|
253
|
+
if (msg.rows !== undefined && (typeof msg.rows !== 'number' || msg.rows < 1 || msg.rows > 200)) break;
|
|
254
|
+
try {
|
|
255
|
+
const id = this.terminalManager.spawn(ws, { cwd: msg.cwd, cols: msg.cols, rows: msg.rows });
|
|
256
|
+
ws.send(JSON.stringify({ type: 'terminal:spawned', id }));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error('[terminal] spawn error:', err);
|
|
259
|
+
ws.send(JSON.stringify({ type: 'terminal:error', message: err.message }));
|
|
260
|
+
}
|
|
218
261
|
break;
|
|
219
262
|
}
|
|
220
263
|
case 'terminal:input':
|
|
@@ -226,6 +269,13 @@ export class Daemon {
|
|
|
226
269
|
case 'terminal:kill':
|
|
227
270
|
if (msg.id) this.terminalManager.kill(msg.id);
|
|
228
271
|
break;
|
|
272
|
+
case 'terminal:rename':
|
|
273
|
+
if (msg.id && typeof msg.label === 'string') {
|
|
274
|
+
if (this.terminalManager.rename(msg.id, msg.label)) {
|
|
275
|
+
this.broadcast({ type: 'terminal:renamed', id: msg.id, label: msg.label });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
229
279
|
}
|
|
230
280
|
} catch { /* ignore malformed messages */ }
|
|
231
281
|
});
|
|
@@ -248,6 +298,7 @@ export class Daemon {
|
|
|
248
298
|
}
|
|
249
299
|
|
|
250
300
|
broadcast(message) {
|
|
301
|
+
if (!this.wss) return;
|
|
251
302
|
const payload = JSON.stringify(message);
|
|
252
303
|
for (const client of this.wss.clients) {
|
|
253
304
|
if (client.readyState === 1) {
|
|
@@ -256,6 +307,66 @@ export class Daemon {
|
|
|
256
307
|
}
|
|
257
308
|
}
|
|
258
309
|
|
|
310
|
+
async setAuthToken(token) {
|
|
311
|
+
this.authToken = token;
|
|
312
|
+
if (token) {
|
|
313
|
+
await this._pollSubscription();
|
|
314
|
+
// Fallback: if external API failed, try syncing from stored user data
|
|
315
|
+
if (!this.subscriptionCache?.active) {
|
|
316
|
+
this.skills?._syncSubscriptionCache(this.skills?.getUser());
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
this.subscriptionCache = { plan: 'community', status: 'none', features: [], active: false, validatedAt: Date.now() };
|
|
320
|
+
this.broadcast({ type: 'subscription:updated', data: this.subscriptionCache });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async _pollSubscription() {
|
|
325
|
+
if (!this.authToken) return;
|
|
326
|
+
const API_BASE = 'https://docs.groovedev.ai/api/v1';
|
|
327
|
+
const delays = [0, 5000, 15000, 30000];
|
|
328
|
+
for (let attempt = 0; attempt < delays.length; attempt++) {
|
|
329
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, delays[attempt]));
|
|
330
|
+
try {
|
|
331
|
+
const resp = await fetch(`${API_BASE}/subscription/status`, {
|
|
332
|
+
headers: { 'Authorization': `Bearer ${this.authToken}` },
|
|
333
|
+
signal: AbortSignal.timeout(10000),
|
|
334
|
+
});
|
|
335
|
+
if (resp.status === 401) {
|
|
336
|
+
this.subscriptionCache = { plan: 'community', status: 'none', features: [], active: false, validatedAt: Date.now() };
|
|
337
|
+
this.broadcast({ type: 'subscription:updated', data: this.subscriptionCache });
|
|
338
|
+
this.broadcast({ type: 'auth:expired' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
342
|
+
const data = await resp.json();
|
|
343
|
+
this.subscriptionCache = {
|
|
344
|
+
plan: data.plan || 'community',
|
|
345
|
+
status: data.status || 'none',
|
|
346
|
+
features: data.features || [],
|
|
347
|
+
active: data.status === 'active' || data.status === 'trialing',
|
|
348
|
+
seats: data.seats || 1,
|
|
349
|
+
periodEnd: data.periodEnd || null,
|
|
350
|
+
cancelAtPeriodEnd: data.cancelAtPeriodEnd || false,
|
|
351
|
+
validatedAt: Date.now(),
|
|
352
|
+
};
|
|
353
|
+
this.broadcast({ type: 'subscription:updated', data: this.subscriptionCache });
|
|
354
|
+
return;
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (attempt < delays.length - 1) {
|
|
357
|
+
console.log(`[Groove:Subscription] Attempt ${attempt + 1} failed, retrying in ${delays[attempt + 1] / 1000}s...`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (this.subscriptionCache?.validatedAt && (Date.now() - this.subscriptionCache.validatedAt < 72 * 3600 * 1000)) {
|
|
361
|
+
console.log('[Groove:Subscription] External API unreachable, keeping cached subscription');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
this.subscriptionCache = { plan: 'community', status: 'none', features: [], active: false, validatedAt: 0 };
|
|
365
|
+
this.broadcast({ type: 'subscription:updated', data: this.subscriptionCache });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
259
370
|
async start() {
|
|
260
371
|
// Kill any existing daemon on our port
|
|
261
372
|
if (existsSync(this.pidFile)) {
|
|
@@ -317,6 +428,8 @@ export class Daemon {
|
|
|
317
428
|
|
|
318
429
|
return new Promise((resolvePromise) => {
|
|
319
430
|
this.server.listen(this.port, this.host, () => {
|
|
431
|
+
// Read back actual port (critical for port 0 / dynamic allocation)
|
|
432
|
+
this.port = this.server.address().port;
|
|
320
433
|
writeFileSync(this.pidFile, String(process.pid));
|
|
321
434
|
// Write actual port and host so CLI can find us
|
|
322
435
|
writeFileSync(resolve(this.grooveDir, 'daemon.port'), String(this.port));
|
|
@@ -330,8 +443,34 @@ export class Daemon {
|
|
|
330
443
|
this.scheduler.start();
|
|
331
444
|
this.timeline.start();
|
|
332
445
|
this.gateways.start();
|
|
446
|
+
this.federation.initialize();
|
|
333
447
|
this._startGarbageCollector();
|
|
334
448
|
|
|
449
|
+
// Restore auth token from stored config so subscription polling works after restart
|
|
450
|
+
const storedToken = this.skills.getToken();
|
|
451
|
+
if (storedToken) {
|
|
452
|
+
this.authToken = storedToken;
|
|
453
|
+
this._pollSubscription().catch(() => {});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Re-validate subscription every 30 minutes
|
|
457
|
+
this._subscriptionPollInterval = setInterval(() => {
|
|
458
|
+
this._pollSubscription().catch(() => {});
|
|
459
|
+
}, 30 * 60 * 1000);
|
|
460
|
+
|
|
461
|
+
// Classifier broadcasting — decoupled from stdout handler
|
|
462
|
+
// Runs every 30s, checks for classification changes, broadcasts to GUI
|
|
463
|
+
this._classifierInterval = setInterval(() => {
|
|
464
|
+
try {
|
|
465
|
+
const updates = this.classifier.getUpdates();
|
|
466
|
+
for (const update of updates) {
|
|
467
|
+
this.broadcast({ type: 'classifier:update', data: update });
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
// Never let classifier broadcasting break the daemon
|
|
471
|
+
}
|
|
472
|
+
}, 30_000);
|
|
473
|
+
|
|
335
474
|
// Scan codebase for workspace/structure awareness
|
|
336
475
|
this.indexer.scan();
|
|
337
476
|
|
|
@@ -353,6 +492,9 @@ export class Daemon {
|
|
|
353
492
|
this.tokens.setProjectStats(stats.totalFiles, stats.totalDirs);
|
|
354
493
|
}
|
|
355
494
|
|
|
495
|
+
// Auto-connect saved tunnels that have autoConnect enabled
|
|
496
|
+
this.tunnelManager.init();
|
|
497
|
+
|
|
356
498
|
resolvePromise(this);
|
|
357
499
|
});
|
|
358
500
|
});
|
|
@@ -362,6 +504,11 @@ export class Daemon {
|
|
|
362
504
|
// Run once on startup, then every 24 hours
|
|
363
505
|
this._gc();
|
|
364
506
|
this._gcInterval = setInterval(() => this._gc(), 24 * 60 * 60 * 1000);
|
|
507
|
+
|
|
508
|
+
// Periodic state save — crash protection (every 30s)
|
|
509
|
+
this._stateSaveInterval = setInterval(() => {
|
|
510
|
+
try { this.state.set('agents', this.registry.getAll()); this.state.save(); } catch {}
|
|
511
|
+
}, 30000);
|
|
365
512
|
}
|
|
366
513
|
|
|
367
514
|
_gc() {
|
|
@@ -449,11 +596,17 @@ export class Daemon {
|
|
|
449
596
|
this.scheduler.stop();
|
|
450
597
|
this.timeline.stop();
|
|
451
598
|
if (this._gcInterval) clearInterval(this._gcInterval);
|
|
599
|
+
if (this._stateSaveInterval) clearInterval(this._stateSaveInterval);
|
|
600
|
+
if (this._classifierInterval) clearInterval(this._classifierInterval);
|
|
601
|
+
if (this._subscriptionPollInterval) clearInterval(this._subscriptionPollInterval);
|
|
452
602
|
|
|
453
603
|
// Clean up file watchers and terminal sessions
|
|
454
604
|
this.fileWatcher.unwatchAll();
|
|
455
605
|
this.terminalManager.killAll();
|
|
456
606
|
|
|
607
|
+
// Clean up federation (whitelist probing, connections, ambassadors)
|
|
608
|
+
this.federation.destroy();
|
|
609
|
+
|
|
457
610
|
// Disconnect all SSH tunnels
|
|
458
611
|
this.tunnelManager.shutdown();
|
|
459
612
|
|
|
@@ -14,7 +14,7 @@ export class Introducer {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
generateContext(newAgent, options = {}) {
|
|
17
|
-
const { taskNegotiation } = options;
|
|
17
|
+
const { taskNegotiation, hasTask } = options;
|
|
18
18
|
const agents = this.daemon.registry.getAll();
|
|
19
19
|
// Only include ACTIVE agents — not completed/killed ones from previous sessions
|
|
20
20
|
// Completed agents' work is captured in the journalist's project map, not here
|
|
@@ -178,11 +178,11 @@ export class Introducer {
|
|
|
178
178
|
lines.push(`If you see files that seem unrelated to your task, leave them alone — they belong to another project or agent.`);
|
|
179
179
|
|
|
180
180
|
// Memory containment — prevent agents from reading/writing auto-memory
|
|
181
|
-
//
|
|
181
|
+
// GROOVE manages project memory automatically via Layer 7
|
|
182
182
|
lines.push('');
|
|
183
183
|
lines.push(`## Memory Policy`);
|
|
184
184
|
lines.push('');
|
|
185
|
-
lines.push(`
|
|
185
|
+
lines.push(`GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.`);
|
|
186
186
|
lines.push(`GROOVE provides all your project context through handoff briefs, AGENTS_REGISTRY.md, and GROOVE_PROJECT_MAP.md.`);
|
|
187
187
|
lines.push(`Do NOT save memories — your state is managed by GROOVE's rotation and handoff system.`);
|
|
188
188
|
|
|
@@ -312,15 +312,24 @@ export class Introducer {
|
|
|
312
312
|
lines.push('To clone a NEW GitHub repo, use: `POST http://localhost:31415/api/repos/import` with `{ "repoUrl": "...", "targetPath": "~/Projects/name", "createTeam": true }`. Do NOT run `git clone` directly.');
|
|
313
313
|
|
|
314
314
|
// Surface stored API keys so agents know what's available in their environment
|
|
315
|
+
// Only inject provider API keys (codex, gemini, ollama) and integration credentials
|
|
316
|
+
// that are relevant to this agent's attached integrations — skip OAuth boilerplate
|
|
315
317
|
const KEY_MAP = { codex: 'OPENAI_API_KEY', gemini: 'GEMINI_API_KEY', ollama: 'OLLAMA_API_KEY' };
|
|
318
|
+
const agentIntegrations = new Set(newAgent.integrations || []);
|
|
316
319
|
try {
|
|
317
320
|
const credProviders = this.daemon.credentials?.listProviders() || [];
|
|
318
|
-
|
|
321
|
+
const relevant = credProviders.filter((cp) => {
|
|
322
|
+
if (KEY_MAP[cp.provider]) return true;
|
|
323
|
+
if (!cp.provider.startsWith('integration:')) return true;
|
|
324
|
+
const parts = cp.provider.split(':');
|
|
325
|
+
return parts.length >= 2 && agentIntegrations.has(parts[1]);
|
|
326
|
+
});
|
|
327
|
+
if (relevant.length > 0) {
|
|
319
328
|
lines.push('');
|
|
320
329
|
lines.push('## Available API Keys');
|
|
321
330
|
lines.push('');
|
|
322
331
|
lines.push('GROOVE has API keys stored and injected into your environment. Do NOT ask the user for these:');
|
|
323
|
-
for (const cp of
|
|
332
|
+
for (const cp of relevant) {
|
|
324
333
|
const envVar = KEY_MAP[cp.provider];
|
|
325
334
|
if (envVar) {
|
|
326
335
|
lines.push(`- **${cp.provider}**: available as \`${envVar}\` in your environment`);
|
|
@@ -333,7 +342,43 @@ export class Introducer {
|
|
|
333
342
|
}
|
|
334
343
|
} catch { /* credentials not available */ }
|
|
335
344
|
|
|
336
|
-
|
|
345
|
+
// --- Layer 7: Project Memory (injected at end, bounded) ---
|
|
346
|
+
let memorySection = '';
|
|
347
|
+
try {
|
|
348
|
+
if (this.daemon.memory) {
|
|
349
|
+
const parts = [];
|
|
350
|
+
|
|
351
|
+
const constraints = this.daemon.memory.getConstraintsMarkdown(2000);
|
|
352
|
+
if (constraints) {
|
|
353
|
+
parts.push(`### Constraints (read carefully)\n${constraints}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (hasTask) {
|
|
357
|
+
const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 15, 1000);
|
|
358
|
+
if (discoveries) {
|
|
359
|
+
parts.push(`### Known Fixes for ${newAgent.role} Role\n${discoveries}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const handoffs = this.daemon.memory.getRecentHandoffMarkdown(newAgent.role, 2, 1000, newAgent.workingDir);
|
|
363
|
+
if (handoffs) {
|
|
364
|
+
parts.push(`### Recent Handoff History\n${handoffs}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (parts.length > 0) {
|
|
369
|
+
memorySection = `\n## Project Memory (auto-generated)\n\n${parts.join('\n\n')}\n`;
|
|
370
|
+
// Hard budget: 4K chars total
|
|
371
|
+
if (memorySection.length > 4000) {
|
|
372
|
+
memorySection = memorySection.slice(0, 3997) + '...';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
// Memory injection must never break agent spawn
|
|
378
|
+
memorySection = '';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return lines.join('\n') + memorySection;
|
|
337
382
|
}
|
|
338
383
|
|
|
339
384
|
loadArchitectureDoc() {
|
|
@@ -416,7 +461,7 @@ export class Introducer {
|
|
|
416
461
|
'',
|
|
417
462
|
`See AGENTS_REGISTRY.md for full agent state.`,
|
|
418
463
|
'',
|
|
419
|
-
`**Memory policy:**
|
|
464
|
+
`**Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.`,
|
|
420
465
|
'',
|
|
421
466
|
GROOVE_SECTION_END,
|
|
422
467
|
].filter(Boolean).join('\n');
|
|
@@ -92,6 +92,10 @@ export class Journalist {
|
|
|
92
92
|
this.writeDecisionsLog(synthesis.decisions);
|
|
93
93
|
this.writeAgentSessionLogs(activeAgents, filteredLogs);
|
|
94
94
|
|
|
95
|
+
// Post-synthesis: extract Layer 7 memory from agent logs
|
|
96
|
+
this._extractDiscoveries(filteredLogs);
|
|
97
|
+
this._extractConstraints(filteredLogs);
|
|
98
|
+
|
|
95
99
|
this.lastSynthesis = synthesis;
|
|
96
100
|
this.history.push({
|
|
97
101
|
cycle: this.cycleCount,
|
|
@@ -733,7 +737,7 @@ export class Journalist {
|
|
|
733
737
|
// Pull recent rotation history from persistent memory (Layer 7).
|
|
734
738
|
// Gives the new agent causal continuity: what the last 3 agents struggled
|
|
735
739
|
// with, decided, and solved — not just what the current session did.
|
|
736
|
-
const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000) || '';
|
|
740
|
+
const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000, agent.workingDir) || '';
|
|
737
741
|
|
|
738
742
|
// Pull the user's recent messages to this agent so the new instance
|
|
739
743
|
// can continue the conversation naturally instead of restarting it.
|
|
@@ -967,6 +971,47 @@ export class Journalist {
|
|
|
967
971
|
return all.sort((a, b) => a.timestamp.localeCompare(b.timestamp)).slice(-30);
|
|
968
972
|
}
|
|
969
973
|
|
|
974
|
+
// --- Layer 7 Memory Extraction ---
|
|
975
|
+
|
|
976
|
+
_extractDiscoveries(filteredLogs) {
|
|
977
|
+
if (!this.daemon.memory) return;
|
|
978
|
+
|
|
979
|
+
try {
|
|
980
|
+
for (const [agentId, data] of Object.entries(filteredLogs)) {
|
|
981
|
+
const { agent, entries } = data;
|
|
982
|
+
if (!entries || entries.length === 0) continue;
|
|
983
|
+
|
|
984
|
+
for (let i = 0; i < entries.length - 1; i++) {
|
|
985
|
+
const entry = entries[i];
|
|
986
|
+
if (entry.type !== 'error') continue;
|
|
987
|
+
|
|
988
|
+
const trigger = (entry.text || '').trim();
|
|
989
|
+
if (trigger.length < 20) continue;
|
|
990
|
+
|
|
991
|
+
for (let j = i + 1; j < Math.min(i + 6, entries.length); j++) {
|
|
992
|
+
const next = entries[j];
|
|
993
|
+
if (next.type !== 'tool' || (next.tool !== 'Edit' && next.tool !== 'Write')) continue;
|
|
994
|
+
const fix = (next.input || '').trim();
|
|
995
|
+
if (fix.length < 10) continue;
|
|
996
|
+
|
|
997
|
+
this.daemon.memory.addDiscovery({
|
|
998
|
+
agentId: agent.id,
|
|
999
|
+
role: agent.role,
|
|
1000
|
+
trigger: trigger.slice(0, 300),
|
|
1001
|
+
fix: fix.slice(0, 500),
|
|
1002
|
+
outcome: 'success',
|
|
1003
|
+
});
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} catch { /* Memory extraction must never break journalist cycle */ }
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
_extractConstraints(filteredLogs) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
970
1015
|
// --- Accessors ---
|
|
971
1016
|
|
|
972
1017
|
getLastSynthesis() {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Read by the introducer on every spawn so agent #50 knows what agent #1 learned.
|
|
11
11
|
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync, statSync } from 'fs';
|
|
13
|
-
import { resolve } from 'path';
|
|
13
|
+
import { resolve, relative } from 'path';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
15
|
|
|
16
16
|
const MAX_CONSTRAINTS = 50;
|
|
@@ -34,6 +34,7 @@ function truncate(text, max) {
|
|
|
34
34
|
export class MemoryStore {
|
|
35
35
|
constructor(grooveDir) {
|
|
36
36
|
this.memDir = resolve(grooveDir, 'memory');
|
|
37
|
+
this.projectDir = resolve(grooveDir, '..');
|
|
37
38
|
this.constraintsPath = resolve(this.memDir, 'project-constraints.md');
|
|
38
39
|
this.handoffDir = resolve(this.memDir, 'handoff-chain');
|
|
39
40
|
this.discoveriesPath = resolve(this.memDir, 'agent-discoveries.jsonl');
|
|
@@ -133,19 +134,29 @@ export class MemoryStore {
|
|
|
133
134
|
|
|
134
135
|
// --- Handoff Chain ---
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
_workspaceSlug(workingDir) {
|
|
138
|
+
if (!workingDir) return '';
|
|
139
|
+
const rel = relative(this.projectDir, workingDir);
|
|
140
|
+
if (!rel || rel === '.' || rel.startsWith('..')) return '';
|
|
141
|
+
return safeName(rel);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_chainPath(role, workingDir) {
|
|
145
|
+
const slug = this._workspaceSlug(workingDir);
|
|
146
|
+
if (slug) {
|
|
147
|
+
const dir = resolve(this.handoffDir, slug);
|
|
148
|
+
mkdirSync(dir, { recursive: true });
|
|
149
|
+
return resolve(dir, `${safeName(role)}.md`);
|
|
150
|
+
}
|
|
137
151
|
return resolve(this.handoffDir, `${safeName(role)}.md`);
|
|
138
152
|
}
|
|
139
153
|
|
|
140
|
-
getHandoffChain(role) {
|
|
141
|
-
const path = this._chainPath(role);
|
|
154
|
+
getHandoffChain(role, workingDir) {
|
|
155
|
+
const path = this._chainPath(role, workingDir);
|
|
142
156
|
if (!existsSync(path)) return [];
|
|
143
157
|
try {
|
|
144
158
|
const content = readFileSync(path, 'utf8');
|
|
145
159
|
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
160
|
const blocks = content.split(/\n(?=## Rotation )/);
|
|
150
161
|
for (const block of blocks) {
|
|
151
162
|
const headerMatch = block.match(/^## Rotation (\d+) —/);
|
|
@@ -162,9 +173,9 @@ export class MemoryStore {
|
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
appendHandoffBrief(role, entry) {
|
|
176
|
+
appendHandoffBrief(role, entry, workingDir) {
|
|
166
177
|
if (!role || !entry) return false;
|
|
167
|
-
const chain = this.getHandoffChain(role);
|
|
178
|
+
const chain = this.getHandoffChain(role, workingDir);
|
|
168
179
|
const nextN = (chain[0]?.rotationN || 0) + 1;
|
|
169
180
|
|
|
170
181
|
const block = [
|
|
@@ -178,7 +189,6 @@ export class MemoryStore {
|
|
|
178
189
|
'',
|
|
179
190
|
].filter(Boolean).join('\n');
|
|
180
191
|
|
|
181
|
-
// Prepend new entry (newest first), keep last N
|
|
182
192
|
const newChain = [{ rotationN: nextN, body: block }, ...chain].slice(0, MAX_HANDOFF_ROTATIONS);
|
|
183
193
|
|
|
184
194
|
const lines = [
|
|
@@ -193,25 +203,27 @@ export class MemoryStore {
|
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
try {
|
|
196
|
-
writeFileSync(this._chainPath(role), lines.join('\n'));
|
|
206
|
+
writeFileSync(this._chainPath(role, workingDir), lines.join('\n'));
|
|
197
207
|
return true;
|
|
198
208
|
} catch {
|
|
199
209
|
return false;
|
|
200
210
|
}
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
getRecentHandoffMarkdown(role, count = 3, maxChars = 4000) {
|
|
204
|
-
const chain = this.getHandoffChain(role);
|
|
213
|
+
getRecentHandoffMarkdown(role, count = 3, maxChars = 4000, workingDir) {
|
|
214
|
+
const chain = this.getHandoffChain(role, workingDir);
|
|
205
215
|
if (chain.length === 0) return '';
|
|
206
216
|
const recent = chain.slice(0, count);
|
|
207
217
|
const out = recent.map((e) => e.body || '').join('\n\n---\n\n');
|
|
208
218
|
return truncate(out, maxChars);
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
listHandoffRoles() {
|
|
212
|
-
|
|
221
|
+
listHandoffRoles(workingDir) {
|
|
222
|
+
const slug = this._workspaceSlug(workingDir);
|
|
223
|
+
const dir = slug ? resolve(this.handoffDir, slug) : this.handoffDir;
|
|
224
|
+
if (!existsSync(dir)) return [];
|
|
213
225
|
try {
|
|
214
|
-
return readdirSync(
|
|
226
|
+
return readdirSync(dir)
|
|
215
227
|
.filter((f) => f.endsWith('.md'))
|
|
216
228
|
.map((f) => f.replace(/\.md$/, ''));
|
|
217
229
|
} catch {
|
|
@@ -225,6 +237,14 @@ export class MemoryStore {
|
|
|
225
237
|
if (!trigger || !fix) return { added: false, error: 'trigger and fix required' };
|
|
226
238
|
if (outcome !== 'success') return { added: false, reason: 'only successes stored' };
|
|
227
239
|
|
|
240
|
+
const fileExtPattern = /[\w./-]+\.(?:js|ts|json|md|jsx|tsx|css|mjs|cjs)\b/g;
|
|
241
|
+
const triggerTokens = new Set((String(trigger).match(fileExtPattern) || []).map(t => t.toLowerCase()));
|
|
242
|
+
const fixTokens = new Set((String(fix).match(fileExtPattern) || []).map(t => t.toLowerCase()));
|
|
243
|
+
const hasOverlap = [...triggerTokens].some(t => fixTokens.has(t));
|
|
244
|
+
if (triggerTokens.size > 0 && fixTokens.size > 0 && !hasOverlap) {
|
|
245
|
+
return { added: false, reason: 'trigger and fix are unrelated' };
|
|
246
|
+
}
|
|
247
|
+
|
|
228
248
|
const entry = {
|
|
229
249
|
ts: new Date().toISOString(),
|
|
230
250
|
agentId: agentId || null,
|