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.
Files changed (49) hide show
  1. package/.groove-staging/state.json +3 -0
  2. package/.groove-staging/timeline.json +13 -0
  3. package/CLAUDE.md +0 -10
  4. package/DECENTRALIZED_NET_WP_V1.md +871 -0
  5. package/README.md +28 -0
  6. package/SECURITY_SWEEP.md +228 -0
  7. package/decentralized-net/ACTION_PLAN.md +422 -0
  8. package/node_modules/@groove-dev/cli/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/package.json +1 -1
  10. package/node_modules/@groove-dev/daemon/src/api.js +99 -0
  11. package/node_modules/@groove-dev/daemon/src/introducer.js +7 -7
  12. package/node_modules/@groove-dev/daemon/src/journalist.js +36 -6
  13. package/node_modules/@groove-dev/daemon/src/memory.js +29 -10
  14. package/node_modules/@groove-dev/daemon/src/process.js +29 -12
  15. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +26 -1
  16. package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -11
  17. package/node_modules/@groove-dev/daemon/src/rotator.js +24 -1
  18. package/node_modules/@groove-dev/daemon/test/introducer.test.js +63 -0
  19. package/node_modules/@groove-dev/daemon/test/journalist.test.js +106 -0
  20. package/node_modules/@groove-dev/daemon/test/memory.test.js +49 -0
  21. package/node_modules/@groove-dev/daemon/test/rotator.test.js +99 -0
  22. package/node_modules/@groove-dev/gui/dist/assets/{index-DieCV-v1.js → index-Ch1N9G4Z.js} +1728 -1728
  23. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  24. package/node_modules/@groove-dev/gui/package.json +1 -1
  25. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +147 -21
  26. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +206 -44
  27. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +11 -24
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +1 -36
  29. package/node_modules/@groove-dev/gui/src/lib/integration-logos.js +39 -0
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/package.json +1 -1
  33. package/packages/daemon/src/api.js +99 -0
  34. package/packages/daemon/src/introducer.js +7 -7
  35. package/packages/daemon/src/journalist.js +36 -6
  36. package/packages/daemon/src/memory.js +29 -10
  37. package/packages/daemon/src/process.js +29 -12
  38. package/packages/daemon/src/providers/claude-code.js +26 -1
  39. package/packages/daemon/src/providers/codex.js +34 -11
  40. package/packages/daemon/src/rotator.js +24 -1
  41. package/packages/gui/dist/assets/{index-DieCV-v1.js → index-Ch1N9G4Z.js} +1728 -1728
  42. package/packages/gui/dist/index.html +1 -1
  43. package/packages/gui/package.json +1 -1
  44. package/packages/gui/src/components/agents/agent-config.jsx +147 -21
  45. package/packages/gui/src/components/agents/spawn-wizard.jsx +206 -44
  46. package/packages/gui/src/components/marketplace/integration-wizard.jsx +11 -24
  47. package/packages/gui/src/components/marketplace/marketplace-card.jsx +1 -36
  48. package/packages/gui/src/lib/integration-logos.js +39 -0
  49. 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-DieCV-v1.js"></script>
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">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.26",
3
+ "version": "0.27.28",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
- const selectedRole = role || customRole;
156
- const selectedProvider = providers.find((p) => p.id === provider);
157
- const availableModels = selectedProvider?.models || [];
158
- const installedProviders = providers.filter((p) => p.installed);
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
- async function handleSpawn() {
161
- if (!selectedRole) return;
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
- // Reuse integration logos from marketplace-card
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> &rarr; <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. &quot;Groove&quot;), your email, and save. You can skip optional fields.</> },
286
- { text: <>For Application type, choose <strong>Web application</strong>. Give it any name.</> },
287
- { text: <>Under <strong>Authorized redirect URIs</strong>, click <strong>Add URI</strong> and paste this exact URL:</>,
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 &quot;unverified app&quot; warning</strong> — when signing in, Google may show a warning
324
+ that the app isn&apos;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">