groove-dev 0.19.2 → 0.19.4
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/CHANGELOG.md +15 -0
- package/node_modules/@groove-dev/cli/bin/groove.js +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +68 -0
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +164 -26
- package/node_modules/@groove-dev/gui/.groove/codebase-index.json +3 -3
- package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
- package/node_modules/@groove-dev/gui/.groove/state.json +1 -1
- package/node_modules/@groove-dev/gui/.groove/timeline.json +504 -0
- package/node_modules/@groove-dev/gui/AGENTS_REGISTRY.md +9 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DCakRxDE.css → index-DgIS4D-j.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-DbujgqhS.js → index-eysbqREF.js} +123 -123
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +9 -3
- package/node_modules/@groove-dev/gui/src/components/agents/ollama-setup.jsx +375 -0
- package/package.json +1 -1
- package/packages/cli/bin/groove.js +1 -1
- package/packages/daemon/src/api.js +68 -0
- package/packages/daemon/src/providers/ollama.js +164 -26
- package/packages/gui/dist/assets/{index-DCakRxDE.css → index-DgIS4D-j.css} +1 -1
- package/packages/gui/dist/assets/{index-DbujgqhS.js → index-eysbqREF.js} +123 -123
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-config.jsx +9 -3
- package/packages/gui/src/components/agents/ollama-setup.jsx +375 -0
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Groove GUI</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-eysbqREF.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DgIS4D-j.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -14,6 +14,7 @@ import { FolderBrowser } from './folder-browser';
|
|
|
14
14
|
import { api } from '../../lib/api';
|
|
15
15
|
import { cn } from '../../lib/cn';
|
|
16
16
|
import { timeAgo } from '../../lib/format';
|
|
17
|
+
import { OllamaSetup } from './ollama-setup';
|
|
17
18
|
|
|
18
19
|
/* ── Segmented Control ─────────────────────────────────────── */
|
|
19
20
|
|
|
@@ -318,10 +319,15 @@ export function AgentConfig({ agent }) {
|
|
|
318
319
|
</button>
|
|
319
320
|
|
|
320
321
|
{/* Expanded: models + key management */}
|
|
321
|
-
{isExpanded && (
|
|
322
|
+
{isExpanded && p.authType === 'local' && (
|
|
322
323
|
<div className="border-t border-border-subtle">
|
|
323
|
-
{
|
|
324
|
-
|
|
324
|
+
<OllamaSetup isInstalled={available} onModelChange={loadProviders} />
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
{isExpanded && p.authType !== 'local' && (
|
|
328
|
+
<div className="border-t border-border-subtle">
|
|
329
|
+
{/* API Key row */}
|
|
330
|
+
{(!available || p.hasKey) && (
|
|
325
331
|
<div className="px-3 py-2 bg-surface-1/50">
|
|
326
332
|
{settingKeyFor === p.id ? (
|
|
327
333
|
<div className="flex gap-1.5">
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Download, Check, Cpu, HardDrive, RefreshCw, Copy,
|
|
5
|
+
Trash2, ChevronDown, Star, Zap, AlertCircle, Monitor,
|
|
6
|
+
} from 'lucide-react';
|
|
7
|
+
import { Button } from '../ui/button';
|
|
8
|
+
import { Badge } from '../ui/badge';
|
|
9
|
+
import { cn } from '../../lib/cn';
|
|
10
|
+
import { api } from '../../lib/api';
|
|
11
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
12
|
+
|
|
13
|
+
const CATEGORY_LABELS = { code: 'Code', general: 'General' };
|
|
14
|
+
const TIER_COLORS = { light: 'text-success', medium: 'text-accent', heavy: 'text-warning' };
|
|
15
|
+
|
|
16
|
+
function formatSize(gb) {
|
|
17
|
+
return gb < 1 ? `${Math.round(gb * 1024)} MB` : `${gb} GB`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* ── Hardware Info Bar ──────────────────────────────────────── */
|
|
21
|
+
|
|
22
|
+
function HardwareBar({ hardware }) {
|
|
23
|
+
if (!hardware) return null;
|
|
24
|
+
const { totalRamGb, gpu, isAppleSilicon } = hardware;
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex items-center gap-3 bg-surface-0 rounded-lg border border-border-subtle px-3 py-2.5">
|
|
27
|
+
<Monitor size={14} className="text-text-3 flex-shrink-0" />
|
|
28
|
+
<div className="flex-1 min-w-0">
|
|
29
|
+
<div className="flex items-center gap-2 text-xs font-sans">
|
|
30
|
+
<span className="text-text-0 font-semibold">{totalRamGb} GB RAM</span>
|
|
31
|
+
{gpu && (
|
|
32
|
+
<>
|
|
33
|
+
<span className="text-text-4">·</span>
|
|
34
|
+
<span className="text-text-2">{gpu.name}</span>
|
|
35
|
+
</>
|
|
36
|
+
)}
|
|
37
|
+
{isAppleSilicon && (
|
|
38
|
+
<Badge variant="accent" className="text-2xs">Unified Memory</Badge>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Install Section (not installed) ───────────────────────── */
|
|
47
|
+
|
|
48
|
+
function InstallSection({ onRecheck }) {
|
|
49
|
+
const [data, setData] = useState(null);
|
|
50
|
+
const [checking, setChecking] = useState(false);
|
|
51
|
+
const [starting, setStarting] = useState(false);
|
|
52
|
+
const [copied, setCopied] = useState(false);
|
|
53
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
api.post('/providers/ollama/check').then(setData).catch(() => {});
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
async function handleRecheck() {
|
|
60
|
+
setChecking(true);
|
|
61
|
+
try {
|
|
62
|
+
const result = await api.post('/providers/ollama/check');
|
|
63
|
+
setData(result);
|
|
64
|
+
if (result.installed && result.serverRunning) {
|
|
65
|
+
addToast('success', 'Ollama is ready!');
|
|
66
|
+
onRecheck();
|
|
67
|
+
} else if (result.installed) {
|
|
68
|
+
addToast('info', 'Ollama installed — server needs to start');
|
|
69
|
+
} else {
|
|
70
|
+
addToast('info', 'Ollama not found — install and try again');
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
setChecking(false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handleStartServer() {
|
|
77
|
+
setStarting(true);
|
|
78
|
+
try {
|
|
79
|
+
const result = await api.post('/providers/ollama/serve');
|
|
80
|
+
if (result.ok) {
|
|
81
|
+
addToast('success', 'Ollama server started!');
|
|
82
|
+
// Recheck to update state
|
|
83
|
+
const check = await api.post('/providers/ollama/check');
|
|
84
|
+
setData(check);
|
|
85
|
+
if (check.serverRunning) onRecheck();
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
addToast('error', 'Could not start server', err.message);
|
|
89
|
+
}
|
|
90
|
+
setStarting(false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleCopy(text) {
|
|
94
|
+
navigator.clipboard.writeText(text);
|
|
95
|
+
setCopied(true);
|
|
96
|
+
setTimeout(() => setCopied(false), 2000);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!data) return <div className="py-4 text-center text-xs text-text-4 font-sans">Loading...</div>;
|
|
100
|
+
|
|
101
|
+
const { hardware, install, requirements, installed, serverRunning } = data;
|
|
102
|
+
const canRun = hardware.totalRamGb >= requirements.minRAM;
|
|
103
|
+
const recommended = hardware.recommended;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="space-y-3 p-3">
|
|
107
|
+
<HardwareBar hardware={hardware} />
|
|
108
|
+
|
|
109
|
+
{canRun ? (
|
|
110
|
+
<div className="flex items-start gap-2 bg-success/8 border border-success/20 rounded-lg px-3 py-2.5">
|
|
111
|
+
<Check size={14} className="text-success flex-shrink-0 mt-0.5" />
|
|
112
|
+
<div className="text-xs font-sans">
|
|
113
|
+
<span className="text-success font-semibold">Your system is ready.</span>
|
|
114
|
+
<span className="text-text-2 ml-1">
|
|
115
|
+
{recommended.code
|
|
116
|
+
? `Recommended: ${recommended.code}`
|
|
117
|
+
: `${hardware.totalRamGb} GB RAM available`}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex items-start gap-2 bg-warning/8 border border-warning/20 rounded-lg px-3 py-2.5">
|
|
123
|
+
<AlertCircle size={14} className="text-warning flex-shrink-0 mt-0.5" />
|
|
124
|
+
<div className="text-xs font-sans text-text-2">
|
|
125
|
+
<span className="text-warning font-semibold">{hardware.totalRamGb} GB RAM detected.</span>
|
|
126
|
+
{' '}Minimum {requirements.minRAM} GB needed. Smallest models may still work.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* State: Installed but server not running */}
|
|
132
|
+
{installed && !serverRunning && (
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
<div className="flex items-start gap-2 bg-warning/8 border border-warning/20 rounded-lg px-3 py-2.5">
|
|
135
|
+
<AlertCircle size={14} className="text-warning flex-shrink-0 mt-0.5" />
|
|
136
|
+
<div className="text-xs font-sans text-text-2">
|
|
137
|
+
<span className="text-warning font-semibold">Ollama installed but server not running.</span>
|
|
138
|
+
{' '}The server needs to be running to pull and use models.
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<Button variant="primary" size="md" onClick={handleStartServer} disabled={starting} className="w-full gap-1.5">
|
|
142
|
+
<Zap size={12} />
|
|
143
|
+
{starting ? 'Starting...' : 'Start Ollama Server'}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{/* State: Not installed */}
|
|
149
|
+
{!installed && (
|
|
150
|
+
<div className="space-y-1.5">
|
|
151
|
+
<p className="text-xs font-semibold text-text-1 font-sans">Install Ollama</p>
|
|
152
|
+
<div className="flex items-center gap-2">
|
|
153
|
+
<code className="flex-1 bg-surface-0 border border-border-subtle rounded-md px-3 py-2 text-xs font-mono text-text-1 truncate">
|
|
154
|
+
{install.command}
|
|
155
|
+
</code>
|
|
156
|
+
<Button
|
|
157
|
+
variant="secondary"
|
|
158
|
+
size="sm"
|
|
159
|
+
onClick={() => handleCopy(install.command)}
|
|
160
|
+
className="h-8 px-2.5 gap-1 flex-shrink-0"
|
|
161
|
+
>
|
|
162
|
+
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
163
|
+
{copied ? 'Copied' : 'Copy'}
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
{install.alt && (
|
|
167
|
+
<p className="text-2xs text-text-4 font-sans">{install.alt}</p>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
<Button variant="secondary" size="md" onClick={handleRecheck} disabled={checking} className="w-full gap-1.5">
|
|
173
|
+
<RefreshCw size={12} className={checking ? 'animate-spin' : ''} />
|
|
174
|
+
{checking ? 'Checking...' : installed ? 'Check again' : 'I installed it — check again'}
|
|
175
|
+
</Button>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ── Model Row ─────────────────────────────────────────────── */
|
|
181
|
+
|
|
182
|
+
function ModelRow({ model, isInstalled, isRecommended, canRun, onPull, onDelete, pulling }) {
|
|
183
|
+
const isPulling = pulling === model.id;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className={cn(
|
|
187
|
+
'flex items-center gap-2 px-3 py-2 border-t border-border-subtle transition-colors',
|
|
188
|
+
!canRun && 'opacity-40',
|
|
189
|
+
)}>
|
|
190
|
+
{isInstalled ? (
|
|
191
|
+
<Check size={12} className="text-success flex-shrink-0" />
|
|
192
|
+
) : (
|
|
193
|
+
<div className="w-3" />
|
|
194
|
+
)}
|
|
195
|
+
<div className="flex-1 min-w-0">
|
|
196
|
+
<div className="flex items-center gap-1.5">
|
|
197
|
+
<span className="text-xs font-mono text-text-1 truncate">{model.name}</span>
|
|
198
|
+
{isRecommended && <Star size={10} className="text-warning flex-shrink-0" />}
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
201
|
+
<span className={cn('text-2xs font-semibold font-sans', TIER_COLORS[model.tier])}>
|
|
202
|
+
{model.tier}
|
|
203
|
+
</span>
|
|
204
|
+
<span className="text-2xs text-text-4 font-sans">{formatSize(model.sizeGb)}</span>
|
|
205
|
+
<span className="text-2xs text-text-4 font-sans">· {model.ramGb} GB RAM</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
{isInstalled ? (
|
|
209
|
+
<button
|
|
210
|
+
onClick={() => onDelete(model.id)}
|
|
211
|
+
className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
|
|
212
|
+
title="Remove model"
|
|
213
|
+
>
|
|
214
|
+
<Trash2 size={12} />
|
|
215
|
+
</button>
|
|
216
|
+
) : canRun ? (
|
|
217
|
+
<Button
|
|
218
|
+
variant="secondary"
|
|
219
|
+
size="sm"
|
|
220
|
+
onClick={() => onPull(model.id)}
|
|
221
|
+
disabled={!!pulling}
|
|
222
|
+
className="h-7 px-2 text-2xs gap-1"
|
|
223
|
+
>
|
|
224
|
+
{isPulling ? (
|
|
225
|
+
<><RefreshCw size={10} className="animate-spin" /> Pulling...</>
|
|
226
|
+
) : (
|
|
227
|
+
<><Download size={10} /> Pull</>
|
|
228
|
+
)}
|
|
229
|
+
</Button>
|
|
230
|
+
) : (
|
|
231
|
+
<span className="text-2xs text-text-4 font-sans">Needs {model.ramGb} GB</span>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ── Model Browser (installed) ─────────────────────────────── */
|
|
238
|
+
|
|
239
|
+
function ModelBrowser({ onModelChange }) {
|
|
240
|
+
const [data, setData] = useState(null);
|
|
241
|
+
const [pulling, setPulling] = useState(null);
|
|
242
|
+
const [category, setCategory] = useState('code');
|
|
243
|
+
const [showAll, setShowAll] = useState(false);
|
|
244
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
245
|
+
|
|
246
|
+
function load() {
|
|
247
|
+
api.get('/providers/ollama/models').then(setData).catch(() => {});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
useEffect(() => { load(); }, []);
|
|
251
|
+
|
|
252
|
+
async function handlePull(modelId) {
|
|
253
|
+
setPulling(modelId);
|
|
254
|
+
try {
|
|
255
|
+
await api.post('/providers/ollama/pull', { model: modelId });
|
|
256
|
+
addToast('success', `Pulled ${modelId}`);
|
|
257
|
+
load();
|
|
258
|
+
if (onModelChange) onModelChange();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
addToast('error', `Pull failed: ${err.message}`);
|
|
261
|
+
}
|
|
262
|
+
setPulling(null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function handleDelete(modelId) {
|
|
266
|
+
try {
|
|
267
|
+
await api.delete(`/providers/ollama/models/${encodeURIComponent(modelId)}`);
|
|
268
|
+
addToast('info', `Removed ${modelId}`);
|
|
269
|
+
load();
|
|
270
|
+
if (onModelChange) onModelChange();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
addToast('error', `Delete failed: ${err.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!data) return <div className="py-4 text-center text-xs text-text-4 font-sans">Loading...</div>;
|
|
277
|
+
|
|
278
|
+
const { installed, catalog, hardware } = data;
|
|
279
|
+
const installedIds = new Set(installed.map((m) => m.id));
|
|
280
|
+
const maxRam = hardware.totalRamGb;
|
|
281
|
+
const recommended = [hardware.recommended?.code, hardware.recommended?.general].filter(Boolean);
|
|
282
|
+
|
|
283
|
+
const filtered = catalog.filter((m) => m.category === category);
|
|
284
|
+
const visible = showAll ? filtered : filtered.filter((m) => m.ramGb <= maxRam);
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className="space-y-2 p-3">
|
|
288
|
+
<HardwareBar hardware={hardware} />
|
|
289
|
+
|
|
290
|
+
{/* Installed count */}
|
|
291
|
+
{installed.length > 0 && (
|
|
292
|
+
<div className="flex items-center gap-1.5 text-xs font-sans text-text-2">
|
|
293
|
+
<HardDrive size={12} className="text-text-3" />
|
|
294
|
+
<span className="font-semibold">{installed.length}</span> model{installed.length !== 1 ? 's' : ''} installed
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{/* Category tabs */}
|
|
299
|
+
<div className="flex bg-surface-0 rounded-lg p-0.5 border border-border-subtle">
|
|
300
|
+
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
|
|
301
|
+
<button
|
|
302
|
+
key={key}
|
|
303
|
+
onClick={() => setCategory(key)}
|
|
304
|
+
className={cn(
|
|
305
|
+
'flex-1 px-3 py-1.5 text-2xs font-semibold font-sans rounded-md transition-all cursor-pointer',
|
|
306
|
+
category === key
|
|
307
|
+
? 'bg-accent/15 text-accent shadow-sm'
|
|
308
|
+
: 'text-text-3 hover:text-text-1',
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
{label}
|
|
312
|
+
</button>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
{/* Model list */}
|
|
317
|
+
<div className="rounded-lg border border-border-subtle bg-surface-0 overflow-hidden">
|
|
318
|
+
{visible.map((model) => (
|
|
319
|
+
<ModelRow
|
|
320
|
+
key={model.id}
|
|
321
|
+
model={model}
|
|
322
|
+
isInstalled={installedIds.has(model.id)}
|
|
323
|
+
isRecommended={recommended.includes(model.id)}
|
|
324
|
+
canRun={model.ramGb <= maxRam}
|
|
325
|
+
onPull={handlePull}
|
|
326
|
+
onDelete={handleDelete}
|
|
327
|
+
pulling={pulling}
|
|
328
|
+
/>
|
|
329
|
+
))}
|
|
330
|
+
{visible.length === 0 && (
|
|
331
|
+
<div className="px-3 py-4 text-center text-xs text-text-4 font-sans">
|
|
332
|
+
No {category} models available for your hardware
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Show models beyond RAM */}
|
|
338
|
+
{!showAll && filtered.length > visible.length && (
|
|
339
|
+
<button
|
|
340
|
+
onClick={() => setShowAll(true)}
|
|
341
|
+
className="flex items-center gap-1 text-2xs text-text-3 hover:text-accent font-sans cursor-pointer transition-colors"
|
|
342
|
+
>
|
|
343
|
+
<ChevronDown size={10} />
|
|
344
|
+
Show {filtered.length - visible.length} more (exceed your RAM)
|
|
345
|
+
</button>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* ── Main Export ────────────────────────────────────────────── */
|
|
352
|
+
|
|
353
|
+
export function OllamaSetup({ isInstalled: initialInstalled, onModelChange }) {
|
|
354
|
+
const [ready, setReady] = useState(false);
|
|
355
|
+
const [checked, setChecked] = useState(false);
|
|
356
|
+
|
|
357
|
+
// On mount, verify server is actually running (not just binary installed)
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (initialInstalled) {
|
|
360
|
+
api.post('/providers/ollama/check')
|
|
361
|
+
.then((data) => { setReady(data.installed && data.serverRunning); setChecked(true); })
|
|
362
|
+
.catch(() => setChecked(true));
|
|
363
|
+
} else {
|
|
364
|
+
setChecked(true);
|
|
365
|
+
}
|
|
366
|
+
}, [initialInstalled]);
|
|
367
|
+
|
|
368
|
+
if (!checked) return <div className="py-4 text-center text-xs text-text-4 font-sans">Checking Ollama...</div>;
|
|
369
|
+
|
|
370
|
+
if (!ready) {
|
|
371
|
+
return <InstallSection onRecheck={() => { setReady(true); if (onModelChange) onModelChange(); }} />;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return <ModelBrowser onModelChange={onModelChange} />;
|
|
375
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.4",
|
|
4
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.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
|
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
|
|
8
8
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
9
9
|
import { listProviders, getProvider } from './providers/index.js';
|
|
10
|
+
import { OllamaProvider } from './providers/ollama.js';
|
|
10
11
|
import { validateAgentConfig } from './validate.js';
|
|
11
12
|
|
|
12
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -126,6 +127,73 @@ export function createApi(app, daemon) {
|
|
|
126
127
|
res.json(providers);
|
|
127
128
|
});
|
|
128
129
|
|
|
130
|
+
// --- Ollama ---
|
|
131
|
+
|
|
132
|
+
app.get('/api/providers/ollama/hardware', (req, res) => {
|
|
133
|
+
res.json(OllamaProvider.getSystemHardware());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.get('/api/providers/ollama/models', (req, res) => {
|
|
137
|
+
const installed = OllamaProvider.isInstalled() ? OllamaProvider.getInstalledModels() : [];
|
|
138
|
+
const catalog = OllamaProvider.catalog;
|
|
139
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
140
|
+
res.json({ installed, catalog, hardware });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
app.post('/api/providers/ollama/pull', async (req, res) => {
|
|
144
|
+
const { model } = req.body;
|
|
145
|
+
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
146
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
147
|
+
const broadcast = daemon.broadcast || (() => {});
|
|
148
|
+
try {
|
|
149
|
+
broadcast({ type: 'ollama:pull:start', model });
|
|
150
|
+
await OllamaProvider.pullModel(model, (progress) => {
|
|
151
|
+
broadcast({ type: 'ollama:pull:progress', model, progress: progress.trim() });
|
|
152
|
+
});
|
|
153
|
+
broadcast({ type: 'ollama:pull:complete', model });
|
|
154
|
+
daemon.audit.log('ollama.pull', { model });
|
|
155
|
+
res.json({ ok: true, model });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
broadcast({ type: 'ollama:pull:error', model, error: err.message });
|
|
158
|
+
res.status(500).json({ error: `Pull failed: ${err.message}` });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.delete('/api/providers/ollama/models/:model', (req, res) => {
|
|
163
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
164
|
+
const success = OllamaProvider.deleteModel(req.params.model);
|
|
165
|
+
if (success) {
|
|
166
|
+
daemon.audit.log('ollama.delete', { model: req.params.model });
|
|
167
|
+
res.json({ ok: true });
|
|
168
|
+
} else {
|
|
169
|
+
res.status(500).json({ error: 'Failed to delete model' });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
app.post('/api/providers/ollama/check', async (req, res) => {
|
|
174
|
+
const installed = OllamaProvider.isInstalled();
|
|
175
|
+
const serverRunning = installed ? await OllamaProvider.isServerRunning() : false;
|
|
176
|
+
const install = OllamaProvider.installCommand();
|
|
177
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
178
|
+
const requirements = OllamaProvider.hardwareRequirements();
|
|
179
|
+
res.json({ installed, serverRunning, install, hardware, requirements });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
app.post('/api/providers/ollama/serve', async (req, res) => {
|
|
183
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
184
|
+
const already = await OllamaProvider.isServerRunning();
|
|
185
|
+
if (already) return res.json({ ok: true, alreadyRunning: true });
|
|
186
|
+
const result = OllamaProvider.startServer();
|
|
187
|
+
if (result.started) {
|
|
188
|
+
// Wait a moment for server to come up
|
|
189
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
190
|
+
const running = await OllamaProvider.isServerRunning();
|
|
191
|
+
res.json({ ok: running, method: result.method });
|
|
192
|
+
} else {
|
|
193
|
+
res.status(500).json({ error: 'Could not start server', command: result.command });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
129
197
|
// --- Credentials ---
|
|
130
198
|
|
|
131
199
|
app.get('/api/credentials', (req, res) => {
|