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,134 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VibeSurf</title>
7
+ <style>
8
+ body {
9
+ width: 300px;
10
+ height: 200px;
11
+ margin: 0;
12
+ padding: 20px;
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ color: white;
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ text-align: center;
21
+ }
22
+
23
+ .logo {
24
+ font-size: 32px;
25
+ margin-bottom: 10px;
26
+ }
27
+
28
+ h1 {
29
+ font-size: 24px;
30
+ margin: 0 0 10px 0;
31
+ font-weight: 600;
32
+ }
33
+
34
+ p {
35
+ font-size: 14px;
36
+ margin: 0 0 20px 0;
37
+ opacity: 0.9;
38
+ line-height: 1.4;
39
+ }
40
+
41
+ .open-panel-btn {
42
+ background: rgba(255, 255, 255, 0.2);
43
+ border: 1px solid rgba(255, 255, 255, 0.3);
44
+ color: white;
45
+ padding: 10px 20px;
46
+ border-radius: 6px;
47
+ cursor: pointer;
48
+ font-size: 14px;
49
+ font-weight: 500;
50
+ transition: all 0.2s ease;
51
+ }
52
+
53
+ .open-panel-btn:hover {
54
+ background: rgba(255, 255, 255, 0.3);
55
+ border-color: rgba(255, 255, 255, 0.5);
56
+ }
57
+
58
+ .status {
59
+ position: absolute;
60
+ bottom: 10px;
61
+ right: 10px;
62
+ font-size: 12px;
63
+ opacity: 0.7;
64
+ }
65
+
66
+ .status-dot {
67
+ width: 8px;
68
+ height: 8px;
69
+ border-radius: 50%;
70
+ background: #28a745;
71
+ display: inline-block;
72
+ margin-right: 5px;
73
+ }
74
+
75
+ .status-dot.disconnected {
76
+ background: #dc3545;
77
+ }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <div class="logo">🌊</div>
82
+ <h1>VibeSurf</h1>
83
+ <p>AI-powered browsing automation at your fingertips</p>
84
+ <button class="open-panel-btn" id="openPanelBtn">Open Side Panel</button>
85
+
86
+ <div class="status">
87
+ <span class="status-dot" id="statusDot"></span>
88
+ <span id="statusText">Ready</span>
89
+ </div>
90
+
91
+ <script>
92
+ document.addEventListener('DOMContentLoaded', async () => {
93
+ const openPanelBtn = document.getElementById('openPanelBtn');
94
+ const statusDot = document.getElementById('statusDot');
95
+ const statusText = document.getElementById('statusText');
96
+
97
+ // Handle open panel button click
98
+ openPanelBtn.addEventListener('click', async () => {
99
+ try {
100
+ // Get current tab
101
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
102
+
103
+ // Open side panel
104
+ await chrome.sidePanel.open({ tabId: tab.id });
105
+
106
+ // Close popup
107
+ window.close();
108
+ } catch (error) {
109
+ console.error('Failed to open side panel:', error);
110
+ statusText.textContent = 'Error';
111
+ statusDot.classList.add('disconnected');
112
+ }
113
+ });
114
+
115
+ // Check backend status
116
+ try {
117
+ const response = await chrome.runtime.sendMessage({ type: 'GET_BACKEND_STATUS' });
118
+
119
+ if (response.status === 'connected') {
120
+ statusText.textContent = 'Connected';
121
+ statusDot.classList.remove('disconnected');
122
+ } else {
123
+ statusText.textContent = 'Disconnected';
124
+ statusDot.classList.add('disconnected');
125
+ }
126
+ } catch (error) {
127
+ console.error('Failed to check backend status:', error);
128
+ statusText.textContent = 'Error';
129
+ statusDot.classList.add('disconnected');
130
+ }
131
+ });
132
+ </script>
133
+ </body>
134
+ </html>
@@ -0,0 +1,473 @@
1
+ // API Client - VibeSurf Backend Communication
2
+ // Handles all HTTP requests to the VibeSurf backend API
3
+
4
+ class VibeSurfAPIClient {
5
+ constructor(baseURL = null) {
6
+ // Use configuration file values as defaults
7
+ const config = window.VIBESURF_CONFIG || {};
8
+ this.baseURL = (baseURL || config.BACKEND_URL || 'http://localhost:9335').replace(/\/$/, ''); // Remove trailing slash
9
+ this.apiPrefix = config.API_PREFIX || '/api';
10
+ this.timeout = config.DEFAULT_TIMEOUT || 30000;
11
+ this.retryAttempts = config.RETRY_ATTEMPTS || 3;
12
+ this.retryDelay = config.RETRY_DELAY || 1000;
13
+ }
14
+
15
+ // Utility method to build full URL
16
+ buildURL(endpoint) {
17
+ const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
18
+ return `${this.baseURL}${this.apiPrefix}${cleanEndpoint}`;
19
+ }
20
+
21
+ // Generic HTTP request method with error handling and retries
22
+ async request(method, endpoint, options = {}) {
23
+ const {
24
+ data,
25
+ params,
26
+ headers = {},
27
+ timeout = this.timeout,
28
+ retries = this.retryAttempts,
29
+ ...fetchOptions
30
+ } = options;
31
+
32
+ const url = new URL(this.buildURL(endpoint));
33
+
34
+ // Add query parameters
35
+ if (params) {
36
+ Object.keys(params).forEach(key => {
37
+ if (params[key] !== undefined && params[key] !== null) {
38
+ url.searchParams.append(key, params[key]);
39
+ }
40
+ });
41
+ }
42
+
43
+ const config = {
44
+ method,
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...headers
48
+ },
49
+ signal: AbortSignal.timeout(timeout),
50
+ ...fetchOptions
51
+ };
52
+
53
+ // Add body for POST/PUT requests
54
+ if (data && method !== 'GET') {
55
+ if (data instanceof FormData) {
56
+ // Remove Content-Type for FormData (browser will set it with boundary)
57
+ delete config.headers['Content-Type'];
58
+ config.body = data;
59
+ } else {
60
+ config.body = JSON.stringify(data);
61
+ }
62
+ }
63
+
64
+ let lastError;
65
+
66
+ for (let attempt = 0; attempt <= retries; attempt++) {
67
+ try {
68
+ console.log(`[API] ${method} ${url} (attempt ${attempt + 1}/${retries + 1})`);
69
+
70
+ const response = await fetch(url, config);
71
+
72
+ // Handle different response types
73
+ const contentType = response.headers.get('content-type');
74
+ let responseData;
75
+
76
+ if (contentType && contentType.includes('application/json')) {
77
+ responseData = await response.json();
78
+ } else {
79
+ responseData = await response.text();
80
+ }
81
+
82
+ if (!response.ok) {
83
+ throw new APIError(
84
+ responseData.detail || responseData.message || `HTTP ${response.status}`,
85
+ response.status,
86
+ responseData
87
+ );
88
+ }
89
+
90
+ console.log(`[API] ${method} ${url} - Success`);
91
+ return responseData;
92
+
93
+ } catch (error) {
94
+ lastError = error;
95
+ console.error(`[API] ${method} ${url} - Error (attempt ${attempt + 1}):`, error);
96
+
97
+ // Don't retry on certain errors
98
+ if (error instanceof APIError) {
99
+ if (error.status >= 400 && error.status < 500) {
100
+ throw error; // Client errors shouldn't be retried
101
+ }
102
+ }
103
+
104
+ // Don't retry on timeout for the last attempt
105
+ if (attempt === retries) {
106
+ break;
107
+ }
108
+
109
+ // Wait before retry
110
+ await this.delay(this.retryDelay * (attempt + 1));
111
+ }
112
+ }
113
+
114
+ throw lastError;
115
+ }
116
+
117
+ // HTTP method helpers
118
+ async get(endpoint, options = {}) {
119
+ return this.request('GET', endpoint, options);
120
+ }
121
+
122
+ async post(endpoint, data, options = {}) {
123
+ return this.request('POST', endpoint, { data, ...options });
124
+ }
125
+
126
+ async put(endpoint, data, options = {}) {
127
+ return this.request('PUT', endpoint, { data, ...options });
128
+ }
129
+
130
+ async delete(endpoint, options = {}) {
131
+ return this.request('DELETE', endpoint, options);
132
+ }
133
+
134
+ // Health check - special method that bypasses API prefix
135
+ async healthCheck() {
136
+ try {
137
+ // Build URL without API prefix for health endpoint
138
+ const url = `${this.baseURL}/health`;
139
+ const response = await fetch(url, {
140
+ method: 'GET',
141
+ headers: {
142
+ 'Content-Type': 'application/json'
143
+ },
144
+ signal: AbortSignal.timeout(5000)
145
+ });
146
+
147
+ if (!response.ok) {
148
+ throw new Error(`HTTP ${response.status}`);
149
+ }
150
+
151
+ const data = await response.json();
152
+ console.log('[API] Health check - Success');
153
+ return { status: 'healthy', data };
154
+ } catch (error) {
155
+ console.error('[API] Health check - Error:', error);
156
+ return { status: 'unhealthy', error: error.message };
157
+ }
158
+ }
159
+
160
+ // System status
161
+ async getSystemStatus() {
162
+ return this.get('/status');
163
+ }
164
+
165
+ // Task Management APIs
166
+ async submitTask(taskData) {
167
+ const {
168
+ session_id,
169
+ task_description,
170
+ llm_profile_name,
171
+ upload_files_path,
172
+ mcp_server_config
173
+ } = taskData;
174
+
175
+ return this.post('/tasks/submit', {
176
+ session_id,
177
+ task_description,
178
+ llm_profile_name,
179
+ upload_files_path,
180
+ mcp_server_config
181
+ });
182
+ }
183
+
184
+ async getTaskStatus() {
185
+ return this.get('/tasks/status');
186
+ }
187
+
188
+ async checkTaskRunning() {
189
+ try {
190
+ const status = await this.getTaskStatus();
191
+
192
+ console.log('[API] Task status check result:', {
193
+ has_active_task: status.has_active_task,
194
+ active_task: status.active_task
195
+ });
196
+
197
+ // Check if there's an active task and its status
198
+ const hasActiveTask = status.has_active_task;
199
+ const activeTask = status.active_task;
200
+
201
+ if (!hasActiveTask || !activeTask) {
202
+ return { isRunning: false, taskInfo: null };
203
+ }
204
+
205
+ // Check if the active task is in a "running" state
206
+ const runningStates = ['running', 'submitted', 'paused'];
207
+ const taskStatus = activeTask.status || '';
208
+ const isRunning = runningStates.includes(taskStatus.toLowerCase());
209
+
210
+ console.log('[API] Task running check:', {
211
+ taskStatus,
212
+ isRunning,
213
+ runningStates
214
+ });
215
+
216
+ return {
217
+ isRunning,
218
+ taskInfo: hasActiveTask ? activeTask : null
219
+ };
220
+ } catch (error) {
221
+ console.error('[API] Failed to check task status:', error);
222
+ return { isRunning: false, taskInfo: null };
223
+ }
224
+ }
225
+
226
+ async pauseTask(reason = 'User requested pause') {
227
+ return this.post('/tasks/pause', { reason });
228
+ }
229
+
230
+ async resumeTask(reason = 'User requested resume') {
231
+ return this.post('/tasks/resume', { reason });
232
+ }
233
+
234
+ async stopTask(reason = 'User requested stop') {
235
+ return this.post('/tasks/stop', { reason });
236
+ }
237
+
238
+ // Activity APIs
239
+ async getTaskInfo(taskId) {
240
+ return this.get(`/activity/${taskId}`);
241
+ }
242
+
243
+ async getSessionTasks(sessionId) {
244
+ return this.get(`/activity/sessions/${sessionId}/tasks`);
245
+ }
246
+
247
+ async getSessionActivity(sessionId, params = {}) {
248
+ return this.get(`/activity/sessions/${sessionId}/activity`, { params });
249
+ }
250
+
251
+ async getLatestActivity(sessionId) {
252
+ return this.get(`/activity/sessions/${sessionId}/latest_activity`);
253
+ }
254
+
255
+ async getRecentTasks(limit = -1) {
256
+ return this.get('/activity/tasks', { params: { limit } });
257
+ }
258
+
259
+ async getAllSessions(limit = -1, offset = 0) {
260
+ return this.get('/activity/sessions', { params: { limit, offset } });
261
+ }
262
+
263
+ // Real-time activity polling
264
+ async pollSessionActivity(sessionId, messageIndex = null, interval = 1000) {
265
+ const params = messageIndex !== null ? { message_index: messageIndex } : {};
266
+
267
+ console.log(`[API] Polling session activity:`, {
268
+ sessionId,
269
+ messageIndex,
270
+ params,
271
+ url: `/activity/sessions/${sessionId}/activity`
272
+ });
273
+
274
+ try {
275
+ const response = await this.getSessionActivity(sessionId, params);
276
+ console.log(`[API] Poll response:`, {
277
+ hasActivityLog: !!response.activity_log,
278
+ messageIndex: response.message_index,
279
+ agentName: response.activity_log?.agent_name,
280
+ agentStatus: response.activity_log?.agent_status
281
+ });
282
+ return response;
283
+ } catch (error) {
284
+ console.error('[API] Activity polling error:', error);
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ // File Management APIs
290
+ async uploadFiles(files, sessionId = null) {
291
+ const formData = new FormData();
292
+
293
+ // Add files
294
+ for (const file of files) {
295
+ formData.append('files', file);
296
+ }
297
+
298
+ // Add session ID if provided
299
+ if (sessionId) {
300
+ formData.append('session_id', sessionId);
301
+ }
302
+
303
+ return this.post('/files/upload', formData);
304
+ }
305
+
306
+ async listFiles(sessionId = null, limit = 50, offset = 0) {
307
+ const params = { limit, offset };
308
+ if (sessionId) {
309
+ params.session_id = sessionId;
310
+ }
311
+
312
+ return this.get('/files', { params });
313
+ }
314
+
315
+ async downloadFile(fileId) {
316
+ const url = this.buildURL(`/files/${fileId}`);
317
+
318
+ try {
319
+ const response = await fetch(url);
320
+ if (!response.ok) {
321
+ throw new Error(`Failed to download file: ${response.status}`);
322
+ }
323
+
324
+ return response.blob();
325
+ } catch (error) {
326
+ console.error('[API] File download error:', error);
327
+ throw error;
328
+ }
329
+ }
330
+
331
+ async deleteFile(fileId) {
332
+ return this.delete(`/files/${fileId}`);
333
+ }
334
+
335
+ // Configuration APIs
336
+ async getConfigStatus() {
337
+ return this.get('/config/status');
338
+ }
339
+
340
+ // LLM Profile Management
341
+ async getLLMProfiles(activeOnly = true, limit = 50, offset = 0) {
342
+ return this.get('/config/llm-profiles', {
343
+ params: { active_only: activeOnly, limit, offset }
344
+ });
345
+ }
346
+
347
+ async getLLMProfile(profileName) {
348
+ return this.get(`/config/llm-profiles/${encodeURIComponent(profileName)}`);
349
+ }
350
+
351
+ async createLLMProfile(profileData) {
352
+ return this.post('/config/llm-profiles', profileData);
353
+ }
354
+
355
+ async updateLLMProfile(profileName, updateData) {
356
+ return this.put(`/config/llm-profiles/${encodeURIComponent(profileName)}`, updateData);
357
+ }
358
+
359
+ async deleteLLMProfile(profileName) {
360
+ return this.delete(`/config/llm-profiles/${encodeURIComponent(profileName)}`);
361
+ }
362
+
363
+ async setDefaultLLMProfile(profileName) {
364
+ return this.post(`/config/llm-profiles/${encodeURIComponent(profileName)}/set-default`);
365
+ }
366
+
367
+ // MCP Profile Management
368
+ async getMCPProfiles(activeOnly = true, limit = 50, offset = 0) {
369
+ return this.get('/config/mcp-profiles', {
370
+ params: { active_only: activeOnly, limit, offset }
371
+ });
372
+ }
373
+
374
+ async getMCPProfile(profileName) {
375
+ return this.get(`/config/mcp-profiles/${encodeURIComponent(profileName)}`);
376
+ }
377
+
378
+ async createMCPProfile(profileData) {
379
+ return this.post('/config/mcp-profiles', profileData);
380
+ }
381
+
382
+ async updateMCPProfile(profileName, updateData) {
383
+ console.log('[API Client] updateMCPProfile called with profile:', profileName);
384
+ const result = await this.put(`/config/mcp-profiles/${encodeURIComponent(profileName)}`, updateData);
385
+ return result;
386
+ }
387
+
388
+ async deleteMCPProfile(profileName) {
389
+ return this.delete(`/config/mcp-profiles/${encodeURIComponent(profileName)}`);
390
+ }
391
+
392
+ // LLM Providers and Models
393
+ async getLLMProviders() {
394
+ return this.get('/config/llm/providers');
395
+ }
396
+
397
+ async getLLMProviderModels(providerName) {
398
+ return this.get(`/config/llm/providers/${encodeURIComponent(providerName)}/models`);
399
+ }
400
+
401
+ // Environment Variables
402
+ async getEnvironmentVariables() {
403
+ return this.get('/config/environments');
404
+ }
405
+
406
+ async updateEnvironmentVariables(variables) {
407
+ return this.put('/config/environments', { environments: variables });
408
+ }
409
+
410
+ // Controller Configuration
411
+ async getControllerConfig() {
412
+ return this.get('/config/controller');
413
+ }
414
+
415
+ async updateControllerConfig(configData) {
416
+ return this.put('/config/controller', configData);
417
+ }
418
+
419
+ // Utility methods
420
+ delay(ms) {
421
+ return new Promise(resolve => setTimeout(resolve, ms));
422
+ }
423
+
424
+ // Update base URL
425
+ setBaseURL(newBaseURL) {
426
+ this.baseURL = newBaseURL.replace(/\/$/, '');
427
+ console.log('[API] Base URL updated to:', this.baseURL);
428
+ }
429
+
430
+ // Create a session ID
431
+ // Session ID generation using backend endpoint with fallback
432
+ async generateSessionId(prefix = 'vibesurf_') {
433
+ try {
434
+ // Use backend endpoint for session ID generation
435
+ const response = await fetch(`${this.baseURL}/generate-session-id?prefix=${encodeURIComponent(prefix)}`, {
436
+ method: 'GET',
437
+ headers: {
438
+ 'Content-Type': 'application/json'
439
+ },
440
+ signal: AbortSignal.timeout(5000)
441
+ });
442
+
443
+ if (!response.ok) {
444
+ throw new Error(`HTTP ${response.status}`);
445
+ }
446
+
447
+ const data = await response.json();
448
+ return data.session_id;
449
+ } catch (error) {
450
+ console.warn('[API] Failed to generate session ID from backend, using fallback:', error);
451
+ // Fallback to simple local generation
452
+ const timestamp = Date.now();
453
+ const random = Math.random().toString(36).substr(2, 9);
454
+ return `${prefix}${timestamp}_${random}`;
455
+ }
456
+ }
457
+ }
458
+
459
+ // Custom error class for API errors
460
+ class APIError extends Error {
461
+ constructor(message, status, data) {
462
+ super(message);
463
+ this.name = 'APIError';
464
+ this.status = status;
465
+ this.data = data;
466
+ }
467
+ }
468
+
469
+ // Export for use in other modules
470
+ if (typeof window !== 'undefined') {
471
+ window.VibeSurfAPIClient = VibeSurfAPIClient;
472
+ window.APIError = APIError;
473
+ }