groove-dev 0.27.30 → 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.
Files changed (46) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/cli/src/commands/start.js +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +32 -2
  5. package/node_modules/@groove-dev/daemon/src/firstrun.js +1 -0
  6. package/node_modules/@groove-dev/daemon/src/index.js +14 -0
  7. package/node_modules/@groove-dev/daemon/src/journalist.js +16 -4
  8. package/node_modules/@groove-dev/daemon/src/memory.js +6 -1
  9. package/node_modules/@groove-dev/daemon/src/process.js +44 -28
  10. package/node_modules/@groove-dev/daemon/src/providers/local.js +4 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/ollama.js +35 -3
  12. package/node_modules/@groove-dev/daemon/src/rotator.js +1 -0
  13. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +19 -7
  14. package/node_modules/@groove-dev/daemon/test/rotator.test.js +1 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
  16. package/node_modules/@groove-dev/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
  17. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
  20. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
  21. package/node_modules/@groove-dev/gui/src/components/layout/project-picker.jsx +127 -0
  22. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +22 -15
  23. package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -2
  24. package/package.json +1 -1
  25. package/packages/cli/package.json +1 -1
  26. package/packages/cli/src/commands/start.js +1 -1
  27. package/packages/daemon/package.json +1 -1
  28. package/packages/daemon/src/api.js +32 -2
  29. package/packages/daemon/src/firstrun.js +1 -0
  30. package/packages/daemon/src/index.js +14 -0
  31. package/packages/daemon/src/journalist.js +16 -4
  32. package/packages/daemon/src/memory.js +6 -1
  33. package/packages/daemon/src/process.js +44 -28
  34. package/packages/daemon/src/providers/local.js +4 -2
  35. package/packages/daemon/src/providers/ollama.js +35 -3
  36. package/packages/daemon/src/rotator.js +1 -0
  37. package/packages/daemon/src/tunnel-manager.js +19 -7
  38. package/packages/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
  39. package/packages/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
  40. package/packages/gui/dist/index.html +2 -2
  41. package/packages/gui/package.json +1 -1
  42. package/packages/gui/src/components/layout/app-shell.jsx +2 -0
  43. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
  44. package/packages/gui/src/components/layout/project-picker.jsx +127 -0
  45. package/packages/gui/src/components/settings/quick-connect.jsx +22 -15
  46. package/packages/gui/src/stores/groove.js +39 -2
@@ -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._writeCompletionHandoff(agent);
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._writeCompletionHandoff(agent);
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
- const filteredLogs = this.daemon.journalist.collectFilteredLogs([agent]);
1157
- const agentLog = filteredLogs[agent.id];
1158
- const entries = agentLog?.entries || [];
1159
- const files = this.daemon.journalist.getAgentFiles(agent) || [];
1160
-
1161
- const toolSummary = entries
1162
- .filter(e => e.type === 'tool')
1163
- .map(e => `- ${e.tool}: ${e.input}`)
1164
- .slice(-15)
1165
- .join('\n');
1166
-
1167
- const errorSummary = entries
1168
- .filter(e => e.type === 'error')
1169
- .map(e => `- ${e.text}`)
1170
- .slice(-5)
1171
- .join('\n');
1172
-
1173
- const brief = [
1174
- `Agent ${agent.name} (${agent.role}) completed.`,
1175
- agent.prompt ? `Task: ${agent.prompt.slice(0, 300)}` : '',
1176
- files.length > 0 ? `\nFiles modified:\n${files.slice(0, 15).map(f => '- ' + f).join('\n')}` : '',
1177
- toolSummary ? `\nRecent actions:\n${toolSummary}` : '',
1178
- errorSummary ? `\nErrors encountered:\n${errorSummary}` : '',
1179
- ].filter(Boolean).join('\n');
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
  }
@@ -93,14 +93,16 @@ export class LocalProvider extends Provider {
93
93
 
94
94
  static _hasOllama() {
95
95
  try {
96
- execSync('which ollama', { stdio: 'ignore' });
96
+ const cmd = process.platform === 'win32' ? 'where ollama' : 'which ollama';
97
+ execSync(cmd, { stdio: 'ignore' });
97
98
  return true;
98
99
  } catch { return false; }
99
100
  }
100
101
 
101
102
  static _hasLlamaServer() {
102
103
  try {
103
- execSync('which llama-server', { stdio: 'ignore' });
104
+ const cmd = process.platform === 'win32' ? 'where llama-server' : 'which llama-server';
105
+ execSync(cmd, { stdio: 'ignore' });
104
106
  return true;
105
107
  } catch { return false; }
106
108
  }
@@ -52,7 +52,8 @@ export class OllamaProvider extends Provider {
52
52
 
53
53
  static isInstalled() {
54
54
  try {
55
- execSync('which ollama', { stdio: 'ignore' });
55
+ const cmd = process.platform === 'win32' ? 'where ollama' : 'which ollama';
56
+ execSync(cmd, { stdio: 'ignore' });
56
57
  return true;
57
58
  } catch {
58
59
  return false;
@@ -67,6 +68,9 @@ export class OllamaProvider extends Provider {
67
68
  if (platform === 'linux') {
68
69
  return { command: 'curl -fsSL https://ollama.ai/install.sh | sh', platform: 'Linux' };
69
70
  }
71
+ if (platform === 'win32') {
72
+ return { command: 'winget install Ollama.Ollama', alt: 'Or download from https://ollama.ai/download', platform: 'Windows' };
73
+ }
70
74
  return { command: 'Download from https://ollama.ai/download', platform: 'other' };
71
75
  }
72
76
 
@@ -98,6 +102,22 @@ export class OllamaProvider extends Provider {
98
102
  }
99
103
  }
100
104
  }
105
+ if (platform === 'win32') {
106
+ try {
107
+ let cmd = 'ollama';
108
+ try {
109
+ execSync('where ollama', { stdio: 'ignore' });
110
+ } catch {
111
+ const localAppData = process.env.LOCALAPPDATA || '';
112
+ const fallback = localAppData + '\\Programs\\Ollama\\ollama.exe';
113
+ cmd = fallback;
114
+ }
115
+ execFile(cmd, ['serve'], { stdio: 'ignore', detached: true, shell: true }).unref();
116
+ return { started: true, method: 'ollama serve' };
117
+ } catch {
118
+ return { started: false, command: 'ollama serve' };
119
+ }
120
+ }
101
121
  // Linux / other
102
122
  try {
103
123
  execFile('ollama', ['serve'], { stdio: 'ignore', detached: true }).unref();
@@ -109,6 +129,14 @@ export class OllamaProvider extends Provider {
109
129
 
110
130
  static stopServer() {
111
131
  const platform = process.platform;
132
+ if (platform === 'win32') {
133
+ try {
134
+ execSync('taskkill /IM ollama.exe /F', { stdio: 'ignore', timeout: 5000 });
135
+ return { stopped: true, method: 'taskkill' };
136
+ } catch {
137
+ return { stopped: false };
138
+ }
139
+ }
112
140
  if (platform === 'darwin') {
113
141
  try {
114
142
  execSync('brew services stop ollama', { stdio: 'ignore', timeout: 10000 });
@@ -135,7 +163,11 @@ export class OllamaProvider extends Provider {
135
163
  minRAM: 4,
136
164
  recommendedRAM: 16,
137
165
  gpuRecommended: true,
138
- note: 'Apple Silicon Macs use unified memory — all RAM is GPU RAM. NVIDIA GPUs recommended on Linux.',
166
+ note: process.platform === 'win32'
167
+ ? 'NVIDIA or AMD GPUs recommended. Ensure GPU drivers are up to date.'
168
+ : process.platform === 'darwin'
169
+ ? 'Apple Silicon Macs use unified memory — all RAM is GPU RAM.'
170
+ : 'NVIDIA GPUs recommended on Linux.',
139
171
  };
140
172
  }
141
173
 
@@ -151,7 +183,7 @@ export class OllamaProvider extends Provider {
151
183
  let gpu = null;
152
184
  if (isAppleSilicon) {
153
185
  gpu = { type: 'apple-silicon', name: cpuModel.replace(/Apple /g, ''), vram: totalRamGb, note: 'Unified memory — all RAM available to GPU' };
154
- } else if (platform === 'linux') {
186
+ } else if (platform === 'linux' || platform === 'win32') {
155
187
  try {
156
188
  const out = execSync('nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits', { encoding: 'utf8', timeout: 5000 });
157
189
  const [name, vram] = out.trim().split(', ');
@@ -309,6 +309,7 @@ export class Rotator extends EventEmitter {
309
309
  timestamp: new Date().toISOString(),
310
310
  };
311
311
 
312
+ processes._rotatingAgents.add(agentId);
312
313
  await processes.kill(agentId);
313
314
 
314
315
  const routingMode = this.daemon.router.getMode(agentId);
@@ -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
- const testResult = await this.test(id);
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
- await new Promise((r) => setTimeout(r, 2000));
261
-
262
- if (tunnel.exitCode !== null) {
263
- throw new Error(`Tunnel failed to start: ${stderrBuf.trim() || 'unknown error'}`);
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');