groove-dev 0.17.7 → 0.18.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.
Files changed (112) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
  3. package/node_modules/@groove-dev/daemon/integrations-registry.json +10 -48
  4. package/node_modules/@groove-dev/daemon/src/api.js +103 -12
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +94 -16
  6. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
  7. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
  8. package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
  9. package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
  10. package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
  11. package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
  12. package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
  13. package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
  14. package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
  15. package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
  16. package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +182 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  19. package/node_modules/@groove-dev/gui/src/App.jsx +27 -4
  20. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +1 -1
  21. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +839 -586
  22. package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
  23. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +157 -42
  24. package/package.json +1 -2
  25. package/packages/daemon/integrations-registry.json +10 -48
  26. package/packages/daemon/src/api.js +103 -12
  27. package/packages/daemon/src/integrations.js +94 -16
  28. package/packages/daemon/src/providers/claude-code.js +4 -0
  29. package/packages/gui/dist/assets/index-DXkccbmd.js +182 -0
  30. package/packages/gui/dist/index.html +1 -1
  31. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
  32. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
  33. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
  34. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
  35. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
  36. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
  37. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
  38. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
  39. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
  40. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
  41. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
  42. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
  43. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
  44. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
  45. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
  46. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
  47. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
  48. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
  49. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
  50. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
  51. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
  52. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
  53. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
  54. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
  55. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
  56. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
  57. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
  58. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
  59. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
  60. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
  61. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
  62. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
  63. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
  64. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
  65. package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
  66. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
  67. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
  68. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
  69. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
  70. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
  71. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
  72. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
  73. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
  74. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
  75. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
  76. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
  77. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
  78. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  79. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  80. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
  81. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
  82. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
  83. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
  84. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
  85. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
  86. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
  87. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
  88. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
  89. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
  90. package/packages/gui/node_modules/.vite/deps/package.json +3 -0
  91. package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
  92. package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
  93. package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
  94. package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  95. package/packages/gui/node_modules/.vite/deps/react.js +5 -0
  96. package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
  97. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  98. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  99. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  100. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  101. package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
  102. package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
  103. package/packages/gui/src/App.jsx +27 -4
  104. package/packages/gui/src/components/AgentChat.jsx +1 -1
  105. package/packages/gui/src/components/SpawnPanel.jsx +839 -586
  106. package/packages/gui/src/views/FileEditor.jsx +85 -1
  107. package/packages/gui/src/views/IntegrationsStore.jsx +157 -42
  108. package/docs/FILE-EDITOR-PLAN.md +0 -253
  109. package/docs/GUI_DESIGN_SPEC.md +0 -402
  110. package/docs/SKILLS-API-SPEC.md +0 -277
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CsymvgNh.js +0 -156
  112. package/packages/gui/dist/assets/index-CsymvgNh.js +0 -156
@@ -1,7 +1,7 @@
1
1
  // GROOVE GUI — File Editor View
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React, { useState, useCallback } from 'react';
4
+ import React, { useState, useEffect, useCallback } from 'react';
5
5
  import { useGrooveStore } from '../stores/groove';
6
6
  import FileTree from '../components/FileTree';
7
7
  import EditorTabs from '../components/EditorTabs';
@@ -18,9 +18,39 @@ export default function FileEditor() {
18
18
  const saveFile = useGrooveStore((s) => s.saveFile);
19
19
  const reloadFile = useGrooveStore((s) => s.reloadFile);
20
20
  const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
21
+ const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
21
22
 
22
23
  const [terminalOpen, setTerminalOpen] = useState(false);
23
24
  const [terminalHeight, setTerminalHeight] = useState(220);
25
+ const [rootDir, setRootDir] = useState('');
26
+ const [rootInput, setRootInput] = useState('');
27
+ const [editingRoot, setEditingRoot] = useState(false);
28
+
29
+ // Fetch current editor root on mount
30
+ useEffect(() => {
31
+ fetch('/api/files/root').then((r) => r.json()).then((d) => {
32
+ setRootDir(d.root || '');
33
+ setRootInput(d.root || '');
34
+ }).catch(() => {});
35
+ }, []);
36
+
37
+ async function handleRootChange() {
38
+ if (!rootInput.trim()) return;
39
+ try {
40
+ const res = await fetch('/api/files/root', {
41
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ root: rootInput.trim() }),
43
+ });
44
+ const data = await res.json();
45
+ if (data.ok) {
46
+ setRootDir(data.root);
47
+ setEditingRoot(false);
48
+ // Clear tree cache and reload
49
+ useGrooveStore.setState({ editorTreeCache: {}, editorOpenTabs: [], editorActiveFile: null, editorFiles: {} });
50
+ fetchTreeDir('');
51
+ }
52
+ } catch { /* ignore */ }
53
+ }
24
54
 
25
55
  const file = activeFile ? files[activeFile] : null;
26
56
  const isChanged = activeFile && changedFiles[activeFile];
@@ -54,6 +84,29 @@ export default function FileEditor() {
54
84
 
55
85
  return (
56
86
  <div style={styles.container}>
87
+ {/* Directory bar */}
88
+ <div style={styles.dirBar}>
89
+ <span style={styles.dirLabel}>DIR</span>
90
+ {editingRoot ? (
91
+ <div style={{ display: 'flex', flex: 1, gap: 4 }}>
92
+ <input
93
+ style={styles.dirInput}
94
+ value={rootInput}
95
+ onChange={(e) => setRootInput(e.target.value)}
96
+ onKeyDown={(e) => { if (e.key === 'Enter') handleRootChange(); if (e.key === 'Escape') { setEditingRoot(false); setRootInput(rootDir); } }}
97
+ placeholder="/absolute/path/to/project"
98
+ autoFocus
99
+ />
100
+ <button onClick={handleRootChange} style={styles.dirOkBtn}>Open</button>
101
+ <button onClick={() => { setEditingRoot(false); setRootInput(rootDir); }} style={styles.dirCancelBtn}>&times;</button>
102
+ </div>
103
+ ) : (
104
+ <button onClick={() => setEditingRoot(true)} style={styles.dirPath} title="Click to change directory">
105
+ {rootDir || '...'}
106
+ </button>
107
+ )}
108
+ </div>
109
+
57
110
  {/* Tab bar */}
58
111
  <EditorTabs />
59
112
 
@@ -130,6 +183,37 @@ const styles = {
130
183
  display: 'flex', flexDirection: 'column',
131
184
  height: '100%', width: '100%',
132
185
  },
186
+ dirBar: {
187
+ display: 'flex', alignItems: 'center', gap: 8,
188
+ padding: '4px 12px', flexShrink: 0,
189
+ borderBottom: '1px solid var(--border)',
190
+ background: 'var(--bg-chrome)',
191
+ },
192
+ dirLabel: {
193
+ fontSize: 9, fontWeight: 700, color: 'var(--text-dim)',
194
+ letterSpacing: 1, flexShrink: 0,
195
+ },
196
+ dirPath: {
197
+ background: 'none', border: 'none', color: 'var(--text-primary)',
198
+ fontSize: 11, fontFamily: 'var(--font)', cursor: 'pointer',
199
+ padding: '2px 6px', borderRadius: 3, textAlign: 'left',
200
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
201
+ flex: 1,
202
+ },
203
+ dirInput: {
204
+ flex: 1, background: 'var(--bg-surface)', border: '1px solid var(--accent)',
205
+ borderRadius: 3, padding: '2px 8px', color: 'var(--text-primary)',
206
+ fontSize: 11, outline: 'none', fontFamily: 'var(--font)',
207
+ },
208
+ dirOkBtn: {
209
+ background: 'var(--accent)', color: 'var(--bg-base)', border: 'none',
210
+ borderRadius: 3, padding: '2px 10px', fontSize: 10, fontWeight: 600,
211
+ cursor: 'pointer', fontFamily: 'var(--font)',
212
+ },
213
+ dirCancelBtn: {
214
+ background: 'none', border: 'none', color: 'var(--text-dim)',
215
+ fontSize: 14, cursor: 'pointer', padding: '0 4px',
216
+ },
133
217
  contentRow: {
134
218
  flex: 1, display: 'flex', overflow: 'hidden',
135
219
  },
@@ -84,10 +84,12 @@ function CredentialModal({ integration, onClose }) {
84
84
  const [googleClientId, setGoogleClientId] = useState('');
85
85
  const [googleClientSecret, setGoogleClientSecret] = useState('');
86
86
  const [showGoogleSetup, setShowGoogleSetup] = useState(false);
87
+ const [errorMsg, setErrorMsg] = useState('');
87
88
 
88
89
  useEffect(() => {
89
- if (integration?.authType === 'oauth-google') {
90
+ if (integration?.authType === 'oauth-google' || integration?.authType === 'google-autoauth' || integration?._googleSetupNeeded) {
90
91
  setOauthStatus('checking');
92
+ setShowGoogleSetup(integration?._googleSetupNeeded || false);
91
93
  fetch('/api/integrations/google-oauth/status')
92
94
  .then((r) => r.json())
93
95
  .then((data) => setOauthStatus(data.configured ? 'ready' : 'not-configured'))
@@ -98,12 +100,14 @@ function CredentialModal({ integration, onClose }) {
98
100
  if (!integration) return null;
99
101
 
100
102
  const isOAuth = integration.authType === 'oauth-google';
103
+ const isGoogleAutoAuth = integration.authType === 'google-autoauth' || integration._googleSetupNeeded;
101
104
  const envKeys = (integration.envKeys || []).filter((ek) => !ek.hidden);
102
105
  const setupSteps = integration.setupSteps || [];
103
106
 
104
107
  async function handleSave(key) {
105
108
  if (!values[key]) return;
106
109
  setSaving(true);
110
+ setErrorMsg('');
107
111
  try {
108
112
  const res = await fetch(`/api/integrations/${integration.id}/credentials`, {
109
113
  method: 'POST',
@@ -113,28 +117,61 @@ function CredentialModal({ integration, onClose }) {
113
117
  if (res.ok) {
114
118
  setSaved((prev) => ({ ...prev, [key]: true }));
115
119
  setValues((prev) => ({ ...prev, [key]: '' }));
120
+ } else {
121
+ const data = await res.json();
122
+ setErrorMsg(data.error || `Failed to save ${key}`);
116
123
  }
117
- } catch { /* ignore */ }
124
+ } catch {
125
+ setErrorMsg('Could not reach the server');
126
+ }
118
127
  setSaving(false);
119
128
  }
120
129
 
121
130
  async function handleGoogleSetup() {
122
131
  if (!googleClientId || !googleClientSecret) return;
123
132
  setSaving(true);
133
+ setErrorMsg('');
124
134
  try {
125
- await fetch('/api/integrations/google-oauth/setup', {
135
+ const res = await fetch('/api/integrations/google-oauth/setup', {
126
136
  method: 'POST',
127
137
  headers: { 'Content-Type': 'application/json' },
128
138
  body: JSON.stringify({ clientId: googleClientId, clientSecret: googleClientSecret }),
129
139
  });
130
- setOauthStatus('ready');
131
- setShowGoogleSetup(false);
132
- } catch { /* ignore */ }
140
+ if (res.ok) {
141
+ setOauthStatus('ready');
142
+ setShowGoogleSetup(false);
143
+ } else {
144
+ const data = await res.json();
145
+ setErrorMsg(data.error || 'Failed to save credentials');
146
+ }
147
+ } catch {
148
+ setErrorMsg('Could not reach the server');
149
+ }
133
150
  setSaving(false);
134
151
  }
135
152
 
153
+ async function handleAutoAuthConnect() {
154
+ setOauthStatus('connecting');
155
+ setErrorMsg('');
156
+ try {
157
+ const res = await fetch(`/api/integrations/${integration.id}/authenticate`, { method: 'POST' });
158
+ const data = await res.json();
159
+ if (data.ok) {
160
+ // MCP server spawned — it will open a browser for OAuth consent
161
+ setTimeout(() => onClose(), 3000);
162
+ } else {
163
+ setErrorMsg(data.error || 'Authentication failed');
164
+ setOauthStatus('ready');
165
+ }
166
+ } catch {
167
+ setErrorMsg('Could not reach the server');
168
+ setOauthStatus('ready');
169
+ }
170
+ }
171
+
136
172
  async function handleOAuthConnect() {
137
173
  setOauthStatus('connecting');
174
+ setErrorMsg('');
138
175
  try {
139
176
  const res = await fetch(`/api/integrations/${integration.id}/oauth/start`, { method: 'POST' });
140
177
  const data = await res.json();
@@ -154,8 +191,12 @@ function CredentialModal({ integration, onClose }) {
154
191
  }, 2000);
155
192
  // Stop polling after 5 minutes
156
193
  setTimeout(() => clearInterval(poll), 300000);
194
+ } else {
195
+ setErrorMsg(data.error || 'Failed to start OAuth flow');
196
+ setOauthStatus('ready');
157
197
  }
158
198
  } catch {
199
+ setErrorMsg('Could not reach the server');
159
200
  setOauthStatus('ready');
160
201
  }
161
202
  }
@@ -218,30 +259,59 @@ function CredentialModal({ integration, onClose }) {
218
259
  </a>
219
260
  )}
220
261
 
221
- {/* OAuth flow for Google integrations */}
222
- {isOAuth && (
262
+ {/* Error message */}
263
+ {errorMsg && (
264
+ <div style={{
265
+ padding: '10px 14px', marginBottom: 14, borderRadius: 6,
266
+ background: 'rgba(224, 108, 117, 0.08)', border: '1px solid var(--red)',
267
+ fontSize: 11, color: 'var(--red)', lineHeight: 1.5,
268
+ }}>
269
+ {errorMsg}
270
+ </div>
271
+ )}
272
+
273
+ {/* OAuth flow for Google integrations (both oauth-google and google-autoauth) */}
274
+ {(isOAuth || isGoogleAutoAuth) && (
223
275
  <div style={{ marginBottom: 16 }}>
224
- {/* Always show the primary Connect button */}
225
- <button
226
- onClick={oauthStatus === 'ready' ? handleOAuthConnect : () => setShowGoogleSetup(true)}
227
- disabled={oauthStatus === 'checking' || oauthStatus === 'connecting'}
228
- style={{
229
- display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
230
- width: '100%', padding: '12px 16px', marginBottom: 12,
231
- background: oauthStatus === 'connecting' ? 'var(--bg-active)' : '#4285f4',
232
- color: '#fff', border: 'none', borderRadius: 6,
233
- fontSize: 13, fontWeight: 600, cursor: 'pointer',
234
- fontFamily: 'var(--font)',
235
- opacity: oauthStatus === 'checking' ? 0.5 : 1,
236
- }}
237
- >
238
- {oauthStatus === 'checking' ? 'Checking...'
239
- : oauthStatus === 'connecting' ? 'Waiting for authorization...'
240
- : `Connect with Google`}
241
- </button>
276
+ {/* Show Connect button only when OAuth is configured */}
277
+ {oauthStatus === 'ready' && (
278
+ <button
279
+ onClick={isGoogleAutoAuth ? handleAutoAuthConnect : handleOAuthConnect}
280
+ disabled={oauthStatus === 'connecting'}
281
+ style={{
282
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
283
+ width: '100%', padding: '12px 16px', marginBottom: 12,
284
+ background: '#4285f4',
285
+ color: '#fff', border: 'none', borderRadius: 6,
286
+ fontSize: 13, fontWeight: 600, cursor: 'pointer',
287
+ fontFamily: 'var(--font)',
288
+ }}
289
+ >
290
+ Connect with Google
291
+ </button>
292
+ )}
293
+ {oauthStatus === 'checking' && (
294
+ <div style={{
295
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
296
+ padding: '12px 16px', marginBottom: 12,
297
+ color: 'var(--text-muted)', fontSize: 12,
298
+ }}>
299
+ Checking Google credentials...
300
+ </div>
301
+ )}
302
+ {oauthStatus === 'connecting' && (
303
+ <div style={{
304
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
305
+ padding: '12px 16px', marginBottom: 12,
306
+ background: 'var(--bg-active)', borderRadius: 6,
307
+ color: 'var(--text-primary)', fontSize: 12, fontWeight: 600,
308
+ }}>
309
+ Waiting for authorization — check your browser...
310
+ </div>
311
+ )}
242
312
 
243
- {/* First-time setup: show inline when Connect is clicked and OAuth not configured */}
244
- {showGoogleSetup && oauthStatus === 'not-configured' && (
313
+ {/* First-time setup: shown when OAuth not configured */}
314
+ {oauthStatus === 'not-configured' && (
245
315
  <div style={{
246
316
  padding: 14, borderRadius: 8,
247
317
  background: 'var(--bg-surface)', border: '1px solid var(--border)',
@@ -292,9 +362,12 @@ function CredentialModal({ integration, onClose }) {
292
362
  <button
293
363
  onClick={async () => {
294
364
  await handleGoogleSetup();
295
- // After saving, immediately trigger the OAuth connect flow
365
+ // After saving, immediately trigger the appropriate connect flow
296
366
  if (googleClientId && googleClientSecret) {
297
- setTimeout(() => handleOAuthConnect(), 500);
367
+ setTimeout(() => {
368
+ if (isGoogleAutoAuth) handleAutoAuthConnect();
369
+ else handleOAuthConnect();
370
+ }, 500);
298
371
  }
299
372
  }}
300
373
  disabled={saving || !googleClientId || !googleClientSecret}
@@ -369,7 +442,8 @@ function CredentialModal({ integration, onClose }) {
369
442
  function IntegrationDetailModal({ integration, installing, onInstall, onUninstall, onConfigure, onAuthenticate, onClose }) {
370
443
  if (!integration) return null;
371
444
 
372
- const isAutoAuth = integration.authType === 'none' && (integration.envKeys || []).length === 0;
445
+ const isAutoAuth = (integration.authType === 'none' || integration.authType === 'google-autoauth')
446
+ && (integration.envKeys || []).length === 0;
373
447
  const hasCredentials = (integration.envKeys || []).filter((ek) => !ek.hidden).length > 0
374
448
  || integration.authType === 'oauth-google';
375
449
 
@@ -683,26 +757,42 @@ export default function IntegrationsStore() {
683
757
  async function handleInstall(id) {
684
758
  setInstalling(id);
685
759
  try {
686
- await fetch(`/api/integrations/${id}/install`, { method: 'POST' });
760
+ const res = await fetch(`/api/integrations/${id}/install`, { method: 'POST' });
761
+ const data = await res.json();
762
+ if (!res.ok) {
763
+ flash(data.error || 'Install failed', 'error');
764
+ setInstalling(null);
765
+ return;
766
+ }
687
767
  await fetchIntegrations();
688
768
  // After install, refresh selected item
689
769
  if (selectedItem?.id === id) {
690
770
  const updated = integrations.find((s) => s.id === id);
691
771
  if (updated) setSelectedItem({ ...updated, installed: true });
692
772
  }
693
- } catch { /* ignore */ }
773
+ } catch (err) {
774
+ flash('Install failed — check daemon logs', 'error');
775
+ }
694
776
  setInstalling(null);
695
777
  }
696
778
 
697
779
  async function handleUninstall(id) {
698
780
  setInstalling(id);
699
781
  try {
700
- await fetch(`/api/integrations/${id}`, { method: 'DELETE' });
782
+ const res = await fetch(`/api/integrations/${id}`, { method: 'DELETE' });
783
+ if (!res.ok) {
784
+ const data = await res.json();
785
+ flash(data.error || 'Uninstall failed', 'error');
786
+ setInstalling(null);
787
+ return;
788
+ }
701
789
  await fetchIntegrations();
702
790
  if (selectedItem?.id === id) {
703
791
  setSelectedItem((prev) => prev ? { ...prev, installed: false, configured: false } : null);
704
792
  }
705
- } catch { /* ignore */ }
793
+ } catch {
794
+ flash('Uninstall failed', 'error');
795
+ }
706
796
  setInstalling(null);
707
797
  }
708
798
 
@@ -716,23 +806,44 @@ export default function IntegrationsStore() {
716
806
  }
717
807
 
718
808
  async function handleAuthenticate(item) {
809
+ // For google-autoauth, check if Google OAuth is configured first
810
+ if (item.authType === 'google-autoauth') {
811
+ try {
812
+ const statusRes = await fetch('/api/integrations/google-oauth/status');
813
+ const statusData = await statusRes.json();
814
+ if (!statusData.configured) {
815
+ // Need Google OAuth setup first — open the credential modal with setup form
816
+ setSelectedItem(null);
817
+ setConfiguring({ ...item, _googleSetupNeeded: true });
818
+ return;
819
+ }
820
+ } catch {
821
+ // Can't check status — open setup form as fallback
822
+ setSelectedItem(null);
823
+ setConfiguring({ ...item, _googleSetupNeeded: true });
824
+ return;
825
+ }
826
+ }
827
+
828
+ // OAuth is configured — trigger the authentication flow
719
829
  setSelectedItem(null);
830
+ setConfiguring({ ...item }); // Show credential modal in connecting state
720
831
  try {
721
832
  const res = await fetch(`/api/integrations/${item.id}/authenticate`, { method: 'POST' });
722
833
  const data = await res.json();
723
834
  if (data.ok) {
724
835
  flash('Sign-in window opened — check your browser');
725
836
  } else {
726
- flash(data.error || 'Authentication failed');
837
+ flash(data.error || 'Authentication failed', 'error');
727
838
  }
728
839
  } catch {
729
- flash('Authentication failed');
840
+ flash('Authentication failed — check daemon logs', 'error');
730
841
  }
731
842
  }
732
843
 
733
- function flash(msg) {
734
- setStatusMsg(msg);
735
- setTimeout(() => setStatusMsg(''), 4000);
844
+ function flash(msg, type = 'info') {
845
+ setStatusMsg({ text: msg, type });
846
+ setTimeout(() => setStatusMsg(''), 6000);
736
847
  }
737
848
 
738
849
  function handleConfigureClose() {
@@ -783,8 +894,12 @@ export default function IntegrationsStore() {
783
894
 
784
895
  {/* Status message */}
785
896
  {statusMsg && (
786
- <div style={{ padding: '4px 20px', fontSize: 10, color: 'var(--accent)', flexShrink: 0 }}>
787
- {statusMsg}
897
+ <div style={{
898
+ padding: '8px 20px', fontSize: 12, fontWeight: 500, flexShrink: 0,
899
+ color: statusMsg.type === 'error' ? 'var(--red)' : 'var(--accent)',
900
+ background: statusMsg.type === 'error' ? 'rgba(224, 108, 117, 0.06)' : 'transparent',
901
+ }}>
902
+ {statusMsg.text || statusMsg}
788
903
  </div>
789
904
  )}
790
905
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.17.7",
3
+ "version": "0.18.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)",
@@ -22,7 +22,6 @@
22
22
  "claude-code",
23
23
  "codex",
24
24
  "gemini",
25
-
26
25
  "ollama",
27
26
  "coding-assistant",
28
27
  "developer-tools",
@@ -99,12 +99,13 @@
99
99
  "transport": "stdio",
100
100
  "command": "npx",
101
101
  "args": ["-y", "@gongrzhe/server-calendar-autoauth-mcp"],
102
- "authType": "none",
102
+ "authType": "google-autoauth",
103
+ "oauthKeysDir": ".calendar-mcp",
103
104
  "envKeys": [],
104
105
  "setupSteps": [
105
- "Click Install, then click 'Sign in with Google'",
106
- "A browser window will openauthorize Groove",
107
- "Done — no API keys or tokens needed"
106
+ "One-time: link your Google Cloud OAuth app (shared across all Google integrations)",
107
+ "Click 'Sign in with Google'a browser opens for authorization",
108
+ "Done — the MCP server handles token refresh automatically"
108
109
  ],
109
110
  "featured": false,
110
111
  "downloads": 0,
@@ -124,12 +125,13 @@
124
125
  "transport": "stdio",
125
126
  "command": "npx",
126
127
  "args": ["-y", "@gongrzhe/server-gmail-autoauth-mcp"],
127
- "authType": "none",
128
+ "authType": "google-autoauth",
129
+ "oauthKeysDir": ".gmail-mcp",
128
130
  "envKeys": [],
129
131
  "setupSteps": [
130
- "Click Install, then click 'Sign in with Google'",
131
- "A browser window will openauthorize Groove",
132
- "Done — no API keys or tokens needed"
132
+ "One-time: link your Google Cloud OAuth app (shared across all Google integrations)",
133
+ "Click 'Sign in with Google'a browser opens for authorization",
134
+ "Done — the MCP server handles token refresh automatically"
133
135
  ],
134
136
  "featured": false,
135
137
  "downloads": 0,
@@ -336,26 +338,6 @@
336
338
  "ratingCount": 0,
337
339
  "verified": "community"
338
340
  },
339
- {
340
- "id": "filesystem",
341
- "name": "Filesystem",
342
- "description": "Read, write, search, and manage files on the local filesystem",
343
- "category": "developer",
344
- "icon": "folder",
345
- "tags": ["files", "filesystem", "local", "storage"],
346
- "roles": ["backend", "fullstack", "devops"],
347
- "npmPackage": "@modelcontextprotocol/server-filesystem",
348
- "transport": "stdio",
349
- "command": "npx",
350
- "args": ["-y", "@modelcontextprotocol/server-filesystem"],
351
- "authType": "none",
352
- "envKeys": [],
353
- "featured": false,
354
- "downloads": 0,
355
- "rating": 0,
356
- "ratingCount": 0,
357
- "verified": "mcp-official"
358
- },
359
341
  {
360
342
  "id": "google-maps",
361
343
  "name": "Google Maps",
@@ -383,25 +365,5 @@
383
365
  "rating": 0,
384
366
  "ratingCount": 0,
385
367
  "verified": "mcp-official"
386
- },
387
- {
388
- "id": "sqlite",
389
- "name": "SQLite",
390
- "description": "Query and manage SQLite databases, inspect schemas, run SQL",
391
- "category": "database",
392
- "icon": "database",
393
- "tags": ["sql", "database", "local", "lightweight"],
394
- "roles": ["analyst", "backend"],
395
- "npmPackage": "mcp-sqlite",
396
- "transport": "stdio",
397
- "command": "npx",
398
- "args": ["-y", "mcp-sqlite"],
399
- "authType": "none",
400
- "envKeys": [],
401
- "featured": false,
402
- "downloads": 0,
403
- "rating": 0,
404
- "ratingCount": 0,
405
- "verified": "mcp-official"
406
368
  }
407
369
  ]