groove-dev 0.27.26 → 0.27.28
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/.groove-staging/state.json +3 -0
- package/.groove-staging/timeline.json +13 -0
- package/CLAUDE.md +0 -10
- package/DECENTRALIZED_NET_WP_V1.md +871 -0
- package/README.md +28 -0
- package/SECURITY_SWEEP.md +228 -0
- package/decentralized-net/ACTION_PLAN.md +422 -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 +99 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +7 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +36 -6
- package/node_modules/@groove-dev/daemon/src/memory.js +29 -10
- package/node_modules/@groove-dev/daemon/src/process.js +29 -12
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +26 -1
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -11
- package/node_modules/@groove-dev/daemon/src/rotator.js +24 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +63 -0
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +106 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +49 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +99 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DieCV-v1.js → index-Ch1N9G4Z.js} +1728 -1728
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +147 -21
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +206 -44
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +11 -24
- package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +1 -36
- package/node_modules/@groove-dev/gui/src/lib/integration-logos.js +39 -0
- 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 +99 -0
- package/packages/daemon/src/introducer.js +7 -7
- package/packages/daemon/src/journalist.js +36 -6
- package/packages/daemon/src/memory.js +29 -10
- package/packages/daemon/src/process.js +29 -12
- package/packages/daemon/src/providers/claude-code.js +26 -1
- package/packages/daemon/src/providers/codex.js +34 -11
- package/packages/daemon/src/rotator.js +24 -1
- package/packages/gui/dist/assets/{index-DieCV-v1.js → index-Ch1N9G4Z.js} +1728 -1728
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-config.jsx +147 -21
- package/packages/gui/src/components/agents/spawn-wizard.jsx +206 -44
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +11 -24
- package/packages/gui/src/components/marketplace/marketplace-card.jsx +1 -36
- package/packages/gui/src/lib/integration-logos.js +39 -0
- package/MUST_FIX_ISSUES.md +0 -305
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Ch1N9G4Z.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
AlertCircle, Layers, Activity,
|
|
7
7
|
RotateCw, Skull, Copy, Trash2,
|
|
8
8
|
Sparkles, Calendar, Plug, MessageCircle, Save, GitBranch,
|
|
9
|
+
ExternalLink, Loader2,
|
|
9
10
|
} from 'lucide-react';
|
|
10
11
|
import { useGrooveStore } from '../../stores/groove';
|
|
11
12
|
import { Badge } from '../ui/badge';
|
|
@@ -15,6 +16,7 @@ import { api } from '../../lib/api';
|
|
|
15
16
|
import { cn } from '../../lib/cn';
|
|
16
17
|
import { timeAgo } from '../../lib/format';
|
|
17
18
|
import { OllamaSetup } from './ollama-setup';
|
|
19
|
+
import { INTEGRATION_LOGOS } from '../../lib/integration-logos';
|
|
18
20
|
|
|
19
21
|
/* ── Segmented Control ─────────────────────────────────────── */
|
|
20
22
|
|
|
@@ -164,13 +166,21 @@ export function AgentConfig({ agent }) {
|
|
|
164
166
|
const [personalityLoaded, setPersonalityLoaded] = useState(false);
|
|
165
167
|
const [personalities, setPersonalities] = useState([]);
|
|
166
168
|
const [savingPersonality, setSavingPersonality] = useState(false);
|
|
169
|
+
const [installedIntegrations, setInstalledIntegrations] = useState([]);
|
|
170
|
+
const [claudeAuth, setClaudeAuth] = useState(null);
|
|
171
|
+
const [claudeAuthLoading, setClaudeAuthLoading] = useState(false);
|
|
172
|
+
const [claudeAuthPolling, setClaudeAuthPolling] = useState(false);
|
|
167
173
|
|
|
168
174
|
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
169
175
|
|
|
170
176
|
useEffect(() => {
|
|
171
177
|
loadProviders();
|
|
172
178
|
api.get('/skills/installed').then((data) => setInstalledSkills(Array.isArray(data) ? data : data.skills || [])).catch(() => {});
|
|
179
|
+
api.get('/integrations/installed').then((data) => setInstalledIntegrations(Array.isArray(data) ? data : [])).catch(() => {});
|
|
173
180
|
api.get('/repos/imported').then((data) => setImportedRepos((Array.isArray(data) ? data : []).filter((r) => r.status === 'active'))).catch(() => {});
|
|
181
|
+
if (agent.provider === 'claude-code') {
|
|
182
|
+
api.get('/providers/claude-code/auth').then((data) => setClaudeAuth(data)).catch(() => setClaudeAuth(null));
|
|
183
|
+
}
|
|
174
184
|
function onChanged() { loadProviders(); }
|
|
175
185
|
window.addEventListener('groove:providers-changed', onChanged);
|
|
176
186
|
return () => window.removeEventListener('groove:providers-changed', onChanged);
|
|
@@ -206,6 +216,23 @@ export function AgentConfig({ agent }) {
|
|
|
206
216
|
}).catch(() => {});
|
|
207
217
|
}, [agent.id, agent.name]);
|
|
208
218
|
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!claudeAuthPolling) return;
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
const interval = setInterval(() => {
|
|
223
|
+
if (Date.now() - start > 300000) { setClaudeAuthPolling(false); clearInterval(interval); return; }
|
|
224
|
+
api.get('/providers/claude-code/auth').then((data) => {
|
|
225
|
+
if (data?.authenticated) {
|
|
226
|
+
setClaudeAuth(data);
|
|
227
|
+
setClaudeAuthPolling(false);
|
|
228
|
+
setClaudeAuthLoading(false);
|
|
229
|
+
clearInterval(interval);
|
|
230
|
+
}
|
|
231
|
+
}).catch(() => {});
|
|
232
|
+
}, 2000);
|
|
233
|
+
return () => clearInterval(interval);
|
|
234
|
+
}, [claudeAuthPolling]);
|
|
235
|
+
|
|
209
236
|
const currentProvider = providers.find((p) => p.id === agent.provider);
|
|
210
237
|
|
|
211
238
|
async function handleModelSwap(providerId, modelId) {
|
|
@@ -276,6 +303,16 @@ export function AgentConfig({ agent }) {
|
|
|
276
303
|
}
|
|
277
304
|
}
|
|
278
305
|
|
|
306
|
+
async function handleClaudeLogin() {
|
|
307
|
+
setClaudeAuthLoading(true);
|
|
308
|
+
try {
|
|
309
|
+
await api.post('/providers/claude-code/login');
|
|
310
|
+
setClaudeAuthPolling(true);
|
|
311
|
+
} catch {
|
|
312
|
+
setClaudeAuthLoading(false);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
279
316
|
const spawned = agent.spawnedAt || agent.createdAt;
|
|
280
317
|
|
|
281
318
|
return (
|
|
@@ -430,6 +467,33 @@ export function AgentConfig({ agent }) {
|
|
|
430
467
|
)}
|
|
431
468
|
</ConfigSection>
|
|
432
469
|
|
|
470
|
+
{/* ── Claude Code Auth ──────────────────────────────── */}
|
|
471
|
+
{agent.provider === 'claude-code' && claudeAuth && !claudeAuth.authenticated && (
|
|
472
|
+
<div className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3 space-y-2">
|
|
473
|
+
<div className="flex items-center gap-2">
|
|
474
|
+
<AlertCircle size={13} className="text-warning flex-shrink-0" />
|
|
475
|
+
<span className="text-xs font-semibold text-text-0 font-sans">Claude Code is not signed in</span>
|
|
476
|
+
</div>
|
|
477
|
+
{claudeAuthLoading ? (
|
|
478
|
+
<div className="flex items-center gap-2 text-2xs text-text-2 font-sans">
|
|
479
|
+
<Loader2 size={12} className="animate-spin text-accent" />
|
|
480
|
+
Waiting for browser authentication...
|
|
481
|
+
</div>
|
|
482
|
+
) : (
|
|
483
|
+
<Button variant="primary" size="sm" onClick={handleClaudeLogin} className="text-2xs gap-1.5">
|
|
484
|
+
<ExternalLink size={10} />
|
|
485
|
+
Sign in to Claude
|
|
486
|
+
</Button>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
{agent.provider === 'claude-code' && claudeAuth?.authenticated && (
|
|
491
|
+
<div className="flex items-center gap-2 text-2xs text-text-2 font-sans">
|
|
492
|
+
<div className="w-2 h-2 rounded-full bg-success flex-shrink-0" />
|
|
493
|
+
Signed in as {claudeAuth.email || 'Claude user'} ({claudeAuth.subscriptionType || 'subscription'})
|
|
494
|
+
</div>
|
|
495
|
+
)}
|
|
496
|
+
|
|
433
497
|
{/* ── Working Directory ──────────────────────────────── */}
|
|
434
498
|
<ConfigSection label="Working Directory" icon={FolderOpen} description="The root directory this agent operates in.">
|
|
435
499
|
<div className="flex gap-2">
|
|
@@ -461,27 +525,6 @@ export function AgentConfig({ agent }) {
|
|
|
461
525
|
/>
|
|
462
526
|
</ConfigSection>
|
|
463
527
|
|
|
464
|
-
{/* ── Integration Approvals ────────────────────────────── */}
|
|
465
|
-
{agent.integrations?.length > 0 && (
|
|
466
|
-
<ConfigSection label="Integration Approvals" icon={Plug} description="Manual = you approve dangerous actions. Auto = agent acts freely.">
|
|
467
|
-
<SegmentedControl
|
|
468
|
-
options={[
|
|
469
|
-
{ value: 'manual', label: 'Manual' },
|
|
470
|
-
{ value: 'auto', label: 'Auto' },
|
|
471
|
-
]}
|
|
472
|
-
value={agent.integrationApproval || 'manual'}
|
|
473
|
-
onChange={async (val) => {
|
|
474
|
-
try {
|
|
475
|
-
await api.patch(`/agents/${agent.id}`, { integrationApproval: val });
|
|
476
|
-
addToast('success', `Integration approvals → ${val === 'auto' ? 'Auto' : 'Manual'}`);
|
|
477
|
-
} catch (err) {
|
|
478
|
-
addToast('error', 'Update failed', err.message);
|
|
479
|
-
}
|
|
480
|
-
}}
|
|
481
|
-
/>
|
|
482
|
-
</ConfigSection>
|
|
483
|
-
)}
|
|
484
|
-
|
|
485
528
|
{/* ── Model Routing ────────────────────────────────────── */}
|
|
486
529
|
<ConfigSection label="Model Routing" icon={Activity} description="How Groove selects models for this agent's tasks.">
|
|
487
530
|
<SegmentedControl
|
|
@@ -637,6 +680,89 @@ export function AgentConfig({ agent }) {
|
|
|
637
680
|
</div>
|
|
638
681
|
</ConfigSection>
|
|
639
682
|
|
|
683
|
+
{/* ── Integrations ─────────────────────────────────── */}
|
|
684
|
+
<ConfigSection label="Integrations" icon={Plug} description="Attach MCP integrations for external services.">
|
|
685
|
+
<div className="flex flex-wrap gap-1.5">
|
|
686
|
+
{(agent.integrations || []).map((integrationId) => {
|
|
687
|
+
const logoUrl = INTEGRATION_LOGOS[integrationId];
|
|
688
|
+
const integration = installedIntegrations.find((i) => i.id === integrationId);
|
|
689
|
+
return (
|
|
690
|
+
<Badge key={integrationId} variant="accent" className="font-mono text-xs gap-1.5 px-2.5 py-1">
|
|
691
|
+
{logoUrl ? (
|
|
692
|
+
<img src={logoUrl} alt="" className="w-2.5 h-2.5" />
|
|
693
|
+
) : (
|
|
694
|
+
<Plug size={9} />
|
|
695
|
+
)}
|
|
696
|
+
{integration?.name || integrationId}
|
|
697
|
+
<button
|
|
698
|
+
onClick={async () => {
|
|
699
|
+
try {
|
|
700
|
+
await api.delete(`/agents/${agent.id}/integrations/${integrationId}`);
|
|
701
|
+
addToast('success', `Detached ${integration?.name || integrationId}`);
|
|
702
|
+
} catch (err) { addToast('error', 'Detach failed', err.message); }
|
|
703
|
+
}}
|
|
704
|
+
className="hover:text-danger cursor-pointer"
|
|
705
|
+
>
|
|
706
|
+
<X size={10} />
|
|
707
|
+
</button>
|
|
708
|
+
</Badge>
|
|
709
|
+
);
|
|
710
|
+
})}
|
|
711
|
+
{installedIntegrations.filter((i) => i.configured !== false && !(agent.integrations || []).includes(i.id)).length > 0 && (
|
|
712
|
+
<div className="relative group">
|
|
713
|
+
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-surface-4 border border-border-subtle text-text-3 hover:text-accent cursor-pointer transition-colors">
|
|
714
|
+
<Plus size={12} />
|
|
715
|
+
</button>
|
|
716
|
+
<div className="absolute top-full left-0 mt-1 z-20 hidden group-hover:block bg-surface-2 border border-border-subtle rounded-lg shadow-xl py-1 min-w-[200px]">
|
|
717
|
+
{installedIntegrations.filter((i) => i.configured !== false && !(agent.integrations || []).includes(i.id)).map((integration) => {
|
|
718
|
+
const logoUrl = INTEGRATION_LOGOS[integration.id];
|
|
719
|
+
return (
|
|
720
|
+
<button
|
|
721
|
+
key={integration.id}
|
|
722
|
+
onClick={async () => {
|
|
723
|
+
try {
|
|
724
|
+
await api.post(`/agents/${agent.id}/integrations/${integration.id}`);
|
|
725
|
+
addToast('success', `Attached ${integration.name || integration.id}`);
|
|
726
|
+
} catch (err) { addToast('error', 'Attach failed', err.message); }
|
|
727
|
+
}}
|
|
728
|
+
className="w-full flex items-center gap-2 text-left px-3 py-1.5 text-xs font-sans text-text-1 hover:bg-surface-4 cursor-pointer transition-colors"
|
|
729
|
+
>
|
|
730
|
+
{logoUrl ? (
|
|
731
|
+
<img src={logoUrl} alt="" className="w-3.5 h-3.5 flex-shrink-0" />
|
|
732
|
+
) : (
|
|
733
|
+
<Plug size={12} className="text-text-3 flex-shrink-0" />
|
|
734
|
+
)}
|
|
735
|
+
{integration.name || integration.id}
|
|
736
|
+
</button>
|
|
737
|
+
);
|
|
738
|
+
})}
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
)}
|
|
742
|
+
{(agent.integrations || []).length === 0 && installedIntegrations.length === 0 && (
|
|
743
|
+
<span className="text-2xs text-text-4 font-sans">No integrations installed — browse the Marketplace</span>
|
|
744
|
+
)}
|
|
745
|
+
</div>
|
|
746
|
+
{(agent.integrations || []).length > 0 && (
|
|
747
|
+
<div className="mt-3">
|
|
748
|
+
<label className="text-2xs font-medium text-text-3 font-sans block mb-1.5">Integration Approvals</label>
|
|
749
|
+
<SegmentedControl
|
|
750
|
+
options={[
|
|
751
|
+
{ value: 'auto', label: 'Auto' },
|
|
752
|
+
{ value: 'manual', label: 'Manual' },
|
|
753
|
+
]}
|
|
754
|
+
value={agent.integrationApproval || 'manual'}
|
|
755
|
+
onChange={async (val) => {
|
|
756
|
+
try {
|
|
757
|
+
await api.patch(`/agents/${agent.id}`, { integrationApproval: val });
|
|
758
|
+
addToast('success', `Integration approvals → ${val === 'auto' ? 'Auto' : 'Manual'}`);
|
|
759
|
+
} catch (err) { addToast('error', 'Update failed', err.message); }
|
|
760
|
+
}}
|
|
761
|
+
/>
|
|
762
|
+
</div>
|
|
763
|
+
)}
|
|
764
|
+
</ConfigSection>
|
|
765
|
+
|
|
640
766
|
{/* ── Repos ─────────────────────────────────────────── */}
|
|
641
767
|
{importedRepos.length > 0 && (
|
|
642
768
|
<ConfigSection label="Repos" icon={GitBranch} description="Attach imported repos so this agent knows where they are.">
|
|
@@ -12,45 +12,11 @@ import {
|
|
|
12
12
|
Shield, Database, Megaphone, Calculator, UserCheck,
|
|
13
13
|
Headphones, BarChart3, Rocket, ChevronDown, Pen, Presentation,
|
|
14
14
|
Sparkles, X, Search, AlertTriangle, Plug, MessageCircle, GitBranch, Globe,
|
|
15
|
+
Check, ExternalLink, Loader2,
|
|
15
16
|
} from 'lucide-react';
|
|
16
17
|
import { api } from '../../lib/api';
|
|
17
18
|
import { Dialog, DialogContent } from '../ui/dialog';
|
|
18
|
-
|
|
19
|
-
const INTEGRATION_LOGOS = {
|
|
20
|
-
'google-workspace': 'https://cdn.simpleicons.org/google/white',
|
|
21
|
-
github: 'https://cdn.simpleicons.org/github/white',
|
|
22
|
-
stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
|
|
23
|
-
gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
|
|
24
|
-
'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
|
|
25
|
-
'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
|
|
26
|
-
'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
|
|
27
|
-
'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
|
|
28
|
-
'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
|
|
29
|
-
'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
|
|
30
|
-
postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
|
|
31
|
-
notion: 'https://cdn.simpleicons.org/notion/white',
|
|
32
|
-
linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
|
|
33
|
-
'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
|
|
34
|
-
'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
|
|
35
|
-
sentry: 'https://cdn.simpleicons.org/sentry/362D59',
|
|
36
|
-
elevenlabs: 'https://cdn.simpleicons.org/elevenlabs/white',
|
|
37
|
-
hubspot: 'https://cdn.simpleicons.org/hubspot/FF7A59',
|
|
38
|
-
jira: 'https://cdn.simpleicons.org/jira/0052CC',
|
|
39
|
-
sendgrid: 'https://cdn.simpleicons.org/sendgrid/1A82E2',
|
|
40
|
-
resend: 'https://cdn.simpleicons.org/resend/white',
|
|
41
|
-
replicate: 'https://cdn.simpleicons.org/replicate/white',
|
|
42
|
-
vercel: 'https://cdn.simpleicons.org/vercel/white',
|
|
43
|
-
supabase: 'https://cdn.simpleicons.org/supabase/3FCF8E',
|
|
44
|
-
mixpanel: 'https://cdn.simpleicons.org/mixpanel/7856FF',
|
|
45
|
-
datadog: 'https://cdn.simpleicons.org/datadog/632CA6',
|
|
46
|
-
airtable: 'https://cdn.simpleicons.org/airtable/18BFFF',
|
|
47
|
-
zendesk: 'https://cdn.simpleicons.org/zendesk/03363D',
|
|
48
|
-
intercom: 'https://cdn.simpleicons.org/intercom/6AFDEF',
|
|
49
|
-
twilio: 'https://cdn.simpleicons.org/twilio/F22F46',
|
|
50
|
-
telnyx: 'https://cdn.simpleicons.org/telnyx/00C08B',
|
|
51
|
-
aws: 'https://cdn.simpleicons.org/amazonaws/FF9900',
|
|
52
|
-
plaid: 'https://cdn.simpleicons.org/plaid/white',
|
|
53
|
-
};
|
|
19
|
+
import { INTEGRATION_LOGOS } from '../../lib/integration-logos';
|
|
54
20
|
|
|
55
21
|
const ROLE_PRESETS = [
|
|
56
22
|
{ id: 'chat', label: 'Chat', desc: 'Companion, assistant, conversation', icon: MessageCircle, tier: 'Medium' },
|
|
@@ -114,14 +80,23 @@ export function SpawnWizard() {
|
|
|
114
80
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
115
81
|
const [spawning, setSpawning] = useState(false);
|
|
116
82
|
const [selectedPeerId, setSelectedPeerId] = useState('');
|
|
83
|
+
const [recommendations, setRecommendations] = useState([]);
|
|
84
|
+
const [preflightDialog, setPreflightDialog] = useState(null);
|
|
85
|
+
const [claudeAuth, setClaudeAuth] = useState(null);
|
|
86
|
+
const [claudeAuthLoading, setClaudeAuthLoading] = useState(false);
|
|
87
|
+
const [claudeAuthPolling, setClaudeAuthPolling] = useState(false);
|
|
117
88
|
const federation = useGrooveStore((s) => s.federation);
|
|
118
89
|
|
|
90
|
+
const selectedRole = role || customRole;
|
|
91
|
+
const selectedProvider = providers.find((p) => p.id === provider);
|
|
92
|
+
const availableModels = selectedProvider?.models || [];
|
|
93
|
+
const installedProviders = providers.filter((p) => p.installed);
|
|
94
|
+
|
|
119
95
|
useEffect(() => {
|
|
120
96
|
if (open) {
|
|
121
97
|
fetchProviders().then((data) => {
|
|
122
98
|
const list = Array.isArray(data) ? data : data.providers || [];
|
|
123
99
|
setProviders(list);
|
|
124
|
-
// Auto-select first installed provider
|
|
125
100
|
const installed = list.filter((p) => p.installed);
|
|
126
101
|
if (installed.length > 0 && !provider) {
|
|
127
102
|
const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
|
|
@@ -149,16 +124,53 @@ export function SpawnWizard() {
|
|
|
149
124
|
setSelectedPersonality('');
|
|
150
125
|
setSelectedPeerId('');
|
|
151
126
|
setShowAdvanced(false);
|
|
127
|
+
setRecommendations([]);
|
|
128
|
+
setPreflightDialog(null);
|
|
129
|
+
setClaudeAuth(null);
|
|
130
|
+
setClaudeAuthLoading(false);
|
|
131
|
+
setClaudeAuthPolling(false);
|
|
152
132
|
}
|
|
153
133
|
}, [open, fetchProviders]);
|
|
154
134
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!selectedRole || !open) { setRecommendations([]); return; }
|
|
137
|
+
api.get(`/roles/integrations?role=${encodeURIComponent(selectedRole)}`).then((data) => {
|
|
138
|
+
const recs = Array.isArray(data) ? data : data?.recommendations || [];
|
|
139
|
+
setRecommendations(recs);
|
|
140
|
+
const autoSelect = recs
|
|
141
|
+
.filter((r) => r.installed && r.configured && r.authenticated)
|
|
142
|
+
.map((r) => r.id);
|
|
143
|
+
if (autoSelect.length > 0) {
|
|
144
|
+
setSelectedIntegrations((prev) => [...new Set([...prev, ...autoSelect])]);
|
|
145
|
+
}
|
|
146
|
+
}).catch(() => setRecommendations([]));
|
|
147
|
+
}, [selectedRole, open]);
|
|
159
148
|
|
|
160
|
-
|
|
161
|
-
if (!
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!open || provider !== 'claude-code') { setClaudeAuth(null); return; }
|
|
151
|
+
api.get('/providers/claude-code/auth').then((data) => {
|
|
152
|
+
setClaudeAuth(data);
|
|
153
|
+
}).catch(() => setClaudeAuth(null));
|
|
154
|
+
}, [open, provider]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!claudeAuthPolling) return;
|
|
158
|
+
const start = Date.now();
|
|
159
|
+
const interval = setInterval(() => {
|
|
160
|
+
if (Date.now() - start > 300000) { setClaudeAuthPolling(false); clearInterval(interval); return; }
|
|
161
|
+
api.get('/providers/claude-code/auth').then((data) => {
|
|
162
|
+
if (data?.authenticated) {
|
|
163
|
+
setClaudeAuth(data);
|
|
164
|
+
setClaudeAuthPolling(false);
|
|
165
|
+
setClaudeAuthLoading(false);
|
|
166
|
+
clearInterval(interval);
|
|
167
|
+
}
|
|
168
|
+
}).catch(() => {});
|
|
169
|
+
}, 2000);
|
|
170
|
+
return () => clearInterval(interval);
|
|
171
|
+
}, [claudeAuthPolling]);
|
|
172
|
+
|
|
173
|
+
async function runSpawn() {
|
|
162
174
|
setSpawning(true);
|
|
163
175
|
try {
|
|
164
176
|
const config = {
|
|
@@ -180,6 +192,33 @@ export function SpawnWizard() {
|
|
|
180
192
|
setSpawning(false);
|
|
181
193
|
}
|
|
182
194
|
|
|
195
|
+
async function handleSpawn() {
|
|
196
|
+
if (!selectedRole) return;
|
|
197
|
+
try {
|
|
198
|
+
const preflight = await api.post('/agents/preflight', {
|
|
199
|
+
role: selectedRole,
|
|
200
|
+
integrations: selectedIntegrations,
|
|
201
|
+
});
|
|
202
|
+
if (preflight?.issues?.length > 0) {
|
|
203
|
+
setPreflightDialog(preflight.issues);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
} catch { /* preflight endpoint may not exist yet — proceed */ }
|
|
207
|
+
runSpawn();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function handleClaudeLogin() {
|
|
211
|
+
setClaudeAuthLoading(true);
|
|
212
|
+
try {
|
|
213
|
+
await api.post('/providers/claude-code/login');
|
|
214
|
+
setClaudeAuthPolling(true);
|
|
215
|
+
} catch {
|
|
216
|
+
setClaudeAuthLoading(false);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const claudeNotAuthed = provider === 'claude-code' && claudeAuth && !claudeAuth.authenticated;
|
|
221
|
+
|
|
183
222
|
return (
|
|
184
223
|
<Sheet open={open} onOpenChange={(o) => { if (!o) closeDetail(); }}>
|
|
185
224
|
<SheetContent title="Spawn Agent" width={480}>
|
|
@@ -237,6 +276,77 @@ export function SpawnWizard() {
|
|
|
237
276
|
</div>
|
|
238
277
|
</div>
|
|
239
278
|
|
|
279
|
+
{/* Recommended Integrations */}
|
|
280
|
+
{selectedRole && recommendations.length > 0 && (
|
|
281
|
+
<div>
|
|
282
|
+
<label className="text-xs font-semibold text-text-2 font-sans uppercase tracking-wider block mb-2">
|
|
283
|
+
Recommended Integrations
|
|
284
|
+
</label>
|
|
285
|
+
<div className="space-y-1.5">
|
|
286
|
+
{recommendations.map((rec) => {
|
|
287
|
+
const logoUrl = INTEGRATION_LOGOS[rec.id];
|
|
288
|
+
if (rec.installed && rec.configured && rec.authenticated) {
|
|
289
|
+
return (
|
|
290
|
+
<div key={rec.id} className="flex items-center gap-2.5 px-3 py-2 rounded-md bg-success/5 border border-success/20">
|
|
291
|
+
<Check size={13} className="text-success flex-shrink-0" />
|
|
292
|
+
{logoUrl ? (
|
|
293
|
+
<img src={logoUrl} alt="" className="w-3.5 h-3.5 flex-shrink-0" />
|
|
294
|
+
) : (
|
|
295
|
+
<Plug size={12} className="text-text-3 flex-shrink-0" />
|
|
296
|
+
)}
|
|
297
|
+
<span className="text-xs font-semibold text-text-0 font-sans">{rec.name || rec.id}</span>
|
|
298
|
+
<Badge variant="success" className="text-2xs ml-auto">Ready</Badge>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (rec.installed) {
|
|
303
|
+
return (
|
|
304
|
+
<div key={rec.id} className="flex items-center gap-2.5 px-3 py-2 rounded-md bg-warning/5 border border-warning/20">
|
|
305
|
+
<AlertTriangle size={13} className="text-warning flex-shrink-0" />
|
|
306
|
+
{logoUrl ? (
|
|
307
|
+
<img src={logoUrl} alt="" className="w-3.5 h-3.5 flex-shrink-0" />
|
|
308
|
+
) : (
|
|
309
|
+
<Plug size={12} className="text-text-3 flex-shrink-0" />
|
|
310
|
+
)}
|
|
311
|
+
<span className="text-xs font-semibold text-text-0 font-sans">{rec.name || rec.id}</span>
|
|
312
|
+
<Button
|
|
313
|
+
variant="ghost"
|
|
314
|
+
size="sm"
|
|
315
|
+
className="ml-auto text-2xs text-warning h-6 px-2"
|
|
316
|
+
onClick={() => {
|
|
317
|
+
closeDetail();
|
|
318
|
+
useGrooveStore.getState().setActiveView('marketplace');
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
Configure
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return (
|
|
327
|
+
<div key={rec.id} className="flex items-center gap-2.5 px-3 py-2 rounded-md bg-surface-1 border border-border-subtle">
|
|
328
|
+
{logoUrl ? (
|
|
329
|
+
<img src={logoUrl} alt="" className="w-3.5 h-3.5 flex-shrink-0 opacity-40" />
|
|
330
|
+
) : (
|
|
331
|
+
<Plug size={12} className="text-text-4 flex-shrink-0" />
|
|
332
|
+
)}
|
|
333
|
+
<span className="text-xs text-text-3 font-sans">{rec.name || rec.id}</span>
|
|
334
|
+
<button
|
|
335
|
+
onClick={() => {
|
|
336
|
+
closeDetail();
|
|
337
|
+
useGrooveStore.getState().setActiveView('marketplace');
|
|
338
|
+
}}
|
|
339
|
+
className="ml-auto text-2xs text-accent hover:underline font-sans cursor-pointer"
|
|
340
|
+
>
|
|
341
|
+
Install in Marketplace
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
})}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
|
|
240
350
|
{/* Ambassador server picker */}
|
|
241
351
|
{selectedRole === 'ambassador' && (() => {
|
|
242
352
|
const eligible = federation.whitelist.filter((e) => typeof e === 'object' && (e.status === 'mutual' || e.status === 'connected'));
|
|
@@ -340,6 +450,33 @@ export function SpawnWizard() {
|
|
|
340
450
|
</div>
|
|
341
451
|
)}
|
|
342
452
|
|
|
453
|
+
{/* Claude Code Auth */}
|
|
454
|
+
{claudeNotAuthed && (
|
|
455
|
+
<div className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3">
|
|
456
|
+
<div className="flex items-center gap-2 mb-2">
|
|
457
|
+
<AlertTriangle size={13} className="text-warning flex-shrink-0" />
|
|
458
|
+
<span className="text-xs font-semibold text-text-0 font-sans">Claude Code is not signed in</span>
|
|
459
|
+
</div>
|
|
460
|
+
{claudeAuthLoading ? (
|
|
461
|
+
<div className="flex items-center gap-2 text-2xs text-text-2 font-sans">
|
|
462
|
+
<Loader2 size={12} className="animate-spin text-accent" />
|
|
463
|
+
Waiting for browser authentication...
|
|
464
|
+
</div>
|
|
465
|
+
) : (
|
|
466
|
+
<Button variant="primary" size="sm" onClick={handleClaudeLogin} className="text-2xs gap-1.5">
|
|
467
|
+
<ExternalLink size={10} />
|
|
468
|
+
Sign in to Claude
|
|
469
|
+
</Button>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
{provider === 'claude-code' && claudeAuth?.authenticated && (
|
|
474
|
+
<div className="flex items-center gap-2 text-2xs text-text-2 font-sans">
|
|
475
|
+
<div className="w-2 h-2 rounded-full bg-success flex-shrink-0" />
|
|
476
|
+
Signed in as {claudeAuth.email || 'Claude user'} ({claudeAuth.subscriptionType || 'subscription'})
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
|
|
343
480
|
{/* Skills */}
|
|
344
481
|
<div className="space-y-1.5">
|
|
345
482
|
<label className="text-xs font-medium text-text-2 font-sans">Skills</label>
|
|
@@ -742,7 +879,7 @@ export function SpawnWizard() {
|
|
|
742
879
|
variant="primary"
|
|
743
880
|
size="lg"
|
|
744
881
|
onClick={handleSpawn}
|
|
745
|
-
disabled={!selectedRole || spawning || installedProviders.length === 0}
|
|
882
|
+
disabled={!selectedRole || spawning || installedProviders.length === 0 || claudeNotAuthed}
|
|
746
883
|
className="w-full"
|
|
747
884
|
>
|
|
748
885
|
{spawning ? 'Spawning...' : 'Spawn Agent'}
|
|
@@ -750,6 +887,31 @@ export function SpawnWizard() {
|
|
|
750
887
|
</div>
|
|
751
888
|
</div>
|
|
752
889
|
</SheetContent>
|
|
890
|
+
|
|
891
|
+
{/* Preflight confirmation dialog */}
|
|
892
|
+
<Dialog open={!!preflightDialog} onOpenChange={(o) => { if (!o) setPreflightDialog(null); }}>
|
|
893
|
+
<DialogContent title="Integration Warning" className="max-w-sm">
|
|
894
|
+
<div className="space-y-4 p-4">
|
|
895
|
+
<div className="space-y-2">
|
|
896
|
+
{(preflightDialog || []).map((issue, i) => (
|
|
897
|
+
<div key={i} className="flex items-start gap-2 text-xs text-text-1 font-sans">
|
|
898
|
+
<AlertTriangle size={13} className="text-warning flex-shrink-0 mt-0.5" />
|
|
899
|
+
<span>{issue.name ? `${issue.name}: ${issue.problem === 'not_installed' ? 'not installed' : issue.problem === 'not_configured' ? 'not configured' : 'not authenticated'}` : issue.message || String(issue)}</span>
|
|
900
|
+
</div>
|
|
901
|
+
))}
|
|
902
|
+
</div>
|
|
903
|
+
<p className="text-2xs text-text-3 font-sans">Continue anyway?</p>
|
|
904
|
+
<div className="flex gap-2">
|
|
905
|
+
<Button variant="ghost" size="md" onClick={() => setPreflightDialog(null)} className="flex-1">
|
|
906
|
+
Cancel
|
|
907
|
+
</Button>
|
|
908
|
+
<Button variant="warning" size="md" onClick={() => { setPreflightDialog(null); runSpawn(); }} className="flex-1">
|
|
909
|
+
Spawn Anyway
|
|
910
|
+
</Button>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
</DialogContent>
|
|
914
|
+
</Dialog>
|
|
753
915
|
</Sheet>
|
|
754
916
|
);
|
|
755
917
|
}
|
|
@@ -12,25 +12,7 @@ import {
|
|
|
12
12
|
Key, Shield, Trash2, ChevronRight, X, Copy, RefreshCw,
|
|
13
13
|
} from 'lucide-react';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const INTEGRATION_LOGOS = {
|
|
17
|
-
slack: 'https://cdn.simpleicons.org/slack/E01E5A',
|
|
18
|
-
github: 'https://cdn.simpleicons.org/github/white',
|
|
19
|
-
stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
|
|
20
|
-
gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
|
|
21
|
-
'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
|
|
22
|
-
'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
|
|
23
|
-
'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
|
|
24
|
-
'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
|
|
25
|
-
'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
|
|
26
|
-
'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
|
|
27
|
-
postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
|
|
28
|
-
notion: 'https://cdn.simpleicons.org/notion/white',
|
|
29
|
-
discord: 'https://cdn.simpleicons.org/discord/5865F2',
|
|
30
|
-
linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
|
|
31
|
-
'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
|
|
32
|
-
'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
|
|
33
|
-
};
|
|
15
|
+
import { INTEGRATION_LOGOS } from '../../lib/integration-logos';
|
|
34
16
|
|
|
35
17
|
function IntegrationIcon({ item, size = 48 }) {
|
|
36
18
|
const logoUrl = INTEGRATION_LOGOS[item.id];
|
|
@@ -272,8 +254,6 @@ function GoogleOAuthSetup({ integrationId, onConfigured }) {
|
|
|
272
254
|
setSaving(false);
|
|
273
255
|
}
|
|
274
256
|
|
|
275
|
-
const redirectUri = 'http://localhost:31415/api/integrations/oauth/callback';
|
|
276
|
-
|
|
277
257
|
const steps = [
|
|
278
258
|
{ text: 'Go to the Google Cloud Console and sign in with your Google account',
|
|
279
259
|
link: { url: 'https://console.cloud.google.com', label: 'Open Google Cloud Console' } },
|
|
@@ -283,9 +263,8 @@ function GoogleOAuthSetup({ integrationId, onConfigured }) {
|
|
|
283
263
|
{ text: <>Go to <strong>Credentials</strong> and click <strong>Create Credentials</strong> → <strong>OAuth client ID</strong></>,
|
|
284
264
|
link: { url: 'https://console.cloud.google.com/apis/credentials', label: 'Open Credentials page' } },
|
|
285
265
|
{ text: <>If prompted to configure the consent screen, choose <strong>External</strong>, fill in an app name (e.g. "Groove"), your email, and save. You can skip optional fields.</> },
|
|
286
|
-
{ text: <>
|
|
287
|
-
{ text: <>
|
|
288
|
-
copyable: redirectUri },
|
|
266
|
+
{ text: <>Go to <strong>Audience</strong> and click <strong>Publish App</strong>. Then scroll down to <strong>Test users</strong>, click <strong>Add Users</strong>, enter your Google email address, and save.</> },
|
|
267
|
+
{ text: <>For Application type, choose <strong>Desktop app</strong> (not Web application). Give it any name.</> },
|
|
289
268
|
{ text: <>Click <strong>Create</strong>, then copy the <strong>Client ID</strong> and <strong>Client Secret</strong> and paste them below.</> },
|
|
290
269
|
];
|
|
291
270
|
|
|
@@ -339,6 +318,14 @@ function GoogleOAuthSetup({ integrationId, onConfigured }) {
|
|
|
339
318
|
</p>
|
|
340
319
|
</div>
|
|
341
320
|
|
|
321
|
+
<div className="bg-warning/8 border border-warning/15 rounded-md px-4 py-2.5">
|
|
322
|
+
<p className="text-2xs text-text-2 font-sans leading-relaxed">
|
|
323
|
+
<strong className="text-text-1">Google "unverified app" warning</strong> — when signing in, Google may show a warning
|
|
324
|
+
that the app isn't verified. This is normal for personal OAuth apps. Click <strong>Advanced</strong>, then <strong>Go
|
|
325
|
+
to [your app name] (unsafe)</strong> to continue. Your credentials stay local and are never sent to Groove servers.
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
342
329
|
<div className="h-px bg-border-subtle" />
|
|
343
330
|
|
|
344
331
|
<div className="space-y-3">
|