groove-dev 0.27.32 → 0.27.33
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/start.js +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +28 -1
- package/node_modules/@groove-dev/daemon/src/firstrun.js +1 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +16 -4
- package/node_modules/@groove-dev/daemon/src/memory.js +6 -1
- package/node_modules/@groove-dev/daemon/src/process.js +44 -28
- package/node_modules/@groove-dev/daemon/src/rotator.js +1 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +19 -7
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
- package/node_modules/@groove-dev/gui/src/components/layout/project-picker.jsx +127 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +22 -15
- package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/start.js +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +28 -1
- package/packages/daemon/src/firstrun.js +1 -0
- package/packages/daemon/src/index.js +14 -0
- package/packages/daemon/src/journalist.js +16 -4
- package/packages/daemon/src/memory.js +6 -1
- package/packages/daemon/src/process.js +44 -28
- package/packages/daemon/src/rotator.js +1 -0
- package/packages/daemon/src/tunnel-manager.js +19 -7
- package/packages/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
- package/packages/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/layout/app-shell.jsx +2 -0
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
- package/packages/gui/src/components/layout/project-picker.jsx +127 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +22 -15
- package/packages/gui/src/stores/groove.js +39 -2
|
@@ -19,7 +19,7 @@ export async function start(options) {
|
|
|
19
19
|
setupKeys = result.keys || {};
|
|
20
20
|
} catch (err) {
|
|
21
21
|
// If stdin is not interactive (piped), skip wizard
|
|
22
|
-
if (err.code === 'ERR_USE_AFTER_CLOSE') {
|
|
22
|
+
if (err.code === 'ERR_USE_AFTER_CLOSE' || !process.stdin.isTTY) {
|
|
23
23
|
console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
|
|
24
24
|
} else {
|
|
25
25
|
throw err;
|
|
@@ -695,6 +695,28 @@ export function createApi(app, daemon) {
|
|
|
695
695
|
});
|
|
696
696
|
});
|
|
697
697
|
|
|
698
|
+
// --- Project Directory ---
|
|
699
|
+
|
|
700
|
+
app.get('/api/project-dir', (req, res) => {
|
|
701
|
+
res.json({
|
|
702
|
+
projectDir: daemon.projectDir,
|
|
703
|
+
recentProjects: daemon.config.recentProjects || [],
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
app.post('/api/project-dir', (req, res) => {
|
|
708
|
+
const { path: dirPath } = req.body || {};
|
|
709
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
710
|
+
return res.status(400).json({ error: 'path is required' });
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
daemon.setProjectDir(dirPath);
|
|
714
|
+
res.json({ projectDir: daemon.projectDir, recentProjects: daemon.config.recentProjects || [] });
|
|
715
|
+
} catch (err) {
|
|
716
|
+
res.status(400).json({ error: err.message });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
698
720
|
// --- Teams (live agent groups) ---
|
|
699
721
|
|
|
700
722
|
app.get('/api/teams', (req, res) => {
|
|
@@ -3330,7 +3352,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3330
3352
|
|
|
3331
3353
|
app.post('/api/tunnels/:id/connect', proOnly, async (req, res) => {
|
|
3332
3354
|
try {
|
|
3333
|
-
const
|
|
3355
|
+
const opts = {};
|
|
3356
|
+
if (req.body?.skipTest && req.body?.testResult) {
|
|
3357
|
+
opts.skipTest = true;
|
|
3358
|
+
opts.testResult = req.body.testResult;
|
|
3359
|
+
}
|
|
3360
|
+
const result = await daemon.tunnelManager.connect(req.params.id, opts);
|
|
3334
3361
|
res.json(result);
|
|
3335
3362
|
} catch (err) {
|
|
3336
3363
|
const body = { error: err.message };
|
|
@@ -337,6 +337,20 @@ export class Daemon {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
setProjectDir(dirPath) {
|
|
341
|
+
if (!dirPath || typeof dirPath !== 'string') throw new Error('Invalid path');
|
|
342
|
+
const resolved = resolve(dirPath);
|
|
343
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
344
|
+
throw new Error('Directory does not exist');
|
|
345
|
+
}
|
|
346
|
+
this.projectDir = resolved;
|
|
347
|
+
const recents = (this.config.recentProjects || []).filter((r) => r.path !== resolved);
|
|
348
|
+
recents.unshift({ path: resolved, name: resolved.split('/').pop(), openedAt: new Date().toISOString() });
|
|
349
|
+
this.config.recentProjects = recents.slice(0, 10);
|
|
350
|
+
saveConfig(this.grooveDir, this.config);
|
|
351
|
+
this.broadcast({ type: 'project-dir:changed', data: { projectDir: resolved } });
|
|
352
|
+
}
|
|
353
|
+
|
|
340
354
|
async _pollSubscription() {
|
|
341
355
|
if (!this.authToken) return;
|
|
342
356
|
const API_BASE = 'https://docs.groovedev.ai/api/v1';
|
|
@@ -22,6 +22,7 @@ export class Journalist {
|
|
|
22
22
|
this.history = []; // recent synthesis summaries
|
|
23
23
|
this._debounceTimer = null;
|
|
24
24
|
this._debounceReason = null;
|
|
25
|
+
this._forceNextCycle = false;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
start(intervalMs = DEFAULT_INTERVAL) {
|
|
@@ -61,6 +62,9 @@ export class Journalist {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
requestSynthesis(reason = 'unknown') {
|
|
65
|
+
if (reason === 'completion' || reason === 'rotation_complete') {
|
|
66
|
+
this._forceNextCycle = true;
|
|
67
|
+
}
|
|
64
68
|
if (this._debounceTimer) {
|
|
65
69
|
this._debounceReason = reason;
|
|
66
70
|
return;
|
|
@@ -84,7 +88,10 @@ export class Journalist {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
async cycle() {
|
|
87
|
-
if (this.synthesizing)
|
|
91
|
+
if (this.synthesizing) {
|
|
92
|
+
console.log(' Journalist: skipping cycle (already synthesizing)');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
88
95
|
|
|
89
96
|
const agents = this.daemon.registry.getAll();
|
|
90
97
|
const running = agents.filter((a) => a.status === 'running');
|
|
@@ -99,14 +106,19 @@ export class Journalist {
|
|
|
99
106
|
const activeAgents = [...running, ...recentlyCompleted];
|
|
100
107
|
|
|
101
108
|
// Skip if no agents to synthesize
|
|
102
|
-
if (activeAgents.length === 0)
|
|
109
|
+
if (activeAgents.length === 0) {
|
|
110
|
+
console.log(' Journalist: skipping cycle (no active agents)');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
103
113
|
|
|
104
|
-
// Smart scheduling: skip if no new log output since last cycle
|
|
105
|
-
if (this.lastCycleAt && !this.hasNewActivity(activeAgents)) {
|
|
114
|
+
// Smart scheduling: skip if no new log output since last cycle (unless forced by completion/rotation)
|
|
115
|
+
if (this.lastCycleAt && !this._forceNextCycle && !this.hasNewActivity(activeAgents)) {
|
|
116
|
+
console.log(' Journalist: skipping cycle (no new activity)');
|
|
106
117
|
return;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
this.synthesizing = true;
|
|
121
|
+
this._forceNextCycle = false;
|
|
110
122
|
this.cycleCount++;
|
|
111
123
|
this.lastCycleAt = Date.now();
|
|
112
124
|
|
|
@@ -165,12 +165,13 @@ export class MemoryStore {
|
|
|
165
165
|
const entries = [];
|
|
166
166
|
const blocks = content.split(/\n(?=## Rotation )/);
|
|
167
167
|
for (const block of blocks) {
|
|
168
|
-
const headerMatch = block.match(/^## Rotation (\d+)
|
|
168
|
+
const headerMatch = block.match(/^## Rotation (\d+) — [^(]*\(([\w?-]+) →/);
|
|
169
169
|
if (!headerMatch) continue;
|
|
170
170
|
const body = block.replace(/\n---\s*$/, '').trim();
|
|
171
171
|
entries.push({
|
|
172
172
|
rotationN: parseInt(headerMatch[1], 10),
|
|
173
173
|
body,
|
|
174
|
+
agentId: headerMatch[2] || null,
|
|
174
175
|
});
|
|
175
176
|
}
|
|
176
177
|
return entries;
|
|
@@ -182,6 +183,10 @@ export class MemoryStore {
|
|
|
182
183
|
appendHandoffBrief(role, entry, workingDir, teamId) {
|
|
183
184
|
if (!role || !entry) return false;
|
|
184
185
|
const chain = this.getHandoffChain(role, workingDir, teamId);
|
|
186
|
+
|
|
187
|
+
// Dedup: prevent the same agent from having multiple entries in the chain
|
|
188
|
+
if (entry.agentId && chain.some(c => c.agentId === entry.agentId)) return false;
|
|
189
|
+
|
|
185
190
|
const nextN = (chain[0]?.rotationN || 0) + 1;
|
|
186
191
|
|
|
187
192
|
const block = [
|
|
@@ -296,6 +296,7 @@ export class ProcessManager {
|
|
|
296
296
|
this.peakContextUsage = new Map(); // agentId -> highest contextUsage seen
|
|
297
297
|
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
298
298
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
299
|
+
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
299
300
|
|
|
300
301
|
}
|
|
301
302
|
|
|
@@ -623,7 +624,11 @@ For normal file edits within your scope, proceed without review.
|
|
|
623
624
|
const files = this.daemon.journalist?.getAgentFiles(agent) || [];
|
|
624
625
|
if (files.length > 0) this._triggerIdleQC(agent);
|
|
625
626
|
this._processHandoffs(agent);
|
|
626
|
-
this.
|
|
627
|
+
if (this._rotatingAgents.has(agent.id)) {
|
|
628
|
+
this._rotatingAgents.delete(agent.id);
|
|
629
|
+
} else {
|
|
630
|
+
this._writeCompletionHandoff(agent).catch(err => console.error(`[Groove] Completion handoff failed for ${agent.name}:`, err.message));
|
|
631
|
+
}
|
|
627
632
|
}
|
|
628
633
|
});
|
|
629
634
|
|
|
@@ -815,7 +820,11 @@ For normal file edits within your scope, proceed without review.
|
|
|
815
820
|
const files = this.daemon.journalist?.getAgentFiles(agent) || [];
|
|
816
821
|
if (files.length > 0) this._triggerIdleQC(agent);
|
|
817
822
|
this._processHandoffs(agent);
|
|
818
|
-
this.
|
|
823
|
+
if (this._rotatingAgents.has(agent.id)) {
|
|
824
|
+
this._rotatingAgents.delete(agent.id);
|
|
825
|
+
} else {
|
|
826
|
+
this._writeCompletionHandoff(agent).catch(err => console.error(`[Groove] Completion handoff failed for ${agent.name}:`, err.message));
|
|
827
|
+
}
|
|
819
828
|
}
|
|
820
829
|
|
|
821
830
|
// Update Layer 7 specialization profile for this agent's session
|
|
@@ -1149,34 +1158,41 @@ For normal file edits within your scope, proceed without review.
|
|
|
1149
1158
|
});
|
|
1150
1159
|
}
|
|
1151
1160
|
|
|
1152
|
-
_writeCompletionHandoff(agent) {
|
|
1161
|
+
async _writeCompletionHandoff(agent) {
|
|
1153
1162
|
if (!this.daemon.memory || !this.daemon.journalist) return;
|
|
1154
1163
|
try {
|
|
1155
1164
|
const agentData = this.daemon.registry.get(agent.id);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1165
|
+
|
|
1166
|
+
let brief;
|
|
1167
|
+
try {
|
|
1168
|
+
brief = await this.daemon.journalist.generateHandoffBrief(agent, { reason: 'completed' });
|
|
1169
|
+
} catch {
|
|
1170
|
+
// Fallback to structural brief if AI synthesis fails
|
|
1171
|
+
const filteredLogs = this.daemon.journalist.collectFilteredLogs([agent]);
|
|
1172
|
+
const agentLog = filteredLogs[agent.id];
|
|
1173
|
+
const entries = agentLog?.entries || [];
|
|
1174
|
+
const files = this.daemon.journalist.getAgentFiles(agent) || [];
|
|
1175
|
+
|
|
1176
|
+
const toolSummary = entries
|
|
1177
|
+
.filter(e => e.type === 'tool')
|
|
1178
|
+
.map(e => `- ${e.tool}: ${e.input}`)
|
|
1179
|
+
.slice(-15)
|
|
1180
|
+
.join('\n');
|
|
1181
|
+
|
|
1182
|
+
const errorSummary = entries
|
|
1183
|
+
.filter(e => e.type === 'error')
|
|
1184
|
+
.map(e => `- ${e.text}`)
|
|
1185
|
+
.slice(-5)
|
|
1186
|
+
.join('\n');
|
|
1187
|
+
|
|
1188
|
+
brief = [
|
|
1189
|
+
`Agent ${agent.name} (${agent.role}) completed.`,
|
|
1190
|
+
agent.prompt ? `Task: ${agent.prompt.slice(0, 300)}` : '',
|
|
1191
|
+
files.length > 0 ? `\nFiles modified:\n${files.slice(0, 15).map(f => '- ' + f).join('\n')}` : '',
|
|
1192
|
+
toolSummary ? `\nRecent actions:\n${toolSummary}` : '',
|
|
1193
|
+
errorSummary ? `\nErrors encountered:\n${errorSummary}` : '',
|
|
1194
|
+
].filter(Boolean).join('\n');
|
|
1195
|
+
}
|
|
1180
1196
|
|
|
1181
1197
|
this.daemon.memory.appendHandoffBrief(agent.role, {
|
|
1182
1198
|
timestamp: new Date().toISOString(),
|
|
@@ -1184,7 +1200,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1184
1200
|
reason: 'completed',
|
|
1185
1201
|
oldTokens: agentData?.tokensUsed || 0,
|
|
1186
1202
|
contextUsage: agentData?.contextUsage || 0,
|
|
1187
|
-
brief: brief.slice(0, 4000),
|
|
1203
|
+
brief: (typeof brief === 'string' ? brief : '').slice(0, 4000),
|
|
1188
1204
|
}, agent.workingDir, agent.teamId);
|
|
1189
1205
|
} catch { /* best-effort */ }
|
|
1190
1206
|
}
|
|
@@ -211,7 +211,7 @@ export class TunnelManager {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
async connect(id) {
|
|
214
|
+
async connect(id, opts = {}) {
|
|
215
215
|
const config = this.saved.get(id);
|
|
216
216
|
if (!config) throw new Error(`Remote ${id} not found`);
|
|
217
217
|
|
|
@@ -220,7 +220,14 @@ export class TunnelManager {
|
|
|
220
220
|
return { localPort: existing.localPort, pid: existing.pid };
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'testing' } });
|
|
224
|
+
|
|
225
|
+
let testResult;
|
|
226
|
+
if (opts.skipTest && opts.testResult) {
|
|
227
|
+
testResult = opts.testResult;
|
|
228
|
+
} else {
|
|
229
|
+
testResult = await this.test(id);
|
|
230
|
+
}
|
|
224
231
|
if (!testResult.reachable) {
|
|
225
232
|
throw new Error(testResult.error || 'Host unreachable');
|
|
226
233
|
}
|
|
@@ -233,6 +240,8 @@ export class TunnelManager {
|
|
|
233
240
|
await this.autoStart(id);
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'connecting' } });
|
|
244
|
+
|
|
236
245
|
const localPort = await this._findAvailablePort();
|
|
237
246
|
const target = `${config.user}@${config.host}`;
|
|
238
247
|
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
@@ -257,13 +266,16 @@ export class TunnelManager {
|
|
|
257
266
|
let stderrBuf = '';
|
|
258
267
|
tunnel.stderr.on('data', (chunk) => { stderrBuf += chunk.toString(); });
|
|
259
268
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
let tunnelUp = false;
|
|
270
|
+
for (let elapsed = 0; elapsed < 8000; elapsed += 500) {
|
|
271
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
272
|
+
if (tunnel.exitCode !== null) {
|
|
273
|
+
throw new Error(`Tunnel failed to start: ${stderrBuf.trim() || 'unknown error'}`);
|
|
274
|
+
}
|
|
275
|
+
tunnelUp = await this._isPortInUse(localPort);
|
|
276
|
+
if (tunnelUp) break;
|
|
264
277
|
}
|
|
265
278
|
|
|
266
|
-
const tunnelUp = await this._isPortInUse(localPort);
|
|
267
279
|
if (!tunnelUp) {
|
|
268
280
|
try { process.kill(tunnel.pid); } catch { /* ignore */ }
|
|
269
281
|
throw new Error('Tunnel started but port forward not active');
|