mindroom 0.0.0__py3-none-any.whl → 0.1.0__py3-none-any.whl

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 (155) hide show
  1. mindroom/__init__.py +3 -0
  2. mindroom/agent_prompts.py +963 -0
  3. mindroom/agents.py +248 -0
  4. mindroom/ai.py +421 -0
  5. mindroom/api/__init__.py +1 -0
  6. mindroom/api/credentials.py +137 -0
  7. mindroom/api/google_integration.py +355 -0
  8. mindroom/api/google_tools_helper.py +40 -0
  9. mindroom/api/homeassistant_integration.py +421 -0
  10. mindroom/api/integrations.py +189 -0
  11. mindroom/api/main.py +506 -0
  12. mindroom/api/matrix_operations.py +219 -0
  13. mindroom/api/tools.py +94 -0
  14. mindroom/background_tasks.py +87 -0
  15. mindroom/bot.py +2470 -0
  16. mindroom/cli.py +86 -0
  17. mindroom/commands.py +377 -0
  18. mindroom/config.py +343 -0
  19. mindroom/config_commands.py +324 -0
  20. mindroom/config_confirmation.py +411 -0
  21. mindroom/constants.py +52 -0
  22. mindroom/credentials.py +146 -0
  23. mindroom/credentials_sync.py +134 -0
  24. mindroom/custom_tools/__init__.py +8 -0
  25. mindroom/custom_tools/config_manager.py +765 -0
  26. mindroom/custom_tools/gmail.py +92 -0
  27. mindroom/custom_tools/google_calendar.py +92 -0
  28. mindroom/custom_tools/google_sheets.py +92 -0
  29. mindroom/custom_tools/homeassistant.py +341 -0
  30. mindroom/error_handling.py +35 -0
  31. mindroom/file_watcher.py +49 -0
  32. mindroom/interactive.py +313 -0
  33. mindroom/logging_config.py +207 -0
  34. mindroom/matrix/__init__.py +1 -0
  35. mindroom/matrix/client.py +782 -0
  36. mindroom/matrix/event_info.py +173 -0
  37. mindroom/matrix/identity.py +149 -0
  38. mindroom/matrix/large_messages.py +267 -0
  39. mindroom/matrix/mentions.py +141 -0
  40. mindroom/matrix/message_builder.py +94 -0
  41. mindroom/matrix/message_content.py +209 -0
  42. mindroom/matrix/presence.py +178 -0
  43. mindroom/matrix/rooms.py +311 -0
  44. mindroom/matrix/state.py +77 -0
  45. mindroom/matrix/typing.py +91 -0
  46. mindroom/matrix/users.py +217 -0
  47. mindroom/memory/__init__.py +21 -0
  48. mindroom/memory/config.py +137 -0
  49. mindroom/memory/functions.py +396 -0
  50. mindroom/py.typed +0 -0
  51. mindroom/response_tracker.py +128 -0
  52. mindroom/room_cleanup.py +139 -0
  53. mindroom/routing.py +107 -0
  54. mindroom/scheduling.py +758 -0
  55. mindroom/stop.py +207 -0
  56. mindroom/streaming.py +203 -0
  57. mindroom/teams.py +749 -0
  58. mindroom/thread_utils.py +318 -0
  59. mindroom/tools/__init__.py +520 -0
  60. mindroom/tools/agentql.py +64 -0
  61. mindroom/tools/airflow.py +57 -0
  62. mindroom/tools/apify.py +49 -0
  63. mindroom/tools/arxiv.py +64 -0
  64. mindroom/tools/aws_lambda.py +41 -0
  65. mindroom/tools/aws_ses.py +57 -0
  66. mindroom/tools/baidusearch.py +87 -0
  67. mindroom/tools/brightdata.py +116 -0
  68. mindroom/tools/browserbase.py +62 -0
  69. mindroom/tools/cal_com.py +98 -0
  70. mindroom/tools/calculator.py +112 -0
  71. mindroom/tools/cartesia.py +84 -0
  72. mindroom/tools/composio.py +166 -0
  73. mindroom/tools/config_manager.py +44 -0
  74. mindroom/tools/confluence.py +73 -0
  75. mindroom/tools/crawl4ai.py +101 -0
  76. mindroom/tools/csv.py +104 -0
  77. mindroom/tools/custom_api.py +106 -0
  78. mindroom/tools/dalle.py +85 -0
  79. mindroom/tools/daytona.py +180 -0
  80. mindroom/tools/discord.py +81 -0
  81. mindroom/tools/docker.py +73 -0
  82. mindroom/tools/duckdb.py +124 -0
  83. mindroom/tools/duckduckgo.py +99 -0
  84. mindroom/tools/e2b.py +121 -0
  85. mindroom/tools/eleven_labs.py +77 -0
  86. mindroom/tools/email.py +74 -0
  87. mindroom/tools/exa.py +246 -0
  88. mindroom/tools/fal.py +50 -0
  89. mindroom/tools/file.py +80 -0
  90. mindroom/tools/financial_datasets_api.py +112 -0
  91. mindroom/tools/firecrawl.py +124 -0
  92. mindroom/tools/gemini.py +85 -0
  93. mindroom/tools/giphy.py +49 -0
  94. mindroom/tools/github.py +376 -0
  95. mindroom/tools/gmail.py +102 -0
  96. mindroom/tools/google_calendar.py +55 -0
  97. mindroom/tools/google_maps.py +112 -0
  98. mindroom/tools/google_sheets.py +86 -0
  99. mindroom/tools/googlesearch.py +83 -0
  100. mindroom/tools/groq.py +77 -0
  101. mindroom/tools/hackernews.py +54 -0
  102. mindroom/tools/jina.py +108 -0
  103. mindroom/tools/jira.py +70 -0
  104. mindroom/tools/linear.py +103 -0
  105. mindroom/tools/linkup.py +65 -0
  106. mindroom/tools/lumalabs.py +71 -0
  107. mindroom/tools/mem0.py +82 -0
  108. mindroom/tools/modelslabs.py +85 -0
  109. mindroom/tools/moviepy_video_tools.py +62 -0
  110. mindroom/tools/newspaper4k.py +63 -0
  111. mindroom/tools/openai.py +143 -0
  112. mindroom/tools/openweather.py +89 -0
  113. mindroom/tools/oxylabs.py +54 -0
  114. mindroom/tools/pandas.py +35 -0
  115. mindroom/tools/pubmed.py +64 -0
  116. mindroom/tools/python.py +120 -0
  117. mindroom/tools/reddit.py +155 -0
  118. mindroom/tools/replicate.py +56 -0
  119. mindroom/tools/resend.py +55 -0
  120. mindroom/tools/scrapegraph.py +87 -0
  121. mindroom/tools/searxng.py +120 -0
  122. mindroom/tools/serpapi.py +55 -0
  123. mindroom/tools/serper.py +81 -0
  124. mindroom/tools/shell.py +46 -0
  125. mindroom/tools/slack.py +80 -0
  126. mindroom/tools/sleep.py +38 -0
  127. mindroom/tools/spider.py +62 -0
  128. mindroom/tools/sql.py +138 -0
  129. mindroom/tools/tavily.py +104 -0
  130. mindroom/tools/telegram.py +54 -0
  131. mindroom/tools/todoist.py +103 -0
  132. mindroom/tools/trello.py +121 -0
  133. mindroom/tools/twilio.py +97 -0
  134. mindroom/tools/web_browser_tools.py +37 -0
  135. mindroom/tools/webex.py +63 -0
  136. mindroom/tools/website.py +45 -0
  137. mindroom/tools/whatsapp.py +81 -0
  138. mindroom/tools/wikipedia.py +45 -0
  139. mindroom/tools/x.py +97 -0
  140. mindroom/tools/yfinance.py +121 -0
  141. mindroom/tools/youtube.py +81 -0
  142. mindroom/tools/zendesk.py +62 -0
  143. mindroom/tools/zep.py +107 -0
  144. mindroom/tools/zoom.py +62 -0
  145. mindroom/tools_metadata.json +7643 -0
  146. mindroom/tools_metadata.py +220 -0
  147. mindroom/topic_generator.py +153 -0
  148. mindroom/voice_handler.py +266 -0
  149. mindroom-0.1.0.dist-info/METADATA +425 -0
  150. mindroom-0.1.0.dist-info/RECORD +152 -0
  151. {mindroom-0.0.0.dist-info → mindroom-0.1.0.dist-info}/WHEEL +1 -2
  152. mindroom-0.1.0.dist-info/entry_points.txt +2 -0
  153. mindroom-0.0.0.dist-info/METADATA +0 -24
  154. mindroom-0.0.0.dist-info/RECORD +0 -4
  155. mindroom-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,137 @@
1
+ """Unified credentials management API."""
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from pydantic import BaseModel
7
+
8
+ from mindroom.credentials import get_credentials_manager
9
+
10
+ router = APIRouter(prefix="/api/credentials", tags=["credentials"])
11
+
12
+
13
+ class SetApiKeyRequest(BaseModel):
14
+ """Request to set an API key."""
15
+
16
+ service: str
17
+ api_key: str
18
+ key_name: str = "api_key"
19
+
20
+
21
+ class CredentialStatus(BaseModel):
22
+ """Status of a service's credentials."""
23
+
24
+ service: str
25
+ has_credentials: bool
26
+ key_names: list[str] | None = None
27
+
28
+
29
+ class SetCredentialsRequest(BaseModel):
30
+ """Request to set multiple credentials for a service."""
31
+
32
+ credentials: dict[str, Any] # Can be strings, booleans, numbers, etc.
33
+
34
+
35
+ @router.get("/list")
36
+ async def list_services() -> list[str]:
37
+ """List all services with stored credentials."""
38
+ manager = get_credentials_manager()
39
+ return manager.list_services()
40
+
41
+
42
+ @router.get("/{service}/status")
43
+ async def get_credential_status(service: str) -> CredentialStatus:
44
+ """Get the status of credentials for a service."""
45
+ manager = get_credentials_manager()
46
+ credentials = manager.load_credentials(service)
47
+
48
+ if credentials:
49
+ return CredentialStatus(
50
+ service=service,
51
+ has_credentials=True,
52
+ key_names=list(credentials.keys()) if isinstance(credentials, dict) else None,
53
+ )
54
+
55
+ return CredentialStatus(service=service, has_credentials=False)
56
+
57
+
58
+ @router.post("/{service}")
59
+ async def set_credentials(service: str, request: SetCredentialsRequest) -> dict[str, str]:
60
+ """Set multiple credentials for a service."""
61
+ manager = get_credentials_manager()
62
+
63
+ # Save all credentials for the service
64
+ manager.save_credentials(service, request.credentials)
65
+
66
+ return {"status": "success", "message": f"Credentials saved for {service}"}
67
+
68
+
69
+ @router.post("/{service}/api-key")
70
+ async def set_api_key(service: str, request: SetApiKeyRequest) -> dict[str, str]:
71
+ """Set an API key for a service."""
72
+ if request.service != service:
73
+ raise HTTPException(status_code=400, detail="Service mismatch in request")
74
+
75
+ manager = get_credentials_manager()
76
+ manager.set_api_key(service, request.api_key, request.key_name)
77
+
78
+ return {"status": "success", "message": f"API key set for {service}"}
79
+
80
+
81
+ @router.get("/{service}/api-key")
82
+ async def get_api_key(service: str, key_name: str = "api_key") -> dict[str, Any]:
83
+ """Get the API key for a service (returns only existence status for security)."""
84
+ manager = get_credentials_manager()
85
+ api_key = manager.get_api_key(service, key_name)
86
+
87
+ if api_key:
88
+ # Don't return the actual key for security
89
+ return {
90
+ "service": service,
91
+ "has_key": True,
92
+ "key_name": key_name,
93
+ # Return masked version
94
+ "masked_key": f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else "****",
95
+ }
96
+
97
+ return {"service": service, "has_key": False, "key_name": key_name}
98
+
99
+
100
+ @router.get("/{service}")
101
+ async def get_credentials(service: str) -> dict[str, Any]:
102
+ """Get credentials for a service (for editing)."""
103
+ manager = get_credentials_manager()
104
+ credentials = manager.load_credentials(service)
105
+
106
+ if not credentials:
107
+ return {"service": service, "credentials": {}}
108
+
109
+ return {"service": service, "credentials": credentials}
110
+
111
+
112
+ @router.delete("/{service}")
113
+ async def delete_credentials(service: str) -> dict[str, str]:
114
+ """Delete all credentials for a service."""
115
+ manager = get_credentials_manager()
116
+ manager.delete_credentials(service)
117
+
118
+ return {"status": "success", "message": f"Credentials deleted for {service}"}
119
+
120
+
121
+ @router.post("/{service}/test")
122
+ async def test_credentials(service: str) -> dict[str, Any]:
123
+ """Test if credentials are valid for a service."""
124
+ # This is a placeholder - actual testing would depend on the service
125
+ manager = get_credentials_manager()
126
+ credentials = manager.load_credentials(service)
127
+
128
+ if not credentials:
129
+ raise HTTPException(status_code=404, detail=f"No credentials found for {service}")
130
+
131
+ # For now, just check if credentials exist
132
+ # In the future, we could implement actual validation per service
133
+ return {
134
+ "service": service,
135
+ "status": "success",
136
+ "message": "Credentials exist (validation not implemented)",
137
+ }
@@ -0,0 +1,355 @@
1
+ """Unified Google Integration for MindRoom.
2
+
3
+ This module provides a single, comprehensive Google OAuth integration supporting:
4
+ - Gmail (read, compose, modify)
5
+ - Google Calendar (events, scheduling)
6
+ - Google Drive (file access)
7
+
8
+ Replaces the previous fragmented gmail_config.py, google_auth.py, and google_setup_wizard.py
9
+ """
10
+
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Any
14
+ from urllib.parse import urlparse
15
+
16
+ import jwt
17
+ from fastapi import APIRouter, HTTPException, Request
18
+ from fastapi.responses import RedirectResponse
19
+ from google.auth.transport.requests import Request as GoogleRequest
20
+ from google.oauth2.credentials import Credentials
21
+ from google_auth_oauthlib.flow import Flow # type: ignore[import-untyped]
22
+ from pydantic import BaseModel
23
+
24
+ from mindroom.credentials import CredentialsManager
25
+
26
+ router = APIRouter(prefix="/api/google", tags=["google-integration"])
27
+
28
+ # Initialize credentials manager
29
+ creds_manager = CredentialsManager()
30
+
31
+ # OAuth scopes for all Google services needed by MindRoom
32
+ SCOPES = [
33
+ # Gmail
34
+ "https://www.googleapis.com/auth/gmail.readonly",
35
+ "https://www.googleapis.com/auth/gmail.modify",
36
+ "https://www.googleapis.com/auth/gmail.compose",
37
+ # Calendar
38
+ "https://www.googleapis.com/auth/calendar",
39
+ # Sheets
40
+ "https://www.googleapis.com/auth/spreadsheets",
41
+ # Drive
42
+ "https://www.googleapis.com/auth/drive.file",
43
+ # User info
44
+ "openid",
45
+ "https://www.googleapis.com/auth/userinfo.email",
46
+ "https://www.googleapis.com/auth/userinfo.profile",
47
+ ]
48
+
49
+ # Environment path for OAuth credentials
50
+ ENV_PATH = Path(__file__).parent.parent.parent.parent.parent / ".env"
51
+
52
+ # Get configuration from environment
53
+ BACKEND_PORT = os.getenv("BACKEND_PORT", "8765")
54
+ REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI", f"http://localhost:{BACKEND_PORT}/api/google/callback")
55
+
56
+
57
+ class GoogleStatus(BaseModel):
58
+ """Google integration status."""
59
+
60
+ connected: bool
61
+ email: str | None = None
62
+ services: list[str] = []
63
+ error: str | None = None
64
+ has_credentials: bool = False
65
+
66
+
67
+ class GoogleAuthUrl(BaseModel):
68
+ """Google OAuth URL response."""
69
+
70
+ auth_url: str
71
+
72
+
73
+ def get_oauth_credentials() -> dict[str, Any] | None:
74
+ """Get OAuth credentials from environment variables."""
75
+ client_id = os.getenv("GOOGLE_CLIENT_ID")
76
+ client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
77
+
78
+ if not client_id or not client_secret:
79
+ return None
80
+
81
+ return {
82
+ "web": {
83
+ "client_id": client_id,
84
+ "client_secret": client_secret,
85
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
86
+ "token_uri": "https://oauth2.googleapis.com/token",
87
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
88
+ "redirect_uris": [REDIRECT_URI],
89
+ },
90
+ }
91
+
92
+
93
+ def get_google_credentials() -> Credentials | None:
94
+ """Get Google credentials from stored token."""
95
+ token_data = creds_manager.load_credentials("google")
96
+ if not token_data:
97
+ return None
98
+
99
+ try:
100
+ creds = Credentials(
101
+ token=token_data.get("token"),
102
+ refresh_token=token_data.get("refresh_token"),
103
+ token_uri=token_data.get("token_uri"),
104
+ client_id=token_data.get("client_id"),
105
+ client_secret=token_data.get("client_secret"),
106
+ scopes=token_data.get("scopes", SCOPES),
107
+ )
108
+
109
+ # Refresh token if expired
110
+ if creds and creds.expired and creds.refresh_token:
111
+ creds.refresh(GoogleRequest())
112
+ # Save refreshed credentials
113
+ save_credentials(creds)
114
+ except Exception:
115
+ return None
116
+ else:
117
+ return creds if creds and creds.valid else None
118
+
119
+
120
+ def save_credentials(creds: Credentials) -> None:
121
+ """Save credentials using the unified credentials manager."""
122
+ # Full token with all scopes
123
+ token_data = {
124
+ "token": creds.token,
125
+ "refresh_token": creds.refresh_token,
126
+ "token_uri": creds.token_uri,
127
+ "client_id": creds.client_id,
128
+ "client_secret": creds.client_secret,
129
+ "scopes": creds.scopes,
130
+ }
131
+
132
+ # Add ID token if available for user info
133
+ if hasattr(creds, "_id_token") and creds._id_token:
134
+ token_data["_id_token"] = creds._id_token
135
+
136
+ # Save using credentials manager (handles backward compatibility)
137
+ creds_manager.save_credentials("google", token_data)
138
+
139
+
140
+ def save_env_credentials(client_id: str, client_secret: str, project_id: str | None = None) -> None:
141
+ """Save OAuth credentials to .env file."""
142
+ env_lines = []
143
+ if ENV_PATH.exists():
144
+ with ENV_PATH.open() as f:
145
+ env_lines = f.readlines()
146
+
147
+ # Update or add credentials
148
+ # Use current environment variable for redirect URI to support multiple deployments
149
+ current_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", REDIRECT_URI)
150
+ env_vars = {
151
+ "GOOGLE_CLIENT_ID": client_id,
152
+ "GOOGLE_CLIENT_SECRET": client_secret,
153
+ "GOOGLE_PROJECT_ID": project_id or "mindroom-integration",
154
+ "GOOGLE_REDIRECT_URI": current_redirect_uri,
155
+ "BACKEND_PORT": BACKEND_PORT,
156
+ }
157
+
158
+ for key, value in env_vars.items():
159
+ found = False
160
+ for i, line in enumerate(env_lines):
161
+ if line.startswith(f"{key}="):
162
+ env_lines[i] = f"{key}={value}\n"
163
+ found = True
164
+ break
165
+ if not found:
166
+ env_lines.append(f"{key}={value}\n")
167
+
168
+ # Write back to .env file
169
+ with ENV_PATH.open("w") as f:
170
+ f.writelines(env_lines)
171
+
172
+ # Also set in current environment
173
+ for key, value in env_vars.items():
174
+ os.environ[key] = value
175
+
176
+
177
+ @router.get("/status")
178
+ async def get_status() -> GoogleStatus:
179
+ """Check Google integration status."""
180
+ # Check environment variables
181
+ client_id = os.getenv("GOOGLE_CLIENT_ID")
182
+ client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
183
+ has_credentials = bool(client_id and client_secret)
184
+
185
+ # Get current credentials
186
+ creds = get_google_credentials()
187
+
188
+ if not creds:
189
+ return GoogleStatus(
190
+ connected=False,
191
+ has_credentials=has_credentials,
192
+ )
193
+
194
+ try:
195
+ # Check which services are accessible based on scopes
196
+ services = []
197
+ if creds.has_scopes(["https://www.googleapis.com/auth/gmail.modify"]):
198
+ services.append("Gmail")
199
+ if creds.has_scopes(["https://www.googleapis.com/auth/calendar"]):
200
+ services.append("Google Calendar")
201
+ if creds.has_scopes(["https://www.googleapis.com/auth/spreadsheets"]):
202
+ services.append("Google Sheets")
203
+ if creds.has_scopes(["https://www.googleapis.com/auth/drive.file"]):
204
+ services.append("Google Drive")
205
+
206
+ # Get user email from token
207
+ email = None
208
+ try:
209
+ if hasattr(creds, "_id_token") and creds._id_token:
210
+ decoded = jwt.decode(creds._id_token, options={"verify_signature": False})
211
+ email = decoded.get("email")
212
+ except Exception:
213
+ email = None
214
+
215
+ return GoogleStatus(
216
+ connected=True,
217
+ email=email,
218
+ services=services,
219
+ has_credentials=has_credentials,
220
+ )
221
+ except Exception as e:
222
+ return GoogleStatus(
223
+ connected=False,
224
+ error=str(e),
225
+ has_credentials=has_credentials,
226
+ )
227
+
228
+
229
+ @router.post("/connect")
230
+ async def connect() -> GoogleAuthUrl:
231
+ """Start Google OAuth flow."""
232
+ oauth_config = get_oauth_credentials()
233
+ if not oauth_config:
234
+ raise HTTPException(
235
+ status_code=503,
236
+ detail="Google OAuth is not configured. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.",
237
+ )
238
+
239
+ try:
240
+ # Create OAuth flow with all scopes
241
+ # Use current environment variable for redirect URI to support multiple deployments
242
+ current_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", REDIRECT_URI)
243
+ flow = Flow.from_client_config(oauth_config, scopes=SCOPES, redirect_uri=current_redirect_uri)
244
+
245
+ # Generate authorization URL
246
+ auth_url, _ = flow.authorization_url(
247
+ access_type="offline",
248
+ include_granted_scopes="true",
249
+ prompt="consent",
250
+ )
251
+
252
+ return GoogleAuthUrl(auth_url=auth_url)
253
+ except Exception as e:
254
+ raise HTTPException(status_code=500, detail=f"Failed to start Google OAuth: {e!s}") from e
255
+
256
+
257
+ @router.get("/callback")
258
+ async def callback(request: Request) -> RedirectResponse:
259
+ """Handle Google OAuth callback."""
260
+ # Get the authorization code from the callback
261
+ code = request.query_params.get("code")
262
+ if not code:
263
+ raise HTTPException(status_code=400, detail="No authorization code received")
264
+
265
+ oauth_config = get_oauth_credentials()
266
+ if not oauth_config:
267
+ raise HTTPException(status_code=503, detail="OAuth not configured")
268
+
269
+ try:
270
+ # Create OAuth flow and exchange code for tokens
271
+ # Use current environment variable for redirect URI to support multiple deployments
272
+ current_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", REDIRECT_URI)
273
+ flow = Flow.from_client_config(oauth_config, scopes=SCOPES, redirect_uri=current_redirect_uri)
274
+ flow.fetch_token(code=code)
275
+
276
+ # Save credentials
277
+ save_credentials(flow.credentials)
278
+
279
+ # Redirect back to widget with success message
280
+ # Extract the domain from the redirect URI for the final redirect
281
+ parsed_uri = urlparse(current_redirect_uri)
282
+ base_url = f"{parsed_uri.scheme}://{parsed_uri.netloc}"
283
+ return RedirectResponse(url=f"{base_url}/?google=connected")
284
+ except Exception as e:
285
+ # Check if it's a scope change error
286
+ error_msg = str(e)
287
+ if "Scope has changed" in error_msg:
288
+ raise HTTPException(
289
+ status_code=400,
290
+ detail=f"OAuth scope mismatch: {error_msg}. Please disconnect and reconnect to authorize with the new scopes.",
291
+ ) from e
292
+ raise HTTPException(status_code=500, detail=f"OAuth callback failed: {error_msg}") from e
293
+
294
+
295
+ @router.post("/disconnect")
296
+ async def disconnect() -> dict[str, str]:
297
+ """Disconnect Google services by removing stored tokens."""
298
+ try:
299
+ # Remove credentials using the manager
300
+ creds_manager.delete_credentials("google")
301
+ except Exception as e:
302
+ raise HTTPException(status_code=500, detail=f"Failed to disconnect: {e!s}") from e
303
+ else:
304
+ return {"status": "disconnected"}
305
+
306
+
307
+ @router.post("/configure")
308
+ async def configure(credentials: dict[str, str]) -> dict[str, Any]:
309
+ """Configure Google OAuth credentials manually."""
310
+ client_id = credentials.get("client_id")
311
+ client_secret = credentials.get("client_secret")
312
+ project_id = credentials.get("project_id", "mindroom-integration")
313
+
314
+ if not client_id or not client_secret:
315
+ raise HTTPException(
316
+ status_code=400,
317
+ detail="client_id and client_secret are required",
318
+ )
319
+
320
+ try:
321
+ # Save to environment
322
+ save_env_credentials(client_id, client_secret, project_id)
323
+ except Exception as e:
324
+ raise HTTPException(status_code=500, detail=f"Failed to save credentials: {e!s}") from e
325
+ else:
326
+ return {"success": True, "message": "Google OAuth credentials configured successfully"}
327
+
328
+
329
+ @router.post("/reset")
330
+ async def reset() -> dict[str, Any]:
331
+ """Reset Google integration by removing all credentials and tokens."""
332
+ try:
333
+ # Remove credentials using the manager
334
+ creds_manager.delete_credentials("google")
335
+
336
+ # Remove from environment variables
337
+ if ENV_PATH.exists():
338
+ with ENV_PATH.open() as f:
339
+ lines = f.readlines()
340
+
341
+ # Filter out Google-related variables
342
+ google_vars = [
343
+ "GOOGLE_CLIENT_ID",
344
+ "GOOGLE_CLIENT_SECRET",
345
+ "GOOGLE_PROJECT_ID",
346
+ "GOOGLE_REDIRECT_URI",
347
+ ]
348
+ filtered_lines = [line for line in lines if not any(line.startswith(f"{var}=") for var in google_vars)]
349
+
350
+ with ENV_PATH.open("w") as f:
351
+ f.writelines(filtered_lines)
352
+ except Exception as e:
353
+ raise HTTPException(status_code=500, detail=f"Failed to reset: {e!s}") from e
354
+ else:
355
+ return {"success": True, "message": "Google integration reset successfully"}
@@ -0,0 +1,40 @@
1
+ """Helper utilities for Google tools management."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def get_google_tool_scopes(tool_name: str) -> list[str]:
7
+ """Get required OAuth scopes for a Google tool."""
8
+ scope_map = {
9
+ "google_calendar": [
10
+ "https://www.googleapis.com/auth/calendar",
11
+ "https://www.googleapis.com/auth/calendar.readonly",
12
+ ],
13
+ "google_sheets": [
14
+ "https://www.googleapis.com/auth/spreadsheets",
15
+ "https://www.googleapis.com/auth/spreadsheets.readonly",
16
+ ],
17
+ "gmail": [
18
+ "https://www.googleapis.com/auth/gmail.modify",
19
+ "https://www.googleapis.com/auth/gmail.readonly",
20
+ "https://www.googleapis.com/auth/gmail.compose",
21
+ "https://www.googleapis.com/auth/gmail.send",
22
+ ],
23
+ }
24
+
25
+ return scope_map.get(tool_name, [])
26
+
27
+
28
+ def check_google_tool_configured(tool_name: str, google_creds: dict[str, Any]) -> bool:
29
+ """Check if a Google tool has the required OAuth scopes configured."""
30
+ if not google_creds or "token" not in google_creds:
31
+ return False
32
+
33
+ configured_scopes = google_creds.get("scopes", [])
34
+ required_scopes = get_google_tool_scopes(tool_name)
35
+
36
+ if not required_scopes:
37
+ return False
38
+
39
+ # Check if any of the required scopes are present
40
+ return any(scope in configured_scopes for scope in required_scopes)