vibesurf 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.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (70) hide show
  1. vibe_surf/__init__.py +12 -0
  2. vibe_surf/_version.py +34 -0
  3. vibe_surf/agents/__init__.py +0 -0
  4. vibe_surf/agents/browser_use_agent.py +1106 -0
  5. vibe_surf/agents/prompts/__init__.py +1 -0
  6. vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
  7. vibe_surf/agents/report_writer_agent.py +360 -0
  8. vibe_surf/agents/vibe_surf_agent.py +1632 -0
  9. vibe_surf/backend/__init__.py +0 -0
  10. vibe_surf/backend/api/__init__.py +3 -0
  11. vibe_surf/backend/api/activity.py +243 -0
  12. vibe_surf/backend/api/config.py +740 -0
  13. vibe_surf/backend/api/files.py +322 -0
  14. vibe_surf/backend/api/models.py +257 -0
  15. vibe_surf/backend/api/task.py +300 -0
  16. vibe_surf/backend/database/__init__.py +13 -0
  17. vibe_surf/backend/database/manager.py +129 -0
  18. vibe_surf/backend/database/models.py +164 -0
  19. vibe_surf/backend/database/queries.py +922 -0
  20. vibe_surf/backend/database/schemas.py +100 -0
  21. vibe_surf/backend/llm_config.py +182 -0
  22. vibe_surf/backend/main.py +137 -0
  23. vibe_surf/backend/migrations/__init__.py +16 -0
  24. vibe_surf/backend/migrations/init_db.py +303 -0
  25. vibe_surf/backend/migrations/seed_data.py +236 -0
  26. vibe_surf/backend/shared_state.py +601 -0
  27. vibe_surf/backend/utils/__init__.py +7 -0
  28. vibe_surf/backend/utils/encryption.py +164 -0
  29. vibe_surf/backend/utils/llm_factory.py +225 -0
  30. vibe_surf/browser/__init__.py +8 -0
  31. vibe_surf/browser/agen_browser_profile.py +130 -0
  32. vibe_surf/browser/agent_browser_session.py +416 -0
  33. vibe_surf/browser/browser_manager.py +296 -0
  34. vibe_surf/browser/utils.py +790 -0
  35. vibe_surf/browser/watchdogs/__init__.py +0 -0
  36. vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
  37. vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
  38. vibe_surf/chrome_extension/background.js +558 -0
  39. vibe_surf/chrome_extension/config.js +48 -0
  40. vibe_surf/chrome_extension/content.js +284 -0
  41. vibe_surf/chrome_extension/dev-reload.js +47 -0
  42. vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
  43. vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
  44. vibe_surf/chrome_extension/icons/logo.png +0 -0
  45. vibe_surf/chrome_extension/manifest.json +53 -0
  46. vibe_surf/chrome_extension/popup.html +134 -0
  47. vibe_surf/chrome_extension/scripts/api-client.js +473 -0
  48. vibe_surf/chrome_extension/scripts/main.js +491 -0
  49. vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
  50. vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
  51. vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
  52. vibe_surf/chrome_extension/sidepanel.html +347 -0
  53. vibe_surf/chrome_extension/styles/animations.css +471 -0
  54. vibe_surf/chrome_extension/styles/components.css +670 -0
  55. vibe_surf/chrome_extension/styles/main.css +2307 -0
  56. vibe_surf/chrome_extension/styles/settings.css +1100 -0
  57. vibe_surf/cli.py +357 -0
  58. vibe_surf/controller/__init__.py +0 -0
  59. vibe_surf/controller/file_system.py +53 -0
  60. vibe_surf/controller/mcp_client.py +68 -0
  61. vibe_surf/controller/vibesurf_controller.py +616 -0
  62. vibe_surf/controller/views.py +37 -0
  63. vibe_surf/llm/__init__.py +21 -0
  64. vibe_surf/llm/openai_compatible.py +237 -0
  65. vibesurf-0.1.0.dist-info/METADATA +97 -0
  66. vibesurf-0.1.0.dist-info/RECORD +70 -0
  67. vibesurf-0.1.0.dist-info/WHEEL +5 -0
  68. vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
  69. vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
  70. vibesurf-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,740 @@
1
+ """
2
+ Configuration API endpoints for VibeSurf Backend
3
+
4
+ Handles LLM Profile and controller configuration management.
5
+ """
6
+
7
+ from fastapi import APIRouter, HTTPException, Depends
8
+ from fastapi.responses import JSONResponse
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+ from typing import Dict, List
11
+ import logging
12
+
13
+ from ..database.manager import get_db_session
14
+ from ..database.queries import LLMProfileQueries, McpProfileQueries
15
+ from .models import (
16
+ LLMProfileCreateRequest, LLMProfileUpdateRequest, LLMProfileResponse,
17
+ McpProfileCreateRequest, McpProfileUpdateRequest, McpProfileResponse
18
+ )
19
+
20
+ router = APIRouter(prefix="/config", tags=["config"])
21
+ logger = logging.getLogger(__name__)
22
+
23
+ def _profile_to_response_dict(profile) -> dict:
24
+ """Convert SQLAlchemy LLMProfile to dict for Pydantic validation - safe extraction"""
25
+ try:
26
+ # Use SQLAlchemy's __dict__ to avoid lazy loading issues
27
+ profile_dict = profile.__dict__.copy()
28
+
29
+ # Remove SQLAlchemy internal keys
30
+ profile_dict.pop('_sa_instance_state', None)
31
+
32
+ return {
33
+ "profile_id": profile_dict.get("profile_id"),
34
+ "profile_name": profile_dict.get("profile_name"),
35
+ "provider": profile_dict.get("provider"),
36
+ "model": profile_dict.get("model"),
37
+ "base_url": profile_dict.get("base_url"),
38
+ "temperature": profile_dict.get("temperature"),
39
+ "max_tokens": profile_dict.get("max_tokens"),
40
+ "top_p": profile_dict.get("top_p"),
41
+ "frequency_penalty": profile_dict.get("frequency_penalty"),
42
+ "seed": profile_dict.get("seed"),
43
+ "provider_config": profile_dict.get("provider_config"),
44
+ "description": profile_dict.get("description"),
45
+ "is_active": profile_dict.get("is_active"),
46
+ "is_default": profile_dict.get("is_default"),
47
+ "created_at": profile_dict.get("created_at"),
48
+ "updated_at": profile_dict.get("updated_at"),
49
+ "last_used_at": profile_dict.get("last_used_at")
50
+ }
51
+ except Exception as e:
52
+ # Fallback to direct attribute access if __dict__ approach fails
53
+ return {
54
+ "profile_id": str(profile.profile_id),
55
+ "profile_name": str(profile.profile_name),
56
+ "provider": str(profile.provider),
57
+ "model": str(profile.model),
58
+ "base_url": profile.base_url,
59
+ "temperature": profile.temperature,
60
+ "max_tokens": profile.max_tokens,
61
+ "top_p": profile.top_p,
62
+ "frequency_penalty": profile.frequency_penalty,
63
+ "seed": profile.seed,
64
+ "provider_config": profile.provider_config or {},
65
+ "description": profile.description,
66
+ "is_active": bool(profile.is_active),
67
+ "is_default": bool(profile.is_default),
68
+ "created_at": profile.created_at,
69
+ "updated_at": profile.updated_at,
70
+ "last_used_at": profile.last_used_at
71
+ }
72
+
73
+ # LLM Profile Management
74
+ @router.post("/llm-profiles", response_model=LLMProfileResponse)
75
+ async def create_llm_profile(
76
+ profile_request: LLMProfileCreateRequest,
77
+ db: AsyncSession = Depends(get_db_session)
78
+ ):
79
+ """Create a new LLM profile"""
80
+ try:
81
+ # Check if profile name already exists
82
+ existing_profile = await LLMProfileQueries.get_profile(db, profile_request.profile_name)
83
+ if existing_profile:
84
+ raise HTTPException(
85
+ status_code=400,
86
+ detail=f"Profile with name '{profile_request.profile_name}' already exists"
87
+ )
88
+
89
+ # Create new profile - now returns dict directly
90
+ profile_data = await LLMProfileQueries.create_profile(
91
+ db=db,
92
+ profile_name=profile_request.profile_name,
93
+ provider=profile_request.provider,
94
+ model=profile_request.model,
95
+ api_key=profile_request.api_key,
96
+ base_url=profile_request.base_url,
97
+ temperature=profile_request.temperature,
98
+ max_tokens=profile_request.max_tokens,
99
+ top_p=profile_request.top_p,
100
+ frequency_penalty=profile_request.frequency_penalty,
101
+ seed=profile_request.seed,
102
+ provider_config=profile_request.provider_config,
103
+ description=profile_request.description,
104
+ is_default=profile_request.is_default
105
+ )
106
+
107
+ await db.commit()
108
+
109
+ # If this is set as default, update other profiles
110
+ if profile_request.is_default:
111
+ await LLMProfileQueries.set_default_profile(db, profile_request.profile_name)
112
+ await db.commit()
113
+
114
+ return LLMProfileResponse(**profile_data)
115
+
116
+ except Exception as e:
117
+ logger.error(f"Failed to create LLM profile: {e}")
118
+ raise HTTPException(
119
+ status_code=500,
120
+ detail=f"Failed to create LLM profile: {str(e)}"
121
+ )
122
+
123
+ @router.get("/llm-profiles", response_model=List[LLMProfileResponse])
124
+ async def list_llm_profiles(
125
+ active_only: bool = True,
126
+ limit: int = 50,
127
+ offset: int = 0,
128
+ db: AsyncSession = Depends(get_db_session)
129
+ ):
130
+ """List LLM profiles"""
131
+ try:
132
+ profiles = await LLMProfileQueries.list_profiles(
133
+ db=db,
134
+ active_only=active_only,
135
+ limit=limit,
136
+ offset=offset
137
+ )
138
+
139
+ # Use safe extraction to avoid greenlet issues
140
+ return [LLMProfileResponse(**_profile_to_response_dict(profile)) for profile in profiles]
141
+
142
+ except Exception as e:
143
+ logger.error(f"Failed to list LLM profiles: {e}")
144
+ raise HTTPException(
145
+ status_code=500,
146
+ detail=f"Failed to list LLM profiles: {str(e)}"
147
+ )
148
+
149
+ @router.get("/llm-profiles/{profile_name}", response_model=LLMProfileResponse)
150
+ async def get_llm_profile(
151
+ profile_name: str,
152
+ db: AsyncSession = Depends(get_db_session)
153
+ ):
154
+ """Get specific LLM profile by name"""
155
+ try:
156
+ profile = await LLMProfileQueries.get_profile(db, profile_name)
157
+ if not profile:
158
+ raise HTTPException(
159
+ status_code=404,
160
+ detail=f"LLM profile '{profile_name}' not found"
161
+ )
162
+
163
+ # Use safe extraction to avoid greenlet issues
164
+ return LLMProfileResponse(**_profile_to_response_dict(profile))
165
+
166
+ except HTTPException:
167
+ raise
168
+ except Exception as e:
169
+ logger.error(f"Failed to get LLM profile: {e}")
170
+ raise HTTPException(
171
+ status_code=500,
172
+ detail=f"Failed to get LLM profile: {str(e)}"
173
+ )
174
+
175
+ @router.put("/llm-profiles/{profile_name}", response_model=LLMProfileResponse)
176
+ async def update_llm_profile(
177
+ profile_name: str,
178
+ update_request: LLMProfileUpdateRequest,
179
+ db: AsyncSession = Depends(get_db_session)
180
+ ):
181
+ """Update an existing LLM profile"""
182
+ try:
183
+ # Check if profile exists
184
+ existing_profile = await LLMProfileQueries.get_profile(db, profile_name)
185
+ if not existing_profile:
186
+ raise HTTPException(
187
+ status_code=404,
188
+ detail=f"LLM profile '{profile_name}' not found"
189
+ )
190
+
191
+ # Prepare update data
192
+ update_data = {}
193
+ for field, value in update_request.model_dump(exclude_unset=True).items():
194
+ if value is not None:
195
+ update_data[field] = value
196
+
197
+ if not update_data:
198
+ raise HTTPException(
199
+ status_code=400,
200
+ detail="No valid fields provided for update"
201
+ )
202
+
203
+ # Update profile
204
+ success = await LLMProfileQueries.update_profile(db, profile_name, update_data)
205
+ if not success:
206
+ raise HTTPException(
207
+ status_code=500,
208
+ detail="Failed to update profile"
209
+ )
210
+
211
+ await db.commit()
212
+
213
+ # Handle default profile setting
214
+ if update_request.is_default:
215
+ await LLMProfileQueries.set_default_profile(db, profile_name)
216
+ await db.commit()
217
+
218
+ # Return updated profile
219
+ updated_profile = await LLMProfileQueries.get_profile(db, profile_name)
220
+
221
+ # Use safe extraction to avoid greenlet issues
222
+ return LLMProfileResponse(**_profile_to_response_dict(updated_profile))
223
+
224
+ except HTTPException:
225
+ raise
226
+ except Exception as e:
227
+ logger.error(f"Failed to update LLM profile: {e}")
228
+ raise HTTPException(
229
+ status_code=500,
230
+ detail=f"Failed to update LLM profile: {str(e)}"
231
+ )
232
+
233
+ @router.delete("/llm-profiles/{profile_name}")
234
+ async def delete_llm_profile(
235
+ profile_name: str,
236
+ db: AsyncSession = Depends(get_db_session)
237
+ ):
238
+ """Delete an LLM profile"""
239
+ try:
240
+ # Check if profile exists
241
+ existing_profile = await LLMProfileQueries.get_profile(db, profile_name)
242
+ if not existing_profile:
243
+ raise HTTPException(
244
+ status_code=404,
245
+ detail=f"LLM profile '{profile_name}' not found"
246
+ )
247
+
248
+ # Don't allow deletion of default profile
249
+ if existing_profile.is_default:
250
+ raise HTTPException(
251
+ status_code=400,
252
+ detail="Cannot delete the default profile. Set another profile as default first."
253
+ )
254
+
255
+ # TODO: Check if profile is being used by any active tasks
256
+ # This would require checking the tasks table
257
+
258
+ success = await LLMProfileQueries.delete_profile(db, profile_name)
259
+ if not success:
260
+ raise HTTPException(
261
+ status_code=500,
262
+ detail="Failed to delete profile"
263
+ )
264
+
265
+ await db.commit()
266
+
267
+ return JSONResponse(
268
+ content={"message": f"LLM profile '{profile_name}' deleted successfully"},
269
+ status_code=200
270
+ )
271
+
272
+ except HTTPException:
273
+ raise
274
+ except Exception as e:
275
+ logger.error(f"Failed to delete LLM profile: {e}")
276
+ raise HTTPException(
277
+ status_code=500,
278
+ detail=f"Failed to delete LLM profile: {str(e)}"
279
+ )
280
+
281
+ @router.post("/llm-profiles/{profile_name}/set-default")
282
+ async def set_default_llm_profile(
283
+ profile_name: str,
284
+ db: AsyncSession = Depends(get_db_session)
285
+ ):
286
+ """Set an LLM profile as the default"""
287
+ try:
288
+ # Check if profile exists and is active
289
+ profile = await LLMProfileQueries.get_profile(db, profile_name)
290
+ if not profile:
291
+ raise HTTPException(
292
+ status_code=404,
293
+ detail=f"LLM profile '{profile_name}' not found"
294
+ )
295
+
296
+ if not profile.is_active:
297
+ raise HTTPException(
298
+ status_code=400,
299
+ detail="Cannot set inactive profile as default"
300
+ )
301
+
302
+ success = await LLMProfileQueries.set_default_profile(db, profile_name)
303
+ if not success:
304
+ raise HTTPException(
305
+ status_code=500,
306
+ detail="Failed to set default profile"
307
+ )
308
+
309
+ await db.commit()
310
+
311
+ return JSONResponse(
312
+ content={"message": f"LLM profile '{profile_name}' set as default"},
313
+ status_code=200
314
+ )
315
+
316
+ except HTTPException:
317
+ raise
318
+ except Exception as e:
319
+ logger.error(f"Failed to set default LLM profile: {e}")
320
+ raise HTTPException(
321
+ status_code=500,
322
+ detail=f"Failed to set default LLM profile: {str(e)}"
323
+ )
324
+
325
+ @router.get("/llm-profiles/default/current", response_model=LLMProfileResponse)
326
+ async def get_default_llm_profile(db: AsyncSession = Depends(get_db_session)):
327
+ """Get the current default LLM profile"""
328
+ try:
329
+ profile = await LLMProfileQueries.get_default_profile(db)
330
+ if not profile:
331
+ raise HTTPException(
332
+ status_code=404,
333
+ detail="No default LLM profile found"
334
+ )
335
+
336
+ # Use safe extraction to avoid greenlet issues
337
+ return LLMProfileResponse(**_profile_to_response_dict(profile))
338
+
339
+ except HTTPException:
340
+ raise
341
+ except Exception as e:
342
+ logger.error(f"Failed to get default LLM profile: {e}")
343
+ raise HTTPException(
344
+ status_code=500,
345
+ detail=f"Failed to get default LLM profile: {str(e)}"
346
+ )
347
+
348
+ # MCP Profile Management
349
+ def _mcp_profile_to_response_dict(profile) -> dict:
350
+ """Convert SQLAlchemy McpProfile to dict for Pydantic validation - safe extraction"""
351
+ try:
352
+ # Use SQLAlchemy's __dict__ to avoid lazy loading issues
353
+ profile_dict = profile.__dict__.copy()
354
+
355
+ # Remove SQLAlchemy internal keys
356
+ profile_dict.pop('_sa_instance_state', None)
357
+
358
+ return {
359
+ "mcp_id": profile_dict.get("mcp_id"),
360
+ "display_name": profile_dict.get("display_name"),
361
+ "mcp_server_name": profile_dict.get("mcp_server_name"),
362
+ "mcp_server_params": profile_dict.get("mcp_server_params"),
363
+ "description": profile_dict.get("description"),
364
+ "is_active": profile_dict.get("is_active"),
365
+ "created_at": profile_dict.get("created_at"),
366
+ "updated_at": profile_dict.get("updated_at"),
367
+ "last_used_at": profile_dict.get("last_used_at")
368
+ }
369
+ except Exception as e:
370
+ # Fallback to direct attribute access if __dict__ approach fails
371
+ return {
372
+ "mcp_id": str(profile.mcp_id),
373
+ "display_name": str(profile.display_name),
374
+ "mcp_server_name": str(profile.mcp_server_name),
375
+ "mcp_server_params": profile.mcp_server_params or {},
376
+ "description": profile.description,
377
+ "is_active": bool(profile.is_active),
378
+ "created_at": profile.created_at,
379
+ "updated_at": profile.updated_at,
380
+ "last_used_at": profile.last_used_at
381
+ }
382
+
383
+ @router.post("/mcp-profiles", response_model=McpProfileResponse)
384
+ async def create_mcp_profile(
385
+ profile_request: McpProfileCreateRequest,
386
+ db: AsyncSession = Depends(get_db_session)
387
+ ):
388
+ """Create a new MCP profile"""
389
+ try:
390
+ # Check if display name already exists
391
+ existing_profile = await McpProfileQueries.get_profile_by_display_name(db, profile_request.display_name)
392
+ if existing_profile:
393
+ raise HTTPException(
394
+ status_code=400,
395
+ detail=f"MCP Profile with display name '{profile_request.display_name}' already exists"
396
+ )
397
+
398
+ # Create new profile
399
+ profile_data = await McpProfileQueries.create_profile(
400
+ db=db,
401
+ display_name=profile_request.display_name,
402
+ mcp_server_name=profile_request.mcp_server_name,
403
+ mcp_server_params=profile_request.mcp_server_params,
404
+ description=profile_request.description
405
+ )
406
+
407
+ await db.commit()
408
+
409
+ return McpProfileResponse(**profile_data)
410
+
411
+ except Exception as e:
412
+ logger.error(f"Failed to create MCP profile: {e}")
413
+ raise HTTPException(
414
+ status_code=500,
415
+ detail=f"Failed to create MCP profile: {str(e)}"
416
+ )
417
+
418
+ @router.get("/mcp-profiles", response_model=List[McpProfileResponse])
419
+ async def list_mcp_profiles(
420
+ active_only: bool = True,
421
+ limit: int = 50,
422
+ offset: int = 0,
423
+ db: AsyncSession = Depends(get_db_session)
424
+ ):
425
+ """List MCP profiles"""
426
+ try:
427
+ profiles = await McpProfileQueries.list_profiles(
428
+ db=db,
429
+ active_only=active_only,
430
+ limit=limit,
431
+ offset=offset
432
+ )
433
+
434
+ return [McpProfileResponse(**_mcp_profile_to_response_dict(profile)) for profile in profiles]
435
+
436
+ except Exception as e:
437
+ logger.error(f"Failed to list MCP profiles: {e}")
438
+ raise HTTPException(
439
+ status_code=500,
440
+ detail=f"Failed to list MCP profiles: {str(e)}"
441
+ )
442
+
443
+ @router.get("/mcp-profiles/{mcp_id}", response_model=McpProfileResponse)
444
+ async def get_mcp_profile(
445
+ mcp_id: str,
446
+ db: AsyncSession = Depends(get_db_session)
447
+ ):
448
+ """Get specific MCP profile by ID"""
449
+ try:
450
+ profile = await McpProfileQueries.get_profile(db, mcp_id)
451
+ if not profile:
452
+ raise HTTPException(
453
+ status_code=404,
454
+ detail=f"MCP profile '{mcp_id}' not found"
455
+ )
456
+
457
+ return McpProfileResponse(**_mcp_profile_to_response_dict(profile))
458
+
459
+ except HTTPException:
460
+ raise
461
+ except Exception as e:
462
+ logger.error(f"Failed to get MCP profile: {e}")
463
+ raise HTTPException(
464
+ status_code=500,
465
+ detail=f"Failed to get MCP profile: {str(e)}"
466
+ )
467
+
468
+ @router.put("/mcp-profiles/{mcp_id}", response_model=McpProfileResponse)
469
+ async def update_mcp_profile(
470
+ mcp_id: str,
471
+ update_request: McpProfileUpdateRequest,
472
+ db: AsyncSession = Depends(get_db_session)
473
+ ):
474
+ """Update an existing MCP profile"""
475
+ try:
476
+ logger.info(f"Updating MCP profile {mcp_id}")
477
+
478
+ # Check if profile exists
479
+ existing_profile = await McpProfileQueries.get_profile(db, mcp_id)
480
+ if not existing_profile:
481
+ raise HTTPException(
482
+ status_code=404,
483
+ detail=f"MCP profile '{mcp_id}' not found"
484
+ )
485
+
486
+ # Prepare update data
487
+ update_data = {}
488
+ for field, value in update_request.model_dump(exclude_unset=True).items():
489
+ if value is not None:
490
+ update_data[field] = value
491
+
492
+ if not update_data:
493
+ raise HTTPException(
494
+ status_code=400,
495
+ detail="No valid fields provided for update"
496
+ )
497
+
498
+ # Update profile
499
+ success = await McpProfileQueries.update_profile(db, mcp_id, update_data)
500
+
501
+ if not success:
502
+ raise HTTPException(
503
+ status_code=500,
504
+ detail="Failed to update profile"
505
+ )
506
+
507
+ await db.commit()
508
+
509
+ # Return updated profile
510
+ updated_profile = await McpProfileQueries.get_profile(db, mcp_id)
511
+ response_data = _mcp_profile_to_response_dict(updated_profile)
512
+
513
+ return McpProfileResponse(**response_data)
514
+
515
+ except HTTPException:
516
+ raise
517
+ except Exception as e:
518
+ logger.error(f"Failed to update MCP profile: {e}")
519
+ raise HTTPException(
520
+ status_code=500,
521
+ detail=f"Failed to update MCP profile: {str(e)}"
522
+ )
523
+
524
+ @router.delete("/mcp-profiles/{mcp_id}")
525
+ async def delete_mcp_profile(
526
+ mcp_id: str,
527
+ db: AsyncSession = Depends(get_db_session)
528
+ ):
529
+ """Delete an MCP profile"""
530
+ try:
531
+ # Check if profile exists
532
+ existing_profile = await McpProfileQueries.get_profile(db, mcp_id)
533
+ if not existing_profile:
534
+ raise HTTPException(
535
+ status_code=404,
536
+ detail=f"MCP profile '{mcp_id}' not found"
537
+ )
538
+
539
+ # TODO: Check if profile is being used by any active tasks
540
+ # This would require checking the tasks table
541
+
542
+ success = await McpProfileQueries.delete_profile(db, mcp_id)
543
+ if not success:
544
+ raise HTTPException(
545
+ status_code=500,
546
+ detail="Failed to delete profile"
547
+ )
548
+
549
+ await db.commit()
550
+
551
+ return JSONResponse(
552
+ content={"message": f"MCP profile '{existing_profile.display_name}' deleted successfully"},
553
+ status_code=200
554
+ )
555
+
556
+ except HTTPException:
557
+ raise
558
+ except Exception as e:
559
+ logger.error(f"Failed to delete MCP profile: {e}")
560
+ raise HTTPException(
561
+ status_code=500,
562
+ detail=f"Failed to delete MCP profile: {str(e)}"
563
+ )
564
+
565
+ @router.get("/llm/providers")
566
+ async def get_available_providers():
567
+ """Get list of available LLM providers"""
568
+ from ..llm_config import get_supported_providers, get_provider_models, get_provider_metadata
569
+
570
+ providers = []
571
+ for provider_name in get_supported_providers():
572
+ metadata = get_provider_metadata(provider_name)
573
+ models = get_provider_models(provider_name)
574
+
575
+ provider_info = {
576
+ "name": provider_name,
577
+ "display_name": metadata.get("display_name", provider_name.title()),
578
+ "models": models,
579
+ "model_count": len(models),
580
+ "requires_api_key": metadata.get("requires_api_key", True),
581
+ "supports_base_url": metadata.get("supports_base_url", False),
582
+ "requires_base_url": metadata.get("requires_base_url", False),
583
+ "supports_tools": metadata.get("supports_tools", False),
584
+ "supports_vision": metadata.get("supports_vision", False),
585
+ "default_model": metadata.get("default_model", "")
586
+ }
587
+
588
+ # Add default base URL if available
589
+ if "default_base_url" in metadata:
590
+ provider_info["default_base_url"] = metadata["default_base_url"]
591
+ if "base_url" in metadata:
592
+ provider_info["base_url"] = metadata["base_url"]
593
+
594
+ providers.append(provider_info)
595
+
596
+ return {
597
+ "providers": providers,
598
+ "total_providers": len(providers)
599
+ }
600
+
601
+ @router.get("/llm/providers/{provider_name}/models")
602
+ async def get_provider_models_endpoint(provider_name: str):
603
+ """Get models for a specific LLM provider"""
604
+ from ..llm_config import get_provider_models as get_models, get_provider_metadata, is_provider_supported
605
+
606
+ if not is_provider_supported(provider_name):
607
+ raise HTTPException(
608
+ status_code=404,
609
+ detail=f"Provider '{provider_name}' not found or not supported"
610
+ )
611
+
612
+ models = get_models(provider_name)
613
+ metadata = get_provider_metadata(provider_name)
614
+
615
+ return {
616
+ "provider": provider_name,
617
+ "display_name": metadata.get("display_name", provider_name.title()),
618
+ "models": models,
619
+ "model_count": len(models),
620
+ "default_model": metadata.get("default_model", ""),
621
+ "metadata": metadata
622
+ }
623
+
624
+ # Configuration status endpoints
625
+ @router.get("/status")
626
+ async def get_configuration_status(db: AsyncSession = Depends(get_db_session)):
627
+ """Get overall configuration status including LLM profiles"""
628
+ try:
629
+ from .. import shared_state
630
+
631
+ # Get LLM profiles info
632
+ total_profiles = len(await LLMProfileQueries.list_profiles(db, active_only=False))
633
+ active_profiles = len(await LLMProfileQueries.list_profiles(db, active_only=True))
634
+ default_profile = await LLMProfileQueries.get_default_profile(db)
635
+
636
+ status = {
637
+ "llm_profiles": {
638
+ "total_profiles": total_profiles,
639
+ "active_profiles": active_profiles,
640
+ "default_profile": default_profile.profile_name if default_profile else None,
641
+ "has_default": default_profile is not None
642
+ },
643
+ "controller": {
644
+ "initialized": shared_state.controller is not None
645
+ },
646
+ "browser_manager": {
647
+ "initialized": shared_state.browser_manager is not None
648
+ },
649
+ "vibesurf_agent": {
650
+ "initialized": shared_state.vibesurf_agent is not None,
651
+ "workspace_dir": shared_state.workspace_dir
652
+ },
653
+ "overall_status": "ready" if (
654
+ default_profile and
655
+ shared_state.controller and
656
+ shared_state.browser_manager and
657
+ shared_state.vibesurf_agent
658
+ ) else "partial"
659
+ }
660
+
661
+ return status
662
+
663
+ except Exception as e:
664
+ logger.error(f"Failed to get configuration status: {e}")
665
+ raise HTTPException(
666
+ status_code=500,
667
+ detail=f"Failed to get configuration status: {str(e)}"
668
+ )
669
+
670
+ # Environment Variables Management
671
+ @router.get("/environments")
672
+ async def get_environments():
673
+ """Get current environment variables"""
674
+ try:
675
+ from .. import shared_state
676
+ envs = shared_state.get_envs()
677
+
678
+ return {
679
+ "environments": envs,
680
+ "count": len(envs)
681
+ }
682
+
683
+ except Exception as e:
684
+ logger.error(f"Failed to get environments: {e}")
685
+ raise HTTPException(
686
+ status_code=500,
687
+ detail=f"Failed to get environments: {str(e)}"
688
+ )
689
+
690
+ @router.put("/environments")
691
+ async def update_environments(updates: Dict[str, str]):
692
+ """Update environment variables"""
693
+ try:
694
+ from .. import shared_state
695
+
696
+ # Validate that we only update allowed keys
697
+ allowed_keys = {
698
+ "BROWSER_EXECUTION_PATH",
699
+ "BROWSER_USER_DATA",
700
+ "VIBESURF_EXTENSION",
701
+ "VIBESURF_BACKEND_URL"
702
+ }
703
+
704
+ # Filter updates to only include allowed keys
705
+ filtered_updates = {
706
+ key: value for key, value in updates.items()
707
+ if key in allowed_keys
708
+ }
709
+
710
+ if not filtered_updates:
711
+ raise HTTPException(
712
+ status_code=400,
713
+ detail=f"No valid environment variables provided. Allowed keys: {list(allowed_keys)}"
714
+ )
715
+
716
+ # Update environment variables
717
+ success = shared_state.update_envs(filtered_updates)
718
+ if not success:
719
+ raise HTTPException(
720
+ status_code=500,
721
+ detail="Failed to update environment variables"
722
+ )
723
+
724
+ # Return updated environments
725
+ updated_envs = shared_state.get_envs()
726
+
727
+ return {
728
+ "message": "Environment variables updated successfully",
729
+ "updated_keys": list(filtered_updates.keys()),
730
+ "environments": updated_envs
731
+ }
732
+
733
+ except HTTPException:
734
+ raise
735
+ except Exception as e:
736
+ logger.error(f"Failed to update environments: {e}")
737
+ raise HTTPException(
738
+ status_code=500,
739
+ detail=f"Failed to update environments: {str(e)}"
740
+ )