groove-dev 0.27.152 → 0.27.154

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 (53) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/journalist.js +52 -21
  4. package/node_modules/@groove-dev/daemon/src/keeper.js +37 -2
  5. package/node_modules/@groove-dev/daemon/src/llama-server.js +96 -3
  6. package/node_modules/@groove-dev/daemon/src/model-manager.js +52 -10
  7. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +16 -0
  8. package/node_modules/@groove-dev/daemon/src/routes/files.js +71 -2
  9. package/node_modules/@groove-dev/daemon/src/routes/providers.js +11 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-BTLb6zTD.js +1015 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  13. package/node_modules/@groove-dev/gui/package.json +1 -1
  14. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +252 -44
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +51 -3
  16. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +9 -2
  17. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +40 -3
  18. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +23 -8
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +8 -1
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
  21. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +24 -5
  22. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +13 -0
  23. package/node_modules/@groove-dev/gui/src/views/memory.jsx +87 -44
  24. package/node_modules/@groove-dev/gui/src/views/models.jsx +15 -2
  25. package/package.json +1 -1
  26. package/packages/cli/package.json +1 -1
  27. package/packages/daemon/package.json +1 -1
  28. package/packages/daemon/src/journalist.js +52 -21
  29. package/packages/daemon/src/keeper.js +37 -2
  30. package/packages/daemon/src/llama-server.js +96 -3
  31. package/packages/daemon/src/model-manager.js +52 -10
  32. package/packages/daemon/src/routes/coordination.js +16 -0
  33. package/packages/daemon/src/routes/files.js +71 -2
  34. package/packages/daemon/src/routes/providers.js +11 -0
  35. package/packages/gui/dist/assets/index-BTLb6zTD.js +1015 -0
  36. package/packages/gui/dist/assets/index-Diw6wDPU.css +1 -0
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/agent-feed.jsx +252 -44
  40. package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
  41. package/packages/gui/src/components/agents/spawn-wizard.jsx +9 -2
  42. package/packages/gui/src/components/editor/file-tree.jsx +40 -3
  43. package/packages/gui/src/components/lab/runtime-config.jsx +23 -8
  44. package/packages/gui/src/components/settings/quick-connect.jsx +8 -1
  45. package/packages/gui/src/stores/groove.js +9 -1
  46. package/packages/gui/src/stores/slices/agents-slice.js +24 -5
  47. package/packages/gui/src/stores/slices/providers-slice.js +13 -0
  48. package/packages/gui/src/views/memory.jsx +87 -44
  49. package/packages/gui/src/views/models.jsx +15 -2
  50. package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +0 -1
  51. package/node_modules/@groove-dev/gui/dist/assets/index-CReKPWhY.js +0 -1011
  52. package/packages/gui/dist/assets/index-CEkPsSAm.css +0 -1
  53. package/packages/gui/dist/assets/index-CReKPWhY.js +0 -1011
@@ -5,7 +5,10 @@
5
5
  // Each model gets its own server on a unique port.
6
6
  // Auto-starts when an agent needs a GGUF model, auto-stops when idle.
7
7
 
8
- import { spawn, execSync } from 'child_process';
8
+ import { spawn, execSync, execFileSync } from 'child_process';
9
+ import { existsSync, mkdirSync, chmodSync } from 'fs';
10
+ import { resolve } from 'path';
11
+ import { homedir } from 'os';
9
12
 
10
13
  const BASE_PORT = 8081;
11
14
  const MAX_SERVERS = 5;
@@ -25,10 +28,98 @@ export class LlamaServerManager {
25
28
  execSync('which llama-server', { stdio: 'ignore' });
26
29
  return true;
27
30
  } catch {
28
- return false;
31
+ // Check common manual install locations
32
+ const paths = [
33
+ resolve(homedir(), '.local', 'bin', 'llama-server'),
34
+ resolve(homedir(), '.groove', 'bin', 'llama-server'),
35
+ '/usr/local/bin/llama-server',
36
+ ];
37
+ return paths.some(p => existsSync(p));
29
38
  }
30
39
  }
31
40
 
41
+ static getLlamaServerPath() {
42
+ try {
43
+ return execSync('which llama-server', { stdio: 'pipe', encoding: 'utf8' }).trim();
44
+ } catch {
45
+ const paths = [
46
+ resolve(homedir(), '.local', 'bin', 'llama-server'),
47
+ resolve(homedir(), '.groove', 'bin', 'llama-server'),
48
+ '/usr/local/bin/llama-server',
49
+ ];
50
+ return paths.find(p => existsSync(p)) || 'llama-server';
51
+ }
52
+ }
53
+
54
+ static async install() {
55
+ const platform = process.platform;
56
+
57
+ if (platform === 'darwin') {
58
+ try {
59
+ execSync('which brew', { stdio: 'ignore' });
60
+ } catch {
61
+ throw new Error('Homebrew not found. Install it from https://brew.sh then retry.');
62
+ }
63
+ execSync('brew install llama.cpp', { stdio: 'pipe', timeout: 600000 });
64
+ return { method: 'brew', path: execSync('which llama-server', { encoding: 'utf8', stdio: 'pipe' }).trim() };
65
+ }
66
+
67
+ if (platform === 'linux') {
68
+ const installDir = resolve(homedir(), '.local', 'bin');
69
+ mkdirSync(installDir, { recursive: true });
70
+
71
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
72
+ const hasCuda = (() => { try { execSync('which nvidia-smi', { stdio: 'ignore' }); return true; } catch { return false; } })();
73
+
74
+ const resp = await fetch('https://api.github.com/repos/ggml-org/llama.cpp/releases/latest', {
75
+ headers: { 'User-Agent': 'groove-dev' },
76
+ });
77
+ if (!resp.ok) throw new Error(`GitHub API error: ${resp.status}`);
78
+ const release = await resp.json();
79
+
80
+ const suffix = hasCuda ? `ubuntu-${arch}-cuda` : `ubuntu-${arch}`;
81
+ let asset = release.assets.find(a => a.name.includes(suffix) && a.name.endsWith('.zip'));
82
+ if (!asset && hasCuda) {
83
+ asset = release.assets.find(a => a.name.includes(`ubuntu-${arch}`) && a.name.endsWith('.zip'));
84
+ }
85
+ if (!asset) {
86
+ asset = release.assets.find(a => a.name.includes('ubuntu') && a.name.includes(arch) && a.name.endsWith('.zip'));
87
+ }
88
+ if (!asset) throw new Error(`No pre-built binary found for linux-${arch}. Build from source: https://github.com/ggml-org/llama.cpp#build`);
89
+
90
+ const tmpZip = `/tmp/groove-llama-${Date.now()}.zip`;
91
+ const tmpDir = `/tmp/groove-llama-extract-${Date.now()}`;
92
+
93
+ execSync(`curl -fSL "${asset.browser_download_url}" -o "${tmpZip}"`, { stdio: 'pipe', timeout: 600000 });
94
+ execSync(`unzip -o "${tmpZip}" -d "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
95
+
96
+ const findResult = execSync(`find "${tmpDir}" -name llama-server -type f`, { encoding: 'utf8', stdio: 'pipe' }).trim();
97
+ const binPath = findResult.split('\n')[0];
98
+ if (!binPath) throw new Error('llama-server binary not found in release archive');
99
+
100
+ const destPath = resolve(installDir, 'llama-server');
101
+ execSync(`cp "${binPath}" "${destPath}"`, { stdio: 'pipe' });
102
+ chmodSync(destPath, 0o755);
103
+
104
+ // Copy shared libraries if present
105
+ try {
106
+ const libDir = resolve(binPath, '..', '..', 'lib');
107
+ if (existsSync(libDir)) {
108
+ const userLibDir = resolve(homedir(), '.local', 'lib');
109
+ mkdirSync(userLibDir, { recursive: true });
110
+ execSync(`cp -r "${libDir}/"* "${userLibDir}/"`, { stdio: 'pipe' });
111
+ }
112
+ } catch { /* libs are optional */ }
113
+
114
+ // Cleanup
115
+ try { execSync(`rm -rf "${tmpZip}" "${tmpDir}"`, { stdio: 'ignore' }); } catch { /* best-effort */ }
116
+
117
+ return { method: 'github-release', path: destPath, cuda: hasCuda, release: release.tag_name };
118
+ }
119
+
120
+ throw new Error(`Automatic install not supported on ${platform}. Install llama-server manually: https://github.com/ggml-org/llama.cpp#build`);
121
+ }
122
+
32
123
  // --- Server Lifecycle ---
33
124
 
34
125
  /**
@@ -74,9 +165,11 @@ export class LlamaServerManager {
74
165
  args.push('--flash-attn', 'auto');
75
166
  }
76
167
 
77
- const proc = spawn('llama-server', args, {
168
+ const serverBin = LlamaServerManager.getLlamaServerPath();
169
+ const proc = spawn(serverBin, args, {
78
170
  stdio: ['ignore', 'pipe', 'pipe'],
79
171
  detached: false,
172
+ env: { ...process.env, LD_LIBRARY_PATH: [resolve(homedir(), '.local', 'lib'), process.env.LD_LIBRARY_PATH].filter(Boolean).join(':') },
80
173
  });
81
174
 
82
175
  if (!proc.pid) {
@@ -69,7 +69,6 @@ export class ModelManager {
69
69
  async search(query, { limit = 20, sort = 'downloads' } = {}) {
70
70
  const params = new URLSearchParams({
71
71
  search: query,
72
- filter: 'gguf',
73
72
  sort,
74
73
  direction: '-1',
75
74
  limit: String(limit),
@@ -83,15 +82,20 @@ export class ModelManager {
83
82
  if (!res.ok) throw new Error(`HuggingFace API error: ${res.status}`);
84
83
  const models = await res.json();
85
84
 
86
- return models.map((m) => ({
87
- id: m.modelId || m.id,
88
- name: m.modelId?.split('/').pop() || m.id,
89
- author: m.modelId?.split('/')[0] || '',
90
- downloads: m.downloads || 0,
91
- likes: m.likes || 0,
92
- tags: m.tags || [],
93
- lastModified: m.lastModified,
94
- }));
85
+ return models.map((m) => {
86
+ const id = m.modelId || m.id;
87
+ const tags = m.tags || [];
88
+ return {
89
+ id,
90
+ name: id.split('/').pop() || id,
91
+ author: id.split('/')[0] || '',
92
+ downloads: m.downloads || 0,
93
+ likes: m.likes || 0,
94
+ tags,
95
+ lastModified: m.lastModified,
96
+ recommendedRuntimes: inferRuntimes(id, tags),
97
+ };
98
+ });
95
99
  }
96
100
 
97
101
  async getModelFiles(repoId) {
@@ -409,3 +413,41 @@ function classifyTier(params, quant) {
409
413
  if (billions >= 10) return 'medium';
410
414
  return 'light';
411
415
  }
416
+
417
+ function inferRuntimes(repoId, tags) {
418
+ const lower = repoId.toLowerCase();
419
+ const tagSet = new Set(tags.map((t) => t.toLowerCase()));
420
+ const runtimes = new Set();
421
+
422
+ // GGUF → llama.cpp and (implicitly) Ollama
423
+ if (tagSet.has('gguf') || lower.includes('-gguf') || lower.includes('_gguf')) {
424
+ runtimes.add('llama.cpp');
425
+ }
426
+
427
+ // MLX-optimized models
428
+ if (tagSet.has('mlx') || lower.includes('-mlx') || lower.includes('_mlx')) {
429
+ runtimes.add('MLX');
430
+ }
431
+
432
+ // GPTQ / AWQ quantized → vLLM handles these well
433
+ if (tagSet.has('gptq') || tagSet.has('awq') || lower.includes('-gptq') || lower.includes('-awq')) {
434
+ runtimes.add('vLLM');
435
+ }
436
+
437
+ // SafeTensors / standard transformer weights → vLLM, TGI, MLX
438
+ if (tagSet.has('safetensors') || tagSet.has('transformers')) {
439
+ runtimes.add('vLLM');
440
+ runtimes.add('TGI');
441
+ if (!runtimes.has('MLX')) runtimes.add('MLX');
442
+ }
443
+
444
+ // If nothing matched, infer from general model traits
445
+ if (runtimes.size === 0) {
446
+ if (tagSet.has('pytorch') || tagSet.has('tf') || tagSet.has('jax')) {
447
+ runtimes.add('vLLM');
448
+ runtimes.add('TGI');
449
+ }
450
+ }
451
+
452
+ return [...runtimes];
453
+ }
@@ -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
  });
@@ -713,6 +713,17 @@ export function registerProviderRoutes(app, daemon) {
713
713
  res.json(daemon.llamaServer.getStatus());
714
714
  });
715
715
 
716
+ app.post('/api/llama/install', async (req, res) => {
717
+ try {
718
+ const { LlamaServerManager } = await import('../llama-server.js');
719
+ const result = await LlamaServerManager.install();
720
+ daemon.modelLab.refreshInstalledTools();
721
+ res.json({ success: true, ...result });
722
+ } catch (err) {
723
+ res.status(500).json({ error: err.message });
724
+ }
725
+ });
726
+
716
727
  app.get('/api/mlx/status', (req, res) => {
717
728
  res.json(daemon.mlxServer.getStatus());
718
729
  });