groove-dev 0.27.153 → 0.27.155

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 (40) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/journalist.js +52 -21
  5. package/node_modules/@groove-dev/daemon/src/keeper.js +37 -2
  6. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +16 -0
  7. package/node_modules/@groove-dev/daemon/src/routes/files.js +71 -2
  8. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +0 -2
  9. package/node_modules/@groove-dev/gui/dist/assets/index-BTLb6zTD.js +1015 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.css +1 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +252 -44
  14. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +51 -3
  15. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +40 -3
  16. package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
  17. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +24 -5
  18. package/node_modules/@groove-dev/gui/src/views/memory.jsx +87 -44
  19. package/package.json +1 -1
  20. package/packages/cli/package.json +1 -1
  21. package/packages/daemon/package.json +1 -1
  22. package/packages/daemon/src/journalist.js +52 -21
  23. package/packages/daemon/src/keeper.js +37 -2
  24. package/packages/daemon/src/routes/coordination.js +16 -0
  25. package/packages/daemon/src/routes/files.js +71 -2
  26. package/packages/daemon/src/tunnel-manager.js +0 -2
  27. package/packages/gui/dist/assets/index-BTLb6zTD.js +1015 -0
  28. package/packages/gui/dist/assets/index-Diw6wDPU.css +1 -0
  29. package/packages/gui/dist/index.html +2 -2
  30. package/packages/gui/package.json +1 -1
  31. package/packages/gui/src/components/agents/agent-feed.jsx +252 -44
  32. package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
  33. package/packages/gui/src/components/editor/file-tree.jsx +40 -3
  34. package/packages/gui/src/stores/groove.js +9 -1
  35. package/packages/gui/src/stores/slices/agents-slice.js +24 -5
  36. package/packages/gui/src/views/memory.jsx +87 -44
  37. package/node_modules/@groove-dev/gui/dist/assets/index-BU_YTEZo.js +0 -1011
  38. package/node_modules/@groove-dev/gui/dist/assets/index-ChfYTsyc.css +0 -1
  39. package/packages/gui/dist/assets/index-BU_YTEZo.js +0 -1011
  40. package/packages/gui/dist/assets/index-ChfYTsyc.css +0 -1
package/CLAUDE.md CHANGED
@@ -295,3 +295,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
295
295
  - Dashboard: routing donut, cache panel, context health gauges
296
296
  - Monitor/QC agent mode (stay active, loop)
297
297
  - Distribution: demo video, HN launch, Twitter content
298
+
299
+ <!-- GROOVE:START -->
300
+ ## GROOVE Orchestration (auto-injected)
301
+ Active agents: 0
302
+ See AGENTS_REGISTRY.md for full agent state.
303
+ **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
304
+ <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.153",
3
+ "version": "0.27.155",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.153",
3
+ "version": "0.27.155",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -644,20 +644,14 @@ export class Journalist {
644
644
  proc.stdin.write(stdinData);
645
645
  proc.stdin.end();
646
646
  proc.stdout.on('data', (d) => { stdout += d.toString(); });
647
- const timer = setTimeout(() => { proc.kill(); reject(new Error('Headless timeout')); }, 60_000);
647
+ const timer = setTimeout(() => { proc.kill(); reject(new Error('Headless timeout')); }, 120_000);
648
648
  proc.on('exit', (code) => {
649
649
  clearTimeout(timer);
650
650
  if (code !== 0) return reject(new Error(`Headless exited with code ${code}`));
651
651
  this._recordHeadlessUsage(stdout, trackAs, modelId);
652
- const lines = stdout.split('\n');
653
- for (const line of lines) {
654
- try {
655
- const json = JSON.parse(line);
656
- if (json.result) return resolve(json.result);
657
- if (json.content?.[0]?.text) return resolve(json.content[0].text);
658
- } catch { /* not json */ }
659
- }
660
- resolve(stdout.trim());
652
+ const extracted = this._parseHeadlessOutput(stdout);
653
+ if (extracted) return resolve(extracted);
654
+ reject(new Error('Headless produced no usable output'));
661
655
  });
662
656
  return;
663
657
  }
@@ -666,24 +660,61 @@ export class Journalist {
666
660
  env: { ...process.env, ...env },
667
661
  cwd: this.daemon.projectDir,
668
662
  maxBuffer: 1024 * 1024 * 5,
669
- timeout: 60_000,
663
+ timeout: 120_000,
670
664
  }, (err, stdout, stderr) => {
671
665
  if (err) return reject(err);
672
666
  this._recordHeadlessUsage(stdout, trackAs, modelId);
673
- const lines = stdout.split('\n');
674
- for (const line of lines) {
675
- try {
676
- const data = JSON.parse(line);
677
- if (data.type === 'result' && data.result) {
678
- return resolve(data.result);
679
- }
680
- } catch { /* skip */ }
681
- }
682
- resolve(stdout);
667
+ const extracted = this._parseHeadlessOutput(stdout);
668
+ if (extracted) return resolve(extracted);
669
+ reject(new Error('Headless produced no usable output'));
683
670
  });
684
671
  });
685
672
  }
686
673
 
674
+ _parseHeadlessOutput(stdout) {
675
+ const lines = stdout.split('\n');
676
+ let resultText = '';
677
+ let assistantText = '';
678
+ let codexText = '';
679
+ let networkText = '';
680
+
681
+ for (const line of lines) {
682
+ try {
683
+ const json = JSON.parse(line);
684
+
685
+ // Claude/Gemini stream-json: {"type":"result","result":"..."}
686
+ if (typeof json.result === 'string' && json.result.trim()) {
687
+ resultText = json.result;
688
+ }
689
+
690
+ // Claude/Gemini assistant message: {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
691
+ const msgContent = json.message?.content?.[0]?.text || json.content?.[0]?.text;
692
+ if (msgContent && msgContent.trim()) {
693
+ assistantText = msgContent;
694
+ }
695
+
696
+ // Codex --json: {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
697
+ if (json.type === 'item.completed' && json.item?.type === 'agent_message' && json.item.text?.trim()) {
698
+ codexText = json.item.text;
699
+ }
700
+
701
+ // Groove Network: {"type":"done|complete|result","text":"..."}
702
+ if ((json.type === 'done' || json.type === 'complete') && typeof json.text === 'string' && json.text.trim()) {
703
+ networkText = json.text;
704
+ }
705
+ } catch { /* not json */ }
706
+ }
707
+
708
+ if (resultText) return resultText;
709
+ if (codexText) return codexText;
710
+ if (networkText) return networkText;
711
+ if (assistantText) return assistantText;
712
+ // Ollama / plain-text providers: raw text output (no JSON)
713
+ const plain = stdout.trim();
714
+ if (plain && !plain.startsWith('{') && plain.length > 20) return plain;
715
+ return null;
716
+ }
717
+
687
718
  parseSynthesisResult(text, agents) {
688
719
  // Parse the structured output from AI
689
720
  const sections = {
@@ -58,7 +58,7 @@ export class Keeper {
58
58
 
59
59
  save(tag, content) {
60
60
  if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
61
- if (content === undefined || content === null) throw new Error('Content is required');
61
+ if (content === undefined || content === null || !String(content).trim()) throw new Error('Content is required');
62
62
  const normalized = this._normalize(tag);
63
63
  if (!normalized) throw new Error('Tag is required');
64
64
  const filePath = this._tagToPath(normalized);
@@ -129,7 +129,7 @@ export class Keeper {
129
129
  update(tag, content) {
130
130
  const normalized = this._normalize(tag);
131
131
  if (!normalized) throw new Error('Tag is required');
132
- if (content === undefined || content === null) throw new Error('Content is required');
132
+ if (content === undefined || content === null || !String(content).trim()) throw new Error('Content is required');
133
133
  const filePath = this._tagToPath(normalized);
134
134
  if (!existsSync(filePath)) throw new Error(`Memory #${normalized} does not exist`);
135
135
  writeFileSync(filePath, String(content));
@@ -153,6 +153,41 @@ export class Keeper {
153
153
  return true;
154
154
  }
155
155
 
156
+ move(oldTag, newTag) {
157
+ const oldNorm = this._normalize(oldTag);
158
+ const newNorm = this._normalize(newTag);
159
+ if (!oldNorm || !newNorm) throw new Error('Both old and new tags are required');
160
+ if (oldNorm === newNorm) return this._index[oldNorm];
161
+ const oldPath = this._tagToPath(oldNorm);
162
+ if (!existsSync(oldPath)) throw new Error(`Memory #${oldNorm} does not exist`);
163
+ if (this._index[newNorm]) throw new Error(`Memory #${newNorm} already exists`);
164
+ const content = readFileSync(oldPath, 'utf8');
165
+ const newPath = this._tagToPath(newNorm);
166
+ this._ensureParentDir(newPath);
167
+ writeFileSync(newPath, content);
168
+ unlinkSync(oldPath);
169
+ this._index[newNorm] = { ...this._index[oldNorm], tag: newNorm, updatedAt: new Date().toISOString() };
170
+ delete this._index[oldNorm];
171
+ // Move children too (e.g. moving "a" to "b/a" also moves "a/child" to "b/a/child")
172
+ const prefix = oldNorm + '/';
173
+ for (const tag of Object.keys(this._index)) {
174
+ if (tag.startsWith(prefix)) {
175
+ const childSuffix = tag.slice(prefix.length);
176
+ const childNewTag = newNorm + '/' + childSuffix;
177
+ const childOldPath = this._tagToPath(tag);
178
+ const childNewPath = this._tagToPath(childNewTag);
179
+ const childContent = readFileSync(childOldPath, 'utf8');
180
+ this._ensureParentDir(childNewPath);
181
+ writeFileSync(childNewPath, childContent);
182
+ unlinkSync(childOldPath);
183
+ this._index[childNewTag] = { ...this._index[tag], tag: childNewTag, updatedAt: new Date().toISOString() };
184
+ delete this._index[tag];
185
+ }
186
+ }
187
+ this._saveIndex();
188
+ return { tag: newNorm, ...this._index[newNorm] };
189
+ }
190
+
156
191
  // ── Doc (AI-generated) ───────────────────────────────────
157
192
 
158
193
  saveDoc(tag, content) {
@@ -249,6 +249,19 @@ export function registerCoordinationRoutes(app, daemon) {
249
249
  }
250
250
  });
251
251
 
252
+ app.post('/api/keeper/move', (req, res) => {
253
+ try {
254
+ const { oldTag, newTag } = req.body || {};
255
+ if (!oldTag || !newTag) return res.status(400).json({ error: 'oldTag and newTag are required' });
256
+ const item = daemon.keeper.move(oldTag, newTag);
257
+ daemon.audit.log('keeper.move', { oldTag, newTag: item.tag });
258
+ daemon.broadcast({ type: 'keeper:moved', oldTag, item });
259
+ res.json(item);
260
+ } catch (err) {
261
+ res.status(err.message.includes('does not exist') ? 404 : 400).json({ error: err.message });
262
+ }
263
+ });
264
+
252
265
  app.delete('/api/keeper/link/:tag(*)', (req, res) => {
253
266
  try {
254
267
  const { docPath } = req.body || {};
@@ -289,6 +302,9 @@ export function registerCoordinationRoutes(app, daemon) {
289
302
  } else {
290
303
  doc = `# ${tag}\n\n*Auto-generated document from conversation*\n\n${transcript.slice(0, 5000)}`;
291
304
  }
305
+ if (!doc || !doc.trim()) {
306
+ return res.status(502).json({ error: 'AI synthesis returned empty content — try again' });
307
+ }
292
308
  const item = daemon.keeper.saveDoc(tag, doc);
293
309
  daemon.audit.log('keeper.doc', { tag: item.tag, agentId });
294
310
  daemon.broadcast({ type: 'keeper:saved', item });
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { resolve, sep, isAbsolute } from 'path';
2
+ import { resolve, sep, isAbsolute, basename } from 'path';
3
3
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, realpathSync } from 'fs';
4
4
  import { execFile, execFileSync } from 'child_process';
5
5
  import { homedir } from 'os';
@@ -331,6 +331,53 @@ export function registerFileRoutes(app, daemon) {
331
331
  }
332
332
  });
333
333
 
334
+ // Download a file (serves raw with Content-Disposition)
335
+ app.get('/api/files/download', (req, res) => {
336
+ const relPath = req.query.path;
337
+ const result = validateFilePath(relPath, getEditorRoot(daemon));
338
+ if (result.error) return res.status(400).json({ error: result.error });
339
+ if (!existsSync(result.fullPath)) return res.status(404).json({ error: 'File not found' });
340
+
341
+ const stat = statSync(result.fullPath);
342
+ if (stat.isDirectory()) return res.status(400).json({ error: 'Cannot download a directory' });
343
+
344
+ const name = basename(result.fullPath);
345
+ const mime = mimeLookup(name) || 'application/octet-stream';
346
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(name)}"`);
347
+ res.setHeader('Content-Type', mime);
348
+ res.setHeader('Content-Length', stat.size);
349
+ createReadStream(result.fullPath).pipe(res);
350
+ });
351
+
352
+ // Upload files (base64-encoded) to a target directory
353
+ app.post('/api/files/upload', (req, res) => {
354
+ const { dir = '', files } = req.body;
355
+ const rootDir = getEditorRoot(daemon);
356
+ if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
357
+ if (!Array.isArray(files) || files.length === 0) return res.status(400).json({ error: 'files[] required' });
358
+ if (files.length > 50) return res.status(400).json({ error: 'Max 50 files per upload' });
359
+
360
+ const uploaded = [];
361
+ for (const file of files) {
362
+ if (!file.name || !file.content) continue;
363
+ const safeName = String(file.name).replace(/\.\./g, '').replace(/\//g, '_');
364
+ if (!safeName) continue;
365
+ const relPath = dir ? `${dir}/${safeName}` : safeName;
366
+ const result = validateFilePath(relPath, rootDir);
367
+ if (result.error) continue;
368
+
369
+ try {
370
+ const parentDir = resolve(result.fullPath, '..');
371
+ mkdirSync(parentDir, { recursive: true });
372
+ const buf = Buffer.from(file.content, 'base64');
373
+ writeFileSync(result.fullPath, buf);
374
+ daemon.audit.log('file.upload', { path: relPath, size: buf.length });
375
+ uploaded.push({ path: relPath, size: buf.length });
376
+ } catch { /* skip failed files */ }
377
+ }
378
+ res.json({ uploaded, total: uploaded.length });
379
+ });
380
+
334
381
  // Create a new file
335
382
  app.post('/api/files/create', (req, res) => {
336
383
  const { path: relPath, content = '' } = req.body;
@@ -595,9 +642,31 @@ export function registerFileRoutes(app, daemon) {
595
642
  if (!agent) return res.status(404).json({ error: 'Agent not found' });
596
643
  const rawFiles = daemon.registry.getFilesTouched(req.params.id);
597
644
  const rootDir = agent.workingDir || daemon.projectDir;
645
+
646
+ // Build git diff numstat for line-level +/- counts
647
+ let numstatMap = {};
648
+ const writtenPaths = rawFiles.filter(f => f.writes > 0).map(f => f.path);
649
+ if (writtenPaths.length > 0) {
650
+ try {
651
+ const out = execFileSync('git', ['diff', '--numstat', '--', ...writtenPaths], {
652
+ cwd: rootDir, timeout: 10000, maxBuffer: 2 * 1024 * 1024,
653
+ }).toString();
654
+ for (const line of out.split('\n')) {
655
+ const m = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
656
+ if (m) {
657
+ numstatMap[m[3]] = {
658
+ additions: m[1] === '-' ? 0 : Number(m[1]),
659
+ deletions: m[2] === '-' ? 0 : Number(m[2]),
660
+ };
661
+ }
662
+ }
663
+ } catch { /* git not available or not a repo */ }
664
+ }
665
+
598
666
  const files = rawFiles.map(f => {
599
667
  const fullPath = isAbsolute(f.path) ? f.path : resolve(rootDir, f.path);
600
- return { ...f, exists: existsSync(fullPath) };
668
+ const stats = numstatMap[f.path] || null;
669
+ return { ...f, exists: existsSync(fullPath), additions: stats?.additions ?? null, deletions: stats?.deletions ?? null };
601
670
  });
602
671
  res.json({ files, total: files.length });
603
672
  });
@@ -267,8 +267,6 @@ export class TunnelManager {
267
267
  let testResult;
268
268
  if (opts.skipTest && opts.testResult) {
269
269
  testResult = opts.testResult;
270
- } else if (config.lastConnected && opts.skipTest !== false) {
271
- testResult = { reachable: true, daemonRunning: true, grooveInstalled: true, remoteVersion: null };
272
270
  } else {
273
271
  testResult = await this.test(id);
274
272
  }