groove-dev 0.27.42 → 0.27.45
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/default/groovedev-beta-auth-endpoint.md +166 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +619 -0
- package/node_modules/@groove-dev/daemon/src/firstrun.js +11 -0
- package/node_modules/@groove-dev/daemon/src/index.js +28 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +114 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BoIbnaqa.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +3 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +5 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +7 -3
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +12 -0
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +25 -7
- package/node_modules/@groove-dev/gui/src/components/network/network-status.jsx +164 -0
- package/node_modules/@groove-dev/gui/src/components/network/node-details.jsx +66 -0
- package/node_modules/@groove-dev/gui/src/components/network/node-toggle.jsx +172 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +191 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/views/network.jsx +227 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +88 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +619 -0
- package/packages/daemon/src/firstrun.js +11 -0
- package/packages/daemon/src/index.js +28 -0
- package/packages/daemon/src/providers/claude-code.js +1 -1
- package/packages/daemon/src/providers/groove-network.js +114 -0
- package/packages/daemon/src/providers/index.js +2 -0
- package/packages/gui/dist/assets/index-BoIbnaqa.js +8607 -0
- package/packages/gui/dist/assets/index-CyVj0fHl.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +3 -0
- package/packages/gui/src/components/editor/terminal.jsx +5 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +7 -3
- package/packages/gui/src/components/layout/status-bar.jsx +12 -0
- package/packages/gui/src/components/layout/terminal-panel.jsx +25 -7
- package/packages/gui/src/components/network/network-status.jsx +164 -0
- package/packages/gui/src/components/network/node-details.jsx +66 -0
- package/packages/gui/src/components/network/node-toggle.jsx +172 -0
- package/packages/gui/src/stores/groove.js +191 -0
- package/packages/gui/src/views/agents.jsx +1 -1
- package/packages/gui/src/views/network.jsx +227 -0
- package/packages/gui/src/views/settings.jsx +88 -1
- package/analyist/groove-security-audit.md +0 -323
- package/node_modules/@groove-dev/gui/dist/assets/index-C1C2biHU.js +0 -8607
- package/node_modules/@groove-dev/gui/dist/assets/index-Dx7i-7_K.css +0 -1
- package/packages/gui/dist/assets/index-C1C2biHU.js +0 -8607
- package/packages/gui/dist/assets/index-Dx7i-7_K.css +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../stores/groove';
|
|
4
|
+
import { ScrollArea } from '../components/ui/scroll-area';
|
|
5
|
+
import { StatusDot } from '../components/ui/status-dot';
|
|
6
|
+
import { Badge } from '../components/ui/badge';
|
|
7
|
+
import { Button } from '../components/ui/button';
|
|
8
|
+
import { Dialog, DialogContent, DialogTrigger } from '../components/ui/dialog';
|
|
9
|
+
import { NodeToggle } from '../components/network/node-toggle';
|
|
10
|
+
import { NodeDetails } from '../components/network/node-details';
|
|
11
|
+
import { NetworkStatus } from '../components/network/network-status';
|
|
12
|
+
import { Globe, Download, Check, AlertCircle, Loader2, Trash2 } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
const REQUIREMENTS = [
|
|
15
|
+
'Python 3.10 or higher',
|
|
16
|
+
'~2 GB disk space for model shards',
|
|
17
|
+
'8 GB+ RAM recommended',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function InstallProgress({ progress }) {
|
|
21
|
+
const percent = Math.max(0, Math.min(100, Number.isFinite(progress.percent) ? progress.percent : 0));
|
|
22
|
+
return (
|
|
23
|
+
<div className="w-full flex flex-col gap-3">
|
|
24
|
+
<div className="h-2 w-full rounded-full bg-surface-3 overflow-hidden">
|
|
25
|
+
<div
|
|
26
|
+
className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
|
|
27
|
+
style={{ width: `${percent}%` }}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="flex items-center justify-between text-2xs font-mono text-text-3 tabular-nums">
|
|
31
|
+
<div className="flex items-center gap-2 text-text-2 font-sans">
|
|
32
|
+
<Loader2 size={12} className="animate-spin text-accent" />
|
|
33
|
+
<span className="truncate">{progress.message || 'Installing…'}</span>
|
|
34
|
+
</div>
|
|
35
|
+
<span>{percent}%</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function InstallError({ message, onRetry }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="w-full flex flex-col gap-3">
|
|
44
|
+
<div className="rounded-md border border-danger/40 bg-danger/10 px-4 py-3 flex items-start gap-2.5 text-left">
|
|
45
|
+
<AlertCircle size={14} className="text-danger flex-shrink-0 mt-0.5" />
|
|
46
|
+
<div className="flex-1 min-w-0">
|
|
47
|
+
<div className="text-xs font-semibold text-danger font-sans mb-0.5">Install failed</div>
|
|
48
|
+
<div className="text-xs text-text-1 font-sans break-words">{message}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<Button variant="primary" size="lg" onClick={onRetry} className="w-full">
|
|
52
|
+
<Download size={14} />
|
|
53
|
+
Retry Install
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function InstallGate() {
|
|
60
|
+
const installNetworkPackage = useGrooveStore((s) => s.installNetworkPackage);
|
|
61
|
+
const progress = useGrooveStore((s) => s.networkInstallProgress);
|
|
62
|
+
const { installing, error } = progress;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col items-center justify-center min-h-full px-6 py-12">
|
|
66
|
+
<div className="w-full max-w-md flex flex-col items-center text-center">
|
|
67
|
+
<div className="mb-5 rounded-full bg-surface-2 border border-border-subtle p-5">
|
|
68
|
+
<Globe size={48} className="text-text-3" strokeWidth={1.25} />
|
|
69
|
+
</div>
|
|
70
|
+
<h3 className="text-base font-semibold text-text-0 font-sans mb-2">
|
|
71
|
+
Install Groove Network
|
|
72
|
+
</h3>
|
|
73
|
+
<p className="text-sm text-text-2 font-sans leading-relaxed mb-6">
|
|
74
|
+
The network package enables decentralized LLM inference. Contribute your compute power or run models across the Groove network.
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
<div className="w-full rounded-md border border-border-subtle bg-surface-1 px-4 py-3 mb-6">
|
|
78
|
+
<div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2 text-left">
|
|
79
|
+
Requirements
|
|
80
|
+
</div>
|
|
81
|
+
<ul className="flex flex-col gap-1.5">
|
|
82
|
+
{REQUIREMENTS.map((req) => (
|
|
83
|
+
<li key={req} className="flex items-center gap-2 text-xs font-sans text-text-1 text-left">
|
|
84
|
+
<Check size={12} className="text-accent flex-shrink-0" />
|
|
85
|
+
<span>{req}</span>
|
|
86
|
+
</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{installing ? (
|
|
92
|
+
<InstallProgress progress={progress} />
|
|
93
|
+
) : error ? (
|
|
94
|
+
<InstallError message={error} onRetry={() => installNetworkPackage()} />
|
|
95
|
+
) : (
|
|
96
|
+
<>
|
|
97
|
+
<Button
|
|
98
|
+
variant="primary"
|
|
99
|
+
size="lg"
|
|
100
|
+
onClick={() => installNetworkPackage()}
|
|
101
|
+
className="w-full"
|
|
102
|
+
>
|
|
103
|
+
<Download size={14} />
|
|
104
|
+
Install Network Package
|
|
105
|
+
</Button>
|
|
106
|
+
<p className="text-2xs font-sans text-text-3 mt-3">
|
|
107
|
+
This will download and set up the Groove Network runtime (~500 MB)
|
|
108
|
+
</p>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function UninstallButton() {
|
|
117
|
+
const [open, setOpen] = useState(false);
|
|
118
|
+
const uninstallNetworkPackage = useGrooveStore((s) => s.uninstallNetworkPackage);
|
|
119
|
+
const [busy, setBusy] = useState(false);
|
|
120
|
+
|
|
121
|
+
const confirm = async () => {
|
|
122
|
+
setBusy(true);
|
|
123
|
+
try {
|
|
124
|
+
await uninstallNetworkPackage();
|
|
125
|
+
setOpen(false);
|
|
126
|
+
} catch { /* toast already shown */ }
|
|
127
|
+
finally { setBusy(false); }
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
132
|
+
<DialogTrigger asChild>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className="inline-flex items-center gap-1.5 text-2xs font-sans text-text-3 hover:text-danger transition-colors"
|
|
136
|
+
>
|
|
137
|
+
<Trash2 size={11} />
|
|
138
|
+
Uninstall Network Package
|
|
139
|
+
</button>
|
|
140
|
+
</DialogTrigger>
|
|
141
|
+
<DialogContent title="Uninstall Network Package" description="Confirm uninstall">
|
|
142
|
+
<div className="px-5 py-4 flex flex-col gap-3">
|
|
143
|
+
<p className="text-sm text-text-1 font-sans leading-relaxed">
|
|
144
|
+
This will stop your node and remove the network package from <span className="font-mono text-text-2">~/.groove/network</span>.
|
|
145
|
+
</p>
|
|
146
|
+
<p className="text-xs text-text-3 font-sans leading-relaxed">
|
|
147
|
+
Your identity (<span className="font-mono">~/.groove/node_key.json</span>) will be preserved — you can reinstall later without losing your wallet.
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border-subtle bg-surface-0">
|
|
151
|
+
<Button variant="ghost" size="sm" onClick={() => setOpen(false)} disabled={busy}>Cancel</Button>
|
|
152
|
+
<Button variant="danger" size="sm" onClick={confirm} disabled={busy}>
|
|
153
|
+
{busy ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
154
|
+
Uninstall
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
</DialogContent>
|
|
158
|
+
</Dialog>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default function NetworkView() {
|
|
163
|
+
const fetchNetworkNodeStatus = useGrooveStore((s) => s.fetchNetworkNodeStatus);
|
|
164
|
+
const fetchNetworkStatus = useGrooveStore((s) => s.fetchNetworkStatus);
|
|
165
|
+
const node = useGrooveStore((s) => s.networkNode);
|
|
166
|
+
const installed = useGrooveStore((s) => s.networkInstalled);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
fetchNetworkNodeStatus();
|
|
170
|
+
if (installed) {
|
|
171
|
+
fetchNetworkStatus();
|
|
172
|
+
const interval = setInterval(() => { fetchNetworkStatus(); }, 10000);
|
|
173
|
+
return () => clearInterval(interval);
|
|
174
|
+
}
|
|
175
|
+
}, [fetchNetworkNodeStatus, fetchNetworkStatus, installed]);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="flex flex-col h-full">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
<div className="flex items-center gap-3 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
|
|
181
|
+
<Globe size={14} className="text-accent" />
|
|
182
|
+
<h2 className="text-sm font-semibold text-text-0 font-sans">Groove Network</h2>
|
|
183
|
+
<Badge variant="purple">Early Access</Badge>
|
|
184
|
+
<div className="flex-1" />
|
|
185
|
+
{installed && (
|
|
186
|
+
<>
|
|
187
|
+
<UninstallButton />
|
|
188
|
+
<div className="flex items-center gap-1.5 text-2xs font-sans text-text-3">
|
|
189
|
+
<StatusDot status={node.active ? 'running' : 'crashed'} size="sm" />
|
|
190
|
+
{node.active ? 'Contributing' : 'Idle'}
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Body */}
|
|
197
|
+
<ScrollArea className="flex-1">
|
|
198
|
+
{!installed ? (
|
|
199
|
+
<InstallGate />
|
|
200
|
+
) : (
|
|
201
|
+
<div className="p-4 grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
202
|
+
{/* Left column — node operator */}
|
|
203
|
+
<div className="flex flex-col gap-3 min-w-0">
|
|
204
|
+
<div>
|
|
205
|
+
<div className="flex items-center gap-2 mb-2 px-0.5">
|
|
206
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Node Operator</span>
|
|
207
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
208
|
+
</div>
|
|
209
|
+
<NodeToggle />
|
|
210
|
+
</div>
|
|
211
|
+
<NodeDetails />
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Right column — network status */}
|
|
215
|
+
<div className="flex flex-col gap-3 min-w-0">
|
|
216
|
+
<div className="flex items-center gap-2 px-0.5">
|
|
217
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Network Status</span>
|
|
218
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
219
|
+
</div>
|
|
220
|
+
<NetworkStatus />
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</ScrollArea>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -15,7 +15,7 @@ import { fmtUptime } from '../lib/format';
|
|
|
15
15
|
import {
|
|
16
16
|
Key, Eye, EyeOff, Check, Cpu,
|
|
17
17
|
FolderOpen, FolderSearch, Users, Gauge,
|
|
18
|
-
ShieldCheck, Settings,
|
|
18
|
+
ShieldCheck, Settings, Lock,
|
|
19
19
|
Newspaper, Radio, Send, MessageSquare, MessageCircle,
|
|
20
20
|
Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink,
|
|
21
21
|
} from 'lucide-react';
|
|
@@ -940,6 +940,91 @@ function AddGatewayCard({ existingTypes, onAdd }) {
|
|
|
940
940
|
);
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
+
/* ── Early Access Section ─────────────────────────────────── */
|
|
944
|
+
|
|
945
|
+
function EarlyAccessSection() {
|
|
946
|
+
const networkUnlocked = useGrooveStore((s) => s.networkUnlocked);
|
|
947
|
+
const activateBeta = useGrooveStore((s) => s.activateBeta);
|
|
948
|
+
const deactivateBeta = useGrooveStore((s) => s.deactivateBeta);
|
|
949
|
+
const [code, setCode] = useState('');
|
|
950
|
+
const [submitting, setSubmitting] = useState(false);
|
|
951
|
+
const [error, setError] = useState('');
|
|
952
|
+
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
if (!error) return;
|
|
955
|
+
const t = setTimeout(() => setError(''), 3000);
|
|
956
|
+
return () => clearTimeout(t);
|
|
957
|
+
}, [error]);
|
|
958
|
+
|
|
959
|
+
async function handleSubmit() {
|
|
960
|
+
const trimmed = code.trim();
|
|
961
|
+
if (!trimmed || submitting) return;
|
|
962
|
+
setSubmitting(true);
|
|
963
|
+
setError('');
|
|
964
|
+
try {
|
|
965
|
+
await activateBeta(trimmed);
|
|
966
|
+
setCode('');
|
|
967
|
+
} catch (err) {
|
|
968
|
+
setError(err.message || 'Invalid code');
|
|
969
|
+
} finally {
|
|
970
|
+
setSubmitting(false);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function handleDeactivate() {
|
|
975
|
+
try { await deactivateBeta(); } catch { /* toast handled in store */ }
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return (
|
|
979
|
+
<div>
|
|
980
|
+
<div className="flex items-center gap-2 mb-2.5 px-0.5">
|
|
981
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Early Access</span>
|
|
982
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
983
|
+
</div>
|
|
984
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5 max-w-md">
|
|
985
|
+
{networkUnlocked ? (
|
|
986
|
+
<div className="flex items-center gap-2.5">
|
|
987
|
+
<div className="w-6 h-6 rounded-full bg-success/10 flex items-center justify-center flex-shrink-0">
|
|
988
|
+
<Check size={12} className="text-success" />
|
|
989
|
+
</div>
|
|
990
|
+
<div className="flex-1 text-xs font-sans text-text-1">Early access enabled</div>
|
|
991
|
+
<button
|
|
992
|
+
onClick={handleDeactivate}
|
|
993
|
+
className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans"
|
|
994
|
+
>
|
|
995
|
+
Deactivate
|
|
996
|
+
</button>
|
|
997
|
+
</div>
|
|
998
|
+
) : (
|
|
999
|
+
<div className="flex items-center gap-2">
|
|
1000
|
+
<Lock size={12} className="text-text-4 flex-shrink-0" />
|
|
1001
|
+
<input
|
|
1002
|
+
value={code}
|
|
1003
|
+
onChange={(e) => setCode(e.target.value)}
|
|
1004
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
|
|
1005
|
+
type="text"
|
|
1006
|
+
placeholder="Enter invite code"
|
|
1007
|
+
className="flex-1 h-8 px-2.5 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
1008
|
+
/>
|
|
1009
|
+
<Button
|
|
1010
|
+
variant="primary"
|
|
1011
|
+
size="sm"
|
|
1012
|
+
onClick={handleSubmit}
|
|
1013
|
+
disabled={!code.trim() || submitting}
|
|
1014
|
+
className="h-8 text-xs px-3"
|
|
1015
|
+
>
|
|
1016
|
+
{submitting ? '...' : 'Submit'}
|
|
1017
|
+
</Button>
|
|
1018
|
+
</div>
|
|
1019
|
+
)}
|
|
1020
|
+
{error && !networkUnlocked && (
|
|
1021
|
+
<div className="mt-2 text-2xs text-danger font-sans">{error}</div>
|
|
1022
|
+
)}
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
943
1028
|
/* ── Main Settings View ────────────────────────────────────── */
|
|
944
1029
|
|
|
945
1030
|
export default function SettingsView() {
|
|
@@ -1200,6 +1285,8 @@ export default function SettingsView() {
|
|
|
1200
1285
|
</div>
|
|
1201
1286
|
)}
|
|
1202
1287
|
|
|
1288
|
+
{/* ═══════ EARLY ACCESS ═══════ */}
|
|
1289
|
+
<EarlyAccessSection />
|
|
1203
1290
|
|
|
1204
1291
|
</div>
|
|
1205
1292
|
</ScrollArea>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.45",
|
|
4
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)",
|