groove-dev 0.19.9 → 0.21.0
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 +33 -0
- package/CLAUDE.md +1 -1
- package/node_modules/@groove-dev/cli/bin/groove.js +1 -1
- package/node_modules/@groove-dev/daemon/package.json +4 -0
- package/node_modules/@groove-dev/daemon/src/api.js +86 -0
- package/node_modules/@groove-dev/daemon/src/gateways/base.js +87 -0
- package/node_modules/@groove-dev/daemon/src/gateways/discord.js +220 -0
- package/node_modules/@groove-dev/daemon/src/gateways/formatter.js +201 -0
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +695 -0
- package/node_modules/@groove-dev/daemon/src/gateways/slack.js +165 -0
- package/node_modules/@groove-dev/daemon/src/gateways/telegram.js +265 -0
- package/node_modules/@groove-dev/daemon/src/index.js +4 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +55 -0
- package/node_modules/@groove-dev/gui/.groove/audit.log +2 -0
- package/node_modules/@groove-dev/gui/.groove/codebase-index.json +1 -1
- package/node_modules/@groove-dev/gui/.groove/config.json +2 -2
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -1
- package/node_modules/@groove-dev/gui/.groove/timeline.json +3000 -0
- package/node_modules/@groove-dev/gui/AGENTS_REGISTRY.md +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CNqM3_F2.js +552 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-ChDhUvQR.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +7 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +382 -25
- package/package.json +1 -1
- package/packages/cli/bin/groove.js +1 -1
- package/packages/daemon/package.json +4 -0
- package/packages/daemon/src/api.js +86 -0
- package/packages/daemon/src/gateways/base.js +87 -0
- package/packages/daemon/src/gateways/discord.js +220 -0
- package/packages/daemon/src/gateways/formatter.js +201 -0
- package/packages/daemon/src/gateways/manager.js +695 -0
- package/packages/daemon/src/gateways/slack.js +165 -0
- package/packages/daemon/src/gateways/telegram.js +265 -0
- package/packages/daemon/src/index.js +4 -0
- package/packages/daemon/src/validate.js +55 -0
- package/packages/gui/dist/assets/index-CNqM3_F2.js +552 -0
- package/packages/gui/dist/assets/index-ChDhUvQR.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/stores/groove.js +7 -0
- package/packages/gui/src/views/settings.jsx +382 -25
- package/node_modules/@groove-dev/gui/dist/assets/index-CdbNHOqF.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-Db0ZssmH.js +0 -537
- package/packages/gui/dist/assets/index-CdbNHOqF.css +0 -1
- package/packages/gui/dist/assets/index-Db0ZssmH.js +0 -537
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
Key, Eye, EyeOff, Check, Cpu, ChevronDown,
|
|
16
16
|
FolderOpen, FolderSearch, RotateCw, Users, Gauge, Zap,
|
|
17
17
|
LogIn, LogOut, User, ShieldCheck, Settings,
|
|
18
|
-
Newspaper, Layers,
|
|
18
|
+
Newspaper, Layers, Radio, Send, MessageSquare, MessageCircle,
|
|
19
|
+
Plus, Trash2, Plug, PlugZap, TestTube, X,
|
|
19
20
|
} from 'lucide-react';
|
|
20
21
|
|
|
21
22
|
/* ── Toggle ────────────────────────────────────────────────── */
|
|
@@ -128,10 +129,10 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
128
129
|
</div>
|
|
129
130
|
|
|
130
131
|
{/* Body */}
|
|
131
|
-
<div className="flex-1 flex flex-col px-4 py-3">
|
|
132
|
+
<div className="flex-1 flex flex-col px-4 py-3 min-h-[120px]">
|
|
132
133
|
{/* Models */}
|
|
133
134
|
{provider.models?.length > 0 && (
|
|
134
|
-
<div className="flex flex-wrap gap-1 mb-
|
|
135
|
+
<div className="flex flex-wrap gap-1 mb-3">
|
|
135
136
|
{provider.models.map((m) => (
|
|
136
137
|
<span key={m.id} className="px-1.5 py-0.5 rounded bg-surface-4 text-2xs font-mono text-text-3">
|
|
137
138
|
{m.name || m.id}
|
|
@@ -141,16 +142,16 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
141
142
|
)}
|
|
142
143
|
|
|
143
144
|
{/* Subscription info for Claude */}
|
|
144
|
-
{isSubscription && isReady && !provider.hasKey && (
|
|
145
|
-
<div className="flex items-center gap-1.5 h-
|
|
145
|
+
{isSubscription && isReady && !provider.hasKey && !settingKey && (
|
|
146
|
+
<div className="flex items-center gap-1.5 h-8 px-2.5 bg-accent/8 border border-accent/20 rounded-md text-2xs font-sans text-accent mb-3">
|
|
146
147
|
<Check size={10} /> Subscription active
|
|
147
148
|
</div>
|
|
148
149
|
)}
|
|
149
150
|
|
|
150
151
|
{/* Connected state */}
|
|
151
152
|
{provider.hasKey && !settingKey && (
|
|
152
|
-
<div className="flex items-center gap-
|
|
153
|
-
<div className="flex-1 flex items-center gap-1.5 h-
|
|
153
|
+
<div className="flex items-center gap-2 mb-3">
|
|
154
|
+
<div className="flex-1 flex items-center gap-1.5 h-8 px-2.5 bg-success/8 border border-success/20 rounded-md text-2xs font-sans text-success">
|
|
154
155
|
<Check size={10} /> API Connected
|
|
155
156
|
</div>
|
|
156
157
|
<button onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans">Edit</button>
|
|
@@ -158,44 +159,51 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
158
159
|
</div>
|
|
159
160
|
)}
|
|
160
161
|
|
|
161
|
-
{/*
|
|
162
|
+
{/* Spacer */}
|
|
163
|
+
<div className="flex-1" />
|
|
164
|
+
|
|
165
|
+
{/* Key input form — takes over the bottom area */}
|
|
162
166
|
{settingKey && (
|
|
163
|
-
<div className="space-y-
|
|
164
|
-
<div
|
|
165
|
-
<
|
|
167
|
+
<div className="space-y-2.5 pt-1">
|
|
168
|
+
<div>
|
|
169
|
+
<label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">
|
|
170
|
+
{provider.hasKey ? 'Update API Key' : `${provider.name} API Key`}
|
|
171
|
+
</label>
|
|
172
|
+
<div className="relative">
|
|
166
173
|
<input
|
|
167
174
|
value={keyInput}
|
|
168
175
|
onChange={(e) => setKeyInput(e.target.value)}
|
|
169
176
|
onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
|
|
170
177
|
type={showKey ? 'text' : 'password'}
|
|
171
|
-
placeholder="
|
|
172
|
-
className="w-full h-
|
|
178
|
+
placeholder="sk-..."
|
|
179
|
+
className="w-full h-9 px-3 pr-9 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"
|
|
173
180
|
autoFocus
|
|
174
181
|
/>
|
|
175
|
-
<button onClick={() => setShowKey(!showKey)} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
176
|
-
{showKey ? <EyeOff size={
|
|
182
|
+
<button onClick={() => setShowKey(!showKey)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
183
|
+
{showKey ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
177
184
|
</button>
|
|
178
185
|
</div>
|
|
179
186
|
</div>
|
|
180
|
-
<div className="flex gap-
|
|
181
|
-
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="flex-1 h-
|
|
182
|
-
|
|
187
|
+
<div className="flex gap-2">
|
|
188
|
+
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="flex-1 h-8 text-xs">
|
|
189
|
+
Save Key
|
|
190
|
+
</Button>
|
|
191
|
+
<Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-8 text-xs px-3">
|
|
192
|
+
Cancel
|
|
193
|
+
</Button>
|
|
183
194
|
</div>
|
|
184
195
|
</div>
|
|
185
196
|
)}
|
|
186
197
|
|
|
187
|
-
{/* Spacer to push button to bottom */}
|
|
188
|
-
<div className="flex-1" />
|
|
189
|
-
|
|
190
198
|
{/* Bottom action — always at card bottom */}
|
|
191
199
|
{!settingKey && !provider.hasKey && (
|
|
192
200
|
<Button
|
|
193
201
|
variant={isSubscription ? 'secondary' : 'primary'}
|
|
194
202
|
size="sm"
|
|
195
203
|
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
196
|
-
className="w-full h-
|
|
204
|
+
className="w-full h-8 text-2xs gap-1.5 mt-2"
|
|
197
205
|
>
|
|
198
|
-
<Key size={
|
|
206
|
+
<Key size={11} />
|
|
199
207
|
{isSubscription ? 'Add API key for headless mode' : 'Add API Key'}
|
|
200
208
|
</Button>
|
|
201
209
|
)}
|
|
@@ -221,12 +229,330 @@ function ConfigCard({ icon: Icon, label, description, children }) {
|
|
|
221
229
|
);
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
/* ── Gateway Icons ─────────────────────────────────────────── */
|
|
233
|
+
|
|
234
|
+
const GATEWAY_ICONS = { telegram: Send, discord: MessageSquare, slack: MessageCircle };
|
|
235
|
+
const GATEWAY_LABELS = { telegram: 'Telegram', discord: 'Discord', slack: 'Slack' };
|
|
236
|
+
const GATEWAY_PLACEHOLDERS = { telegram: 'Bot token from @BotFather', discord: 'Bot token from Developer Portal', slack: 'Bot token (xoxb-...)' };
|
|
237
|
+
const NOTIFICATION_PRESETS = ['critical', 'lifecycle', 'all'];
|
|
238
|
+
|
|
239
|
+
/* ── Gateway Card ─────────────────────────────────────────── */
|
|
240
|
+
|
|
241
|
+
function GatewayCard({ gateway, onRefresh }) {
|
|
242
|
+
const [settingToken, setSettingToken] = useState(false);
|
|
243
|
+
const [tokenInput, setTokenInput] = useState('');
|
|
244
|
+
const [appTokenInput, setAppTokenInput] = useState('');
|
|
245
|
+
const [showToken, setShowToken] = useState(false);
|
|
246
|
+
const [testing, setTesting] = useState(false);
|
|
247
|
+
const [connecting, setConnecting] = useState(false);
|
|
248
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
249
|
+
|
|
250
|
+
const Icon = GATEWAY_ICONS[gateway.type] || Radio;
|
|
251
|
+
const isSlack = gateway.type === 'slack';
|
|
252
|
+
|
|
253
|
+
async function handleSaveToken() {
|
|
254
|
+
if (!tokenInput.trim()) return;
|
|
255
|
+
try {
|
|
256
|
+
await api.post(`/gateways/${gateway.id}/credentials`, { key: 'bot_token', value: tokenInput.trim() });
|
|
257
|
+
if (isSlack && appTokenInput.trim()) {
|
|
258
|
+
await api.post(`/gateways/${gateway.id}/credentials`, { key: 'app_token', value: appTokenInput.trim() });
|
|
259
|
+
}
|
|
260
|
+
addToast('success', `Token saved for ${GATEWAY_LABELS[gateway.type]}`);
|
|
261
|
+
setTokenInput('');
|
|
262
|
+
setAppTokenInput('');
|
|
263
|
+
setSettingToken(false);
|
|
264
|
+
onRefresh();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
addToast('error', 'Failed to save token', err.message);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function handleTest() {
|
|
271
|
+
setTesting(true);
|
|
272
|
+
try {
|
|
273
|
+
await api.post(`/gateways/${gateway.id}/test`);
|
|
274
|
+
addToast('success', 'Test message sent!');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
addToast('error', 'Test failed', err.message);
|
|
277
|
+
}
|
|
278
|
+
setTesting(false);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function handleToggleConnect() {
|
|
282
|
+
setConnecting(true);
|
|
283
|
+
try {
|
|
284
|
+
if (gateway.connected) {
|
|
285
|
+
await api.post(`/gateways/${gateway.id}/disconnect`);
|
|
286
|
+
addToast('info', `${GATEWAY_LABELS[gateway.type]} disconnected`);
|
|
287
|
+
} else {
|
|
288
|
+
await api.post(`/gateways/${gateway.id}/connect`);
|
|
289
|
+
addToast('success', `${GATEWAY_LABELS[gateway.type]} connected!`);
|
|
290
|
+
}
|
|
291
|
+
onRefresh();
|
|
292
|
+
} catch (err) {
|
|
293
|
+
addToast('error', gateway.connected ? 'Disconnect failed' : 'Connect failed', err.message);
|
|
294
|
+
}
|
|
295
|
+
setConnecting(false);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function handleToggleEnabled(enabled) {
|
|
299
|
+
try {
|
|
300
|
+
await api.patch(`/gateways/${gateway.id}`, { enabled });
|
|
301
|
+
onRefresh();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
addToast('error', 'Update failed', err.message);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function handlePresetChange(preset) {
|
|
308
|
+
try {
|
|
309
|
+
await api.patch(`/gateways/${gateway.id}`, { notifications: { preset } });
|
|
310
|
+
onRefresh();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
addToast('error', 'Update failed', err.message);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function handlePermissionChange(perm) {
|
|
317
|
+
try {
|
|
318
|
+
await api.patch(`/gateways/${gateway.id}`, { commandPermission: perm });
|
|
319
|
+
onRefresh();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
addToast('error', 'Update failed', err.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function handleDelete() {
|
|
326
|
+
try {
|
|
327
|
+
await api.delete(`/gateways/${gateway.id}`);
|
|
328
|
+
addToast('info', `${GATEWAY_LABELS[gateway.type]} gateway removed`);
|
|
329
|
+
onRefresh();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
addToast('error', 'Delete failed', err.message);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const currentPreset = gateway.notifications?.preset || 'critical';
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div className="flex flex-col rounded-lg border border-border-subtle bg-surface-1 overflow-hidden min-w-[220px]">
|
|
339
|
+
{/* Header */}
|
|
340
|
+
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-border-subtle">
|
|
341
|
+
<StatusDot status={gateway.connected ? 'running' : 'crashed'} size="sm" />
|
|
342
|
+
<Icon size={13} className="text-text-2" />
|
|
343
|
+
<span className="text-[13px] font-semibold text-text-0 font-sans">{GATEWAY_LABELS[gateway.type]}</span>
|
|
344
|
+
<div className="flex-1" />
|
|
345
|
+
{gateway.connected ? (
|
|
346
|
+
<Badge variant="success" className="text-2xs gap-1"><PlugZap size={8} /> Connected</Badge>
|
|
347
|
+
) : gateway.enabled ? (
|
|
348
|
+
<Badge variant="warning" className="text-2xs">Disconnected</Badge>
|
|
349
|
+
) : (
|
|
350
|
+
<Badge variant="default" className="text-2xs">Disabled</Badge>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{/* Body */}
|
|
355
|
+
<div className="flex-1 flex flex-col px-4 py-3 min-h-[140px]">
|
|
356
|
+
|
|
357
|
+
{/* Connected state */}
|
|
358
|
+
{gateway.connected && !settingToken && (
|
|
359
|
+
<>
|
|
360
|
+
<div className="flex items-center gap-1.5 h-8 px-2.5 bg-success/8 border border-success/20 rounded-md text-2xs font-sans text-success mb-3">
|
|
361
|
+
<Check size={10} /> Gateway active
|
|
362
|
+
{gateway.botUsername && <span className="text-text-4 ml-1">@{gateway.botUsername}</span>}
|
|
363
|
+
{gateway.botTag && <span className="text-text-4 ml-1">{gateway.botTag}</span>}
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Notification preset */}
|
|
367
|
+
<div className="mb-3">
|
|
368
|
+
<label className="text-2xs font-semibold text-text-3 font-sans mb-1.5 block">Notifications</label>
|
|
369
|
+
<div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
|
|
370
|
+
{NOTIFICATION_PRESETS.map((p) => (
|
|
371
|
+
<button
|
|
372
|
+
key={p}
|
|
373
|
+
onClick={() => handlePresetChange(p)}
|
|
374
|
+
className={cn(
|
|
375
|
+
'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer capitalize',
|
|
376
|
+
currentPreset === p ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
{p}
|
|
380
|
+
</button>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Command permissions */}
|
|
386
|
+
<div className="mb-3">
|
|
387
|
+
<label className="text-2xs font-semibold text-text-3 font-sans mb-1.5 block">Commands</label>
|
|
388
|
+
<div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
|
|
389
|
+
{['full', 'read-only'].map((p) => (
|
|
390
|
+
<button
|
|
391
|
+
key={p}
|
|
392
|
+
onClick={() => handlePermissionChange(p)}
|
|
393
|
+
className={cn(
|
|
394
|
+
'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer capitalize',
|
|
395
|
+
(gateway.commandPermission || 'full') === p ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
|
|
396
|
+
)}
|
|
397
|
+
>
|
|
398
|
+
{p === 'full' ? 'Full Access' : 'Read Only'}
|
|
399
|
+
</button>
|
|
400
|
+
))}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{/* Not connected, not editing — show action */}
|
|
407
|
+
{!gateway.connected && !settingToken && (
|
|
408
|
+
<div className="text-xs text-text-3 font-sans mb-3">
|
|
409
|
+
{gateway.enabled ? 'Configure bot token to connect.' : 'Gateway is disabled.'}
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
|
|
413
|
+
<div className="flex-1" />
|
|
414
|
+
|
|
415
|
+
{/* Token input form */}
|
|
416
|
+
{settingToken && (
|
|
417
|
+
<div className="space-y-2.5 pt-1">
|
|
418
|
+
<div>
|
|
419
|
+
<label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Bot Token</label>
|
|
420
|
+
<div className="relative">
|
|
421
|
+
<input
|
|
422
|
+
value={tokenInput}
|
|
423
|
+
onChange={(e) => setTokenInput(e.target.value)}
|
|
424
|
+
onKeyDown={(e) => e.key === 'Enter' && !isSlack && handleSaveToken()}
|
|
425
|
+
type={showToken ? 'text' : 'password'}
|
|
426
|
+
placeholder={GATEWAY_PLACEHOLDERS[gateway.type]}
|
|
427
|
+
className="w-full h-9 px-3 pr-9 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"
|
|
428
|
+
autoFocus
|
|
429
|
+
/>
|
|
430
|
+
<button onClick={() => setShowToken(!showToken)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
431
|
+
{showToken ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
{isSlack && (
|
|
436
|
+
<div>
|
|
437
|
+
<label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">App Token (Socket Mode)</label>
|
|
438
|
+
<input
|
|
439
|
+
value={appTokenInput}
|
|
440
|
+
onChange={(e) => setAppTokenInput(e.target.value)}
|
|
441
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSaveToken()}
|
|
442
|
+
type={showToken ? 'text' : 'password'}
|
|
443
|
+
placeholder="xapp-..."
|
|
444
|
+
className="w-full h-9 px-3 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"
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
<div className="flex gap-2">
|
|
449
|
+
<Button variant="primary" size="sm" onClick={handleSaveToken} disabled={!tokenInput.trim()} className="flex-1 h-8 text-xs">
|
|
450
|
+
Save Token
|
|
451
|
+
</Button>
|
|
452
|
+
<Button variant="ghost" size="sm" onClick={() => { setSettingToken(false); setTokenInput(''); setAppTokenInput(''); }} className="h-8 text-xs px-3">
|
|
453
|
+
Cancel
|
|
454
|
+
</Button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{/* Bottom actions */}
|
|
460
|
+
{!settingToken && (
|
|
461
|
+
<div className="flex gap-2 mt-2">
|
|
462
|
+
{!gateway.connected && (
|
|
463
|
+
<Button variant="primary" size="sm" onClick={() => setSettingToken(true)} className="flex-1 h-7 text-2xs gap-1.5">
|
|
464
|
+
<Key size={11} />
|
|
465
|
+
{gateway.enabled ? 'Set Token' : 'Configure'}
|
|
466
|
+
</Button>
|
|
467
|
+
)}
|
|
468
|
+
{gateway.connected && (
|
|
469
|
+
<>
|
|
470
|
+
<Button variant="secondary" size="sm" onClick={handleTest} disabled={testing} className="flex-1 h-7 text-2xs gap-1.5">
|
|
471
|
+
<TestTube size={11} />
|
|
472
|
+
{testing ? 'Sending...' : 'Test'}
|
|
473
|
+
</Button>
|
|
474
|
+
<Button variant="secondary" size="sm" onClick={() => setSettingToken(true)} className="h-7 text-2xs px-2.5">
|
|
475
|
+
<Key size={11} />
|
|
476
|
+
</Button>
|
|
477
|
+
</>
|
|
478
|
+
)}
|
|
479
|
+
<Button
|
|
480
|
+
variant="ghost"
|
|
481
|
+
size="sm"
|
|
482
|
+
onClick={handleToggleConnect}
|
|
483
|
+
disabled={connecting}
|
|
484
|
+
className="h-7 text-2xs px-2.5"
|
|
485
|
+
title={gateway.connected ? 'Disconnect' : 'Connect'}
|
|
486
|
+
>
|
|
487
|
+
{gateway.connected ? <Plug size={11} /> : <PlugZap size={11} />}
|
|
488
|
+
</Button>
|
|
489
|
+
<Toggle value={gateway.enabled} onChange={handleToggleEnabled} />
|
|
490
|
+
<button onClick={handleDelete} className="text-text-4 hover:text-danger cursor-pointer p-1" title="Remove gateway">
|
|
491
|
+
<Trash2 size={11} />
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* ── Add Gateway Button ───────────────────────────────────── */
|
|
501
|
+
|
|
502
|
+
function AddGatewayCard({ existingTypes, onAdd }) {
|
|
503
|
+
const [open, setOpen] = useState(false);
|
|
504
|
+
const available = ['telegram', 'discord', 'slack'].filter((t) => !existingTypes.includes(t));
|
|
505
|
+
|
|
506
|
+
if (available.length === 0) return null;
|
|
507
|
+
|
|
508
|
+
if (!open) {
|
|
509
|
+
return (
|
|
510
|
+
<button
|
|
511
|
+
onClick={() => setOpen(true)}
|
|
512
|
+
className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border-subtle bg-surface-1/50 hover:bg-surface-1 hover:border-accent/30 min-h-[140px] min-w-[220px] cursor-pointer transition-all group"
|
|
513
|
+
>
|
|
514
|
+
<div className="w-8 h-8 rounded-full bg-accent/8 group-hover:bg-accent/15 flex items-center justify-center mb-2 transition-colors">
|
|
515
|
+
<Plus size={14} className="text-accent" />
|
|
516
|
+
</div>
|
|
517
|
+
<span className="text-2xs font-semibold text-text-3 group-hover:text-text-1 font-sans transition-colors">Add Gateway</span>
|
|
518
|
+
</button>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<div className="flex flex-col rounded-lg border border-accent/30 bg-surface-1 overflow-hidden min-w-[220px]">
|
|
524
|
+
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-border-subtle">
|
|
525
|
+
<Radio size={13} className="text-accent" />
|
|
526
|
+
<span className="text-[13px] font-semibold text-text-0 font-sans">Add Gateway</span>
|
|
527
|
+
<div className="flex-1" />
|
|
528
|
+
<button onClick={() => setOpen(false)} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={12} /></button>
|
|
529
|
+
</div>
|
|
530
|
+
<div className="p-3 space-y-2">
|
|
531
|
+
{available.map((type) => {
|
|
532
|
+
const Icon = GATEWAY_ICONS[type];
|
|
533
|
+
return (
|
|
534
|
+
<button
|
|
535
|
+
key={type}
|
|
536
|
+
onClick={() => { onAdd(type); setOpen(false); }}
|
|
537
|
+
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md bg-surface-0 hover:bg-accent/8 border border-border-subtle hover:border-accent/20 cursor-pointer transition-all group"
|
|
538
|
+
>
|
|
539
|
+
<Icon size={14} className="text-text-3 group-hover:text-accent" />
|
|
540
|
+
<span className="text-xs font-medium text-text-1 group-hover:text-accent font-sans">{GATEWAY_LABELS[type]}</span>
|
|
541
|
+
</button>
|
|
542
|
+
);
|
|
543
|
+
})}
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
224
549
|
/* ── Main Settings View ────────────────────────────────────── */
|
|
225
550
|
|
|
226
551
|
export default function SettingsView() {
|
|
227
552
|
const [providers, setProviders] = useState([]);
|
|
228
553
|
const [config, setConfig] = useState(null);
|
|
229
554
|
const [daemonInfo, setDaemonInfo] = useState(null);
|
|
555
|
+
const [gwList, setGwList] = useState([]);
|
|
230
556
|
const [loading, setLoading] = useState(true);
|
|
231
557
|
const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
|
|
232
558
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
@@ -239,12 +565,26 @@ export default function SettingsView() {
|
|
|
239
565
|
api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
|
|
240
566
|
}
|
|
241
567
|
|
|
568
|
+
function loadGateways() {
|
|
569
|
+
api.get('/gateways').then((d) => setGwList(Array.isArray(d) ? d : [])).catch(() => {});
|
|
570
|
+
}
|
|
571
|
+
|
|
242
572
|
useEffect(() => {
|
|
243
|
-
Promise.all([api.get('/providers'), api.get('/config'), api.get('/status')])
|
|
244
|
-
.then(([p, c, s]) => { setProviders(Array.isArray(p) ? p : []); setConfig(c); setDaemonInfo(s); setLoading(false); })
|
|
573
|
+
Promise.all([api.get('/providers'), api.get('/config'), api.get('/status'), api.get('/gateways')])
|
|
574
|
+
.then(([p, c, s, g]) => { setProviders(Array.isArray(p) ? p : []); setConfig(c); setDaemonInfo(s); setGwList(Array.isArray(g) ? g : []); setLoading(false); })
|
|
245
575
|
.catch(() => setLoading(false));
|
|
246
576
|
}, []);
|
|
247
577
|
|
|
578
|
+
async function addGateway(type) {
|
|
579
|
+
try {
|
|
580
|
+
await api.post('/gateways', { type });
|
|
581
|
+
addToast('success', `${GATEWAY_LABELS[type]} gateway added`);
|
|
582
|
+
loadGateways();
|
|
583
|
+
} catch (err) {
|
|
584
|
+
addToast('error', 'Failed to add gateway', err.message);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
248
588
|
async function updateConfig(key, value) {
|
|
249
589
|
try {
|
|
250
590
|
const updated = await api.patch('/config', { [key]: value });
|
|
@@ -333,6 +673,23 @@ export default function SettingsView() {
|
|
|
333
673
|
</div>
|
|
334
674
|
</div>
|
|
335
675
|
|
|
676
|
+
{/* ═══════ GATEWAYS ═══════ */}
|
|
677
|
+
<div>
|
|
678
|
+
<div className="flex items-center gap-2 mb-2.5 px-0.5">
|
|
679
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Gateways</span>
|
|
680
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
681
|
+
<span className="text-2xs text-text-4 font-sans">
|
|
682
|
+
{gwList.filter((g) => g.connected).length}/{gwList.length} connected
|
|
683
|
+
</span>
|
|
684
|
+
</div>
|
|
685
|
+
<div className="grid grid-cols-4 gap-3">
|
|
686
|
+
{gwList.map((gw) => (
|
|
687
|
+
<GatewayCard key={gw.id} gateway={gw} onRefresh={loadGateways} />
|
|
688
|
+
))}
|
|
689
|
+
<AddGatewayCard existingTypes={gwList.map((g) => g.type)} onAdd={addGateway} />
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
336
693
|
{/* ═══════ CONFIGURATION ═══════ */}
|
|
337
694
|
{config && (
|
|
338
695
|
<div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
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)",
|
|
@@ -838,6 +838,92 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
838
838
|
res.json({ id: agent.id, integrations });
|
|
839
839
|
});
|
|
840
840
|
|
|
841
|
+
// --- Gateways (Telegram, Discord, Slack) ---
|
|
842
|
+
|
|
843
|
+
app.get('/api/gateways', (req, res) => {
|
|
844
|
+
res.json(daemon.gateways.list());
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
app.post('/api/gateways', async (req, res) => {
|
|
848
|
+
try {
|
|
849
|
+
const result = await daemon.gateways.create(req.body || {});
|
|
850
|
+
res.json(result);
|
|
851
|
+
} catch (err) {
|
|
852
|
+
res.status(400).json({ error: err.message });
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
app.get('/api/gateways/:id', (req, res) => {
|
|
857
|
+
const gw = daemon.gateways.get(req.params.id);
|
|
858
|
+
if (!gw) return res.status(404).json({ error: 'Gateway not found' });
|
|
859
|
+
res.json(gw);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
app.patch('/api/gateways/:id', async (req, res) => {
|
|
863
|
+
try {
|
|
864
|
+
const result = await daemon.gateways.update(req.params.id, req.body || {});
|
|
865
|
+
res.json(result);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
res.status(400).json({ error: err.message });
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
app.delete('/api/gateways/:id', async (req, res) => {
|
|
872
|
+
try {
|
|
873
|
+
await daemon.gateways.delete(req.params.id);
|
|
874
|
+
res.json({ ok: true });
|
|
875
|
+
} catch (err) {
|
|
876
|
+
res.status(400).json({ error: err.message });
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
app.post('/api/gateways/:id/test', async (req, res) => {
|
|
881
|
+
try {
|
|
882
|
+
const result = await daemon.gateways.test(req.params.id);
|
|
883
|
+
res.json(result);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
res.status(400).json({ error: err.message });
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
app.post('/api/gateways/:id/connect', async (req, res) => {
|
|
890
|
+
try {
|
|
891
|
+
const result = await daemon.gateways.connect(req.params.id);
|
|
892
|
+
res.json(result);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
res.status(400).json({ error: err.message });
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
app.post('/api/gateways/:id/disconnect', async (req, res) => {
|
|
899
|
+
try {
|
|
900
|
+
const result = await daemon.gateways.disconnect(req.params.id);
|
|
901
|
+
res.json(result);
|
|
902
|
+
} catch (err) {
|
|
903
|
+
res.status(400).json({ error: err.message });
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
app.post('/api/gateways/:id/credentials', (req, res) => {
|
|
908
|
+
try {
|
|
909
|
+
const { key, value } = req.body || {};
|
|
910
|
+
if (!key || !value) return res.status(400).json({ error: 'key and value are required' });
|
|
911
|
+
daemon.gateways.setCredential(req.params.id, key, value);
|
|
912
|
+
res.json({ ok: true });
|
|
913
|
+
} catch (err) {
|
|
914
|
+
res.status(400).json({ error: err.message });
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
app.delete('/api/gateways/:id/credentials/:key', (req, res) => {
|
|
919
|
+
try {
|
|
920
|
+
daemon.gateways.deleteCredential(req.params.id, req.params.key);
|
|
921
|
+
res.json({ ok: true });
|
|
922
|
+
} catch (err) {
|
|
923
|
+
res.status(400).json({ error: err.message });
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
841
927
|
// --- Schedules ---
|
|
842
928
|
|
|
843
929
|
app.get('/api/schedules', (req, res) => {
|