groove-dev 0.25.21 → 0.26.2

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 (45) hide show
  1. package/node_modules/@groove-dev/daemon/src/agent-loop.js +444 -0
  2. package/node_modules/@groove-dev/daemon/src/api.js +104 -5
  3. package/node_modules/@groove-dev/daemon/src/index.js +6 -1
  4. package/node_modules/@groove-dev/daemon/src/llama-server.js +268 -0
  5. package/node_modules/@groove-dev/daemon/src/model-manager.js +411 -0
  6. package/node_modules/@groove-dev/daemon/src/process.js +160 -9
  7. package/node_modules/@groove-dev/daemon/src/providers/codex.js +51 -1
  8. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +3 -2
  9. package/node_modules/@groove-dev/daemon/src/providers/index.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/local.js +183 -0
  11. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/tool-executor.js +367 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-BC2Bhfv0.js +633 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-BQnZrh4f.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +7 -2
  18. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +7 -1
  20. package/node_modules/@groove-dev/gui/src/views/models.jsx +380 -0
  21. package/package.json +2 -2
  22. package/packages/daemon/src/agent-loop.js +444 -0
  23. package/packages/daemon/src/api.js +104 -5
  24. package/packages/daemon/src/index.js +6 -1
  25. package/packages/daemon/src/llama-server.js +268 -0
  26. package/packages/daemon/src/model-manager.js +411 -0
  27. package/packages/daemon/src/process.js +160 -9
  28. package/packages/daemon/src/providers/codex.js +51 -1
  29. package/packages/daemon/src/providers/gemini.js +3 -2
  30. package/packages/daemon/src/providers/index.js +4 -0
  31. package/packages/daemon/src/providers/local.js +183 -0
  32. package/packages/daemon/src/registry.js +1 -1
  33. package/packages/daemon/src/tool-executor.js +367 -0
  34. package/packages/gui/dist/assets/index-BC2Bhfv0.js +633 -0
  35. package/packages/gui/dist/assets/index-BQnZrh4f.css +1 -0
  36. package/packages/gui/dist/index.html +2 -2
  37. package/packages/gui/src/app.jsx +2 -0
  38. package/packages/gui/src/components/agents/agent-config.jsx +7 -2
  39. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  40. package/packages/gui/src/stores/groove.js +7 -1
  41. package/packages/gui/src/views/models.jsx +380 -0
  42. package/node_modules/@groove-dev/gui/dist/assets/index-B1FkEzF0.js +0 -623
  43. package/node_modules/@groove-dev/gui/dist/assets/index-GYcMwmjs.css +0 -1
  44. package/packages/gui/dist/assets/index-B1FkEzF0.js +0 -623
  45. package/packages/gui/dist/assets/index-GYcMwmjs.css +0 -1
@@ -0,0 +1,380 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import { ScrollArea } from '../components/ui/scroll-area';
4
+ import { Badge } from '../components/ui/badge';
5
+ import { Button } from '../components/ui/button';
6
+ import { Input } from '../components/ui/input';
7
+ import { api } from '../lib/api';
8
+ import { useToast } from '../lib/hooks/use-toast';
9
+ import { useGrooveStore } from '../stores/groove';
10
+ import {
11
+ Search, Download, Trash2, HardDrive, Cpu, MemoryStick,
12
+ Check, X, Loader2, ExternalLink, Box, ChevronDown, ChevronRight,
13
+ } from 'lucide-react';
14
+ import { cn } from '../lib/cn';
15
+
16
+ function formatBytes(bytes) {
17
+ if (!bytes) return '—';
18
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
19
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
20
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
21
+ }
22
+
23
+ function formatSpeed(bytesPerSec) {
24
+ if (!bytesPerSec) return '';
25
+ if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(0)} KB/s`;
26
+ return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
27
+ }
28
+
29
+ // ---- Hardware Info ----
30
+ function HardwareBar({ hardware }) {
31
+ if (!hardware) return null;
32
+ return (
33
+ <div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border border-border-subtle rounded-lg text-xs font-sans text-text-2">
34
+ <div className="flex items-center gap-1.5">
35
+ <MemoryStick size={14} className="text-text-3" />
36
+ <span>{hardware.totalRamGb} GB RAM</span>
37
+ </div>
38
+ <div className="flex items-center gap-1.5">
39
+ <Cpu size={14} className="text-text-3" />
40
+ <span>{hardware.cores} cores</span>
41
+ </div>
42
+ {hardware.gpu && (
43
+ <div className="flex items-center gap-1.5">
44
+ <HardDrive size={14} className="text-text-3" />
45
+ <span>{hardware.gpu.name}{hardware.gpu.vram ? ` (${hardware.gpu.vram} GB)` : ''}</span>
46
+ </div>
47
+ )}
48
+ {hardware.recommended?.code && (
49
+ <div className="ml-auto text-accent">
50
+ Recommended: {hardware.recommended.code}
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // ---- Download Progress Bar ----
58
+ function DownloadProgress({ download }) {
59
+ const pct = Math.round((download.percent || 0) * 100);
60
+ return (
61
+ <div className="space-y-1">
62
+ <div className="flex items-center justify-between text-2xs font-sans text-text-3">
63
+ <span>{download.filename}</span>
64
+ <span>{pct}% {formatSpeed(download.speed)}</span>
65
+ </div>
66
+ <div className="h-1.5 bg-surface-3 rounded-full overflow-hidden">
67
+ <div className="h-full bg-accent rounded-full transition-all" style={{ width: `${pct}%` }} />
68
+ </div>
69
+ <div className="text-2xs text-text-4">
70
+ {formatBytes(download.downloaded)} / {formatBytes(download.totalBytes)}
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ // ---- Installed Model Card ----
77
+ function InstalledModel({ model, onDelete }) {
78
+ const [deleting, setDeleting] = useState(false);
79
+ const tierColors = { light: 'text-green-400', medium: 'text-blue-400', heavy: 'text-orange-400' };
80
+
81
+ return (
82
+ <div className="flex items-center gap-3 px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg">
83
+ <Box size={18} className="text-accent flex-shrink-0" />
84
+ <div className="flex-1 min-w-0">
85
+ <div className="flex items-center gap-2">
86
+ <span className="text-sm font-mono font-bold text-text-0 truncate">{model.id}</span>
87
+ {model.quantization && <Badge variant="subtle" className="text-2xs">{model.quantization}</Badge>}
88
+ {model.parameters && <Badge variant="subtle" className="text-2xs">{model.parameters}</Badge>}
89
+ <span className={cn('text-2xs font-medium capitalize', tierColors[model.tier] || 'text-text-3')}>{model.tier}</span>
90
+ </div>
91
+ <div className="text-2xs text-text-3 font-sans mt-0.5">
92
+ {formatBytes(model.sizeBytes)} &middot; ctx {(model.contextWindow || 0).toLocaleString()} &middot; {model.category}
93
+ {model.repoId && <span className="text-text-4"> &middot; {model.repoId}</span>}
94
+ </div>
95
+ </div>
96
+ <button
97
+ onClick={async () => { setDeleting(true); await onDelete(model.id); setDeleting(false); }}
98
+ disabled={deleting}
99
+ className="p-1.5 rounded-md text-text-4 hover:text-red-400 hover:bg-red-400/10 transition-colors"
100
+ >
101
+ {deleting ? <Loader2 size={14} className="animate-spin" /> : <Trash2 size={14} />}
102
+ </button>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // ---- Search Result Card (HuggingFace) ----
108
+ function SearchResult({ result, onExpand, expanded }) {
109
+ return (
110
+ <button
111
+ onClick={() => onExpand(expanded ? null : result.id)}
112
+ className="w-full text-left px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg hover:border-accent/30 transition-colors cursor-pointer"
113
+ >
114
+ <div className="flex items-center gap-2">
115
+ <span className="text-sm font-mono font-bold text-text-0 truncate flex-1">{result.name}</span>
116
+ <span className="text-2xs text-text-4 font-sans">{result.author}</span>
117
+ {expanded ? <ChevronDown size={14} className="text-text-3" /> : <ChevronRight size={14} className="text-text-3" />}
118
+ </div>
119
+ <div className="text-2xs text-text-3 font-sans mt-0.5 flex gap-3">
120
+ <span>{result.downloads?.toLocaleString()} downloads</span>
121
+ <span>{result.likes} likes</span>
122
+ </div>
123
+ </button>
124
+ );
125
+ }
126
+
127
+ // ---- File Picker (quantization variants) ----
128
+ function FilePicker({ repoId, onDownload }) {
129
+ const [files, setFiles] = useState(null);
130
+ const [loading, setLoading] = useState(true);
131
+ const [downloading, setDownloading] = useState(null);
132
+ const toast = useToast();
133
+
134
+ useEffect(() => {
135
+ setLoading(true);
136
+ api.get(`/models/${repoId}/files`)
137
+ .then((data) => setFiles(data.files || []))
138
+ .catch(() => toast.error('Failed to load model files'))
139
+ .finally(() => setLoading(false));
140
+ }, [repoId]);
141
+
142
+ async function handleDownload(file) {
143
+ setDownloading(file.filename);
144
+ try {
145
+ await api.post('/models/download', { repoId, filename: file.filename });
146
+ toast.success(`Downloading ${file.filename}`);
147
+ onDownload?.(file.filename);
148
+ } catch (err) {
149
+ toast.error(err.message);
150
+ }
151
+ setDownloading(null);
152
+ }
153
+
154
+ if (loading) {
155
+ return <div className="py-3 px-4 text-2xs text-text-4 font-sans">Loading quantization variants...</div>;
156
+ }
157
+
158
+ if (!files?.length) {
159
+ return <div className="py-3 px-4 text-2xs text-text-4 font-sans">No GGUF files found in this repo.</div>;
160
+ }
161
+
162
+ return (
163
+ <div className="pl-6 pr-4 pb-2 space-y-1.5">
164
+ {files.map((f) => (
165
+ <div key={f.filename} className="flex items-center gap-2 py-1.5 px-3 rounded-md bg-surface-2 text-xs font-sans">
166
+ <span className="font-mono text-text-1 truncate flex-1">{f.filename}</span>
167
+ {f.quantization && <Badge variant="subtle" className="text-2xs">{f.quantization}</Badge>}
168
+ <span className="text-text-3 text-2xs w-16 text-right">{formatBytes(f.size)}</span>
169
+ {f.estimatedRamGb && <span className="text-text-4 text-2xs w-14 text-right">~{f.estimatedRamGb}GB</span>}
170
+ <button
171
+ onClick={() => handleDownload(f)}
172
+ disabled={downloading === f.filename}
173
+ className="p-1 rounded text-accent hover:bg-accent/10 transition-colors disabled:opacity-40"
174
+ >
175
+ {downloading === f.filename ? <Loader2 size={13} className="animate-spin" /> : <Download size={13} />}
176
+ </button>
177
+ </div>
178
+ ))}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ // ---- Main View ----
184
+ export default function ModelsView() {
185
+ const [tab, setTab] = useState('installed'); // installed | search
186
+ const [searchQuery, setSearchQuery] = useState('');
187
+ const [searchResults, setSearchResults] = useState([]);
188
+ const [searching, setSearching] = useState(false);
189
+ const [installed, setInstalled] = useState([]);
190
+ const [downloads, setDownloads] = useState([]);
191
+ const [hardware, setHardware] = useState(null);
192
+ const [expandedResult, setExpandedResult] = useState(null);
193
+ const toast = useToast();
194
+
195
+ // Fetch installed models
196
+ const fetchInstalled = useCallback(() => {
197
+ api.get('/models/installed').then((data) => {
198
+ setInstalled(data.models || []);
199
+ }).catch(() => {});
200
+ }, []);
201
+
202
+ // Fetch hardware info
203
+ useEffect(() => {
204
+ api.get('/providers/ollama/hardware').then(setHardware).catch(() => {});
205
+ fetchInstalled();
206
+ }, [fetchInstalled]);
207
+
208
+ // Listen for download progress via WebSocket
209
+ useEffect(() => {
210
+ const unsub = useGrooveStore.subscribe((state, prev) => {
211
+ // Refresh on model events
212
+ });
213
+
214
+ // Poll active downloads
215
+ const poll = setInterval(() => {
216
+ api.get('/models/downloads').then(setDownloads).catch(() => {});
217
+ }, 2000);
218
+
219
+ return () => { unsub(); clearInterval(poll); };
220
+ }, []);
221
+
222
+ // WebSocket events for download progress
223
+ useEffect(() => {
224
+ function handleWs(event) {
225
+ try {
226
+ const msg = JSON.parse(event.data);
227
+ if (msg.type === 'model:download:progress') {
228
+ setDownloads((prev) => {
229
+ const idx = prev.findIndex((d) => d.filename === msg.data.filename);
230
+ if (idx >= 0) {
231
+ const next = [...prev];
232
+ next[idx] = msg.data;
233
+ return next;
234
+ }
235
+ return [...prev, msg.data];
236
+ });
237
+ }
238
+ if (msg.type === 'model:download:complete') {
239
+ setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
240
+ fetchInstalled();
241
+ toast.success(`${msg.data.filename} downloaded`);
242
+ }
243
+ if (msg.type === 'model:download:error') {
244
+ setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
245
+ toast.error(`Download failed: ${msg.data.error}`);
246
+ }
247
+ } catch {}
248
+ }
249
+ const ws = useGrooveStore.getState()._ws;
250
+ if (ws) ws.addEventListener('message', handleWs);
251
+ return () => { if (ws) ws.removeEventListener('message', handleWs); };
252
+ }, [fetchInstalled, toast]);
253
+
254
+ async function handleSearch() {
255
+ if (!searchQuery.trim()) return;
256
+ setSearching(true);
257
+ setTab('search');
258
+ try {
259
+ const results = await api.get(`/models/search?q=${encodeURIComponent(searchQuery.trim())}`);
260
+ setSearchResults(results);
261
+ } catch (err) {
262
+ toast.error(err.message);
263
+ }
264
+ setSearching(false);
265
+ }
266
+
267
+ async function handleDelete(modelId) {
268
+ try {
269
+ await api.delete(`/models/${modelId}`);
270
+ setInstalled((prev) => prev.filter((m) => m.id !== modelId));
271
+ toast.success('Model deleted');
272
+ } catch (err) {
273
+ toast.error(err.message);
274
+ }
275
+ }
276
+
277
+ return (
278
+ <div className="h-full flex flex-col bg-surface-0">
279
+ {/* Header */}
280
+ <div className="flex-shrink-0 px-5 pt-4 pb-3 border-b border-border space-y-3">
281
+ <div className="flex items-center justify-between">
282
+ <h1 className="text-base font-bold font-sans text-text-0">Local Models</h1>
283
+ <Badge variant="subtle" className="text-2xs">{installed.length} installed</Badge>
284
+ </div>
285
+
286
+ <HardwareBar hardware={hardware} />
287
+
288
+ {/* Search */}
289
+ <div className="flex gap-2">
290
+ <div className="relative flex-1">
291
+ <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4" />
292
+ <input
293
+ value={searchQuery}
294
+ onChange={(e) => setSearchQuery(e.target.value)}
295
+ onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
296
+ placeholder="Search HuggingFace for GGUF models..."
297
+ className="w-full h-8 pl-9 pr-3 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
298
+ />
299
+ </div>
300
+ <Button onClick={handleSearch} disabled={searching} size="sm" variant="accent">
301
+ {searching ? <Loader2 size={14} className="animate-spin" /> : 'Search'}
302
+ </Button>
303
+ </div>
304
+
305
+ {/* Tabs */}
306
+ <div className="flex gap-1">
307
+ {['installed', 'search'].map((t) => (
308
+ <button
309
+ key={t}
310
+ onClick={() => setTab(t)}
311
+ className={cn(
312
+ 'px-3 py-1 rounded-md text-xs font-sans font-medium transition-colors cursor-pointer capitalize',
313
+ tab === t ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
314
+ )}
315
+ >
316
+ {t === 'installed' ? `Installed (${installed.length})` : `Search Results (${searchResults.length})`}
317
+ </button>
318
+ ))}
319
+ </div>
320
+ </div>
321
+
322
+ {/* Active Downloads */}
323
+ {downloads.length > 0 && (
324
+ <div className="px-5 py-3 border-b border-border space-y-2">
325
+ <div className="text-xs font-sans font-semibold text-text-2">Downloading</div>
326
+ {downloads.map((d) => <DownloadProgress key={d.filename} download={d} />)}
327
+ </div>
328
+ )}
329
+
330
+ {/* Content */}
331
+ <ScrollArea className="flex-1">
332
+ <div className="px-5 py-4 space-y-2">
333
+ {tab === 'installed' && (
334
+ <>
335
+ {installed.length === 0 ? (
336
+ <div className="text-center py-12">
337
+ <Box size={40} className="mx-auto text-text-4 mb-3" />
338
+ <p className="text-sm text-text-2 font-sans font-medium">No local models yet</p>
339
+ <p className="text-xs text-text-3 font-sans mt-1">Search HuggingFace to download GGUF models, or pull models via Ollama.</p>
340
+ </div>
341
+ ) : (
342
+ installed.map((m) => <InstalledModel key={m.id} model={m} onDelete={handleDelete} />)
343
+ )}
344
+ </>
345
+ )}
346
+
347
+ {tab === 'search' && (
348
+ <>
349
+ {searching ? (
350
+ <div className="text-center py-12">
351
+ <Loader2 size={24} className="mx-auto text-accent animate-spin mb-3" />
352
+ <p className="text-sm text-text-3 font-sans">Searching HuggingFace...</p>
353
+ </div>
354
+ ) : searchResults.length === 0 ? (
355
+ <div className="text-center py-12">
356
+ <Search size={40} className="mx-auto text-text-4 mb-3" />
357
+ <p className="text-sm text-text-2 font-sans font-medium">Search for GGUF models</p>
358
+ <p className="text-xs text-text-3 font-sans mt-1">Try "qwen coder", "deepseek", "codestral", "llama"</p>
359
+ </div>
360
+ ) : (
361
+ searchResults.map((r) => (
362
+ <div key={r.id} className="space-y-1">
363
+ <SearchResult
364
+ result={r}
365
+ expanded={expandedResult === r.id}
366
+ onExpand={setExpandedResult}
367
+ />
368
+ {expandedResult === r.id && (
369
+ <FilePicker repoId={r.id} onDownload={() => fetchInstalled()} />
370
+ )}
371
+ </div>
372
+ ))
373
+ )}
374
+ </>
375
+ )}
376
+ </div>
377
+ </ScrollArea>
378
+ </div>
379
+ );
380
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.25.21",
4
- "description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
3
+ "version": "0.26.2",
4
+ "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
7
7
  "homepage": "https://groovedev.ai",