upfynai-code 3.0.3 → 3.0.4

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 (42) hide show
  1. package/client/dist/api-docs.html +838 -838
  2. package/client/dist/assets/{AppContent-Bvg0CPCO.js → AppContent-CwrTP6TW.js} +43 -43
  3. package/client/dist/assets/BrowserPanel-0TLEl-IC.js +2 -0
  4. package/client/dist/assets/{CanvasFullScreen-BdiJ35aq.js → CanvasFullScreen-D1GWQsGL.js} +1 -1
  5. package/client/dist/assets/{CanvasWorkspace-Bk9R9_e0.js → CanvasWorkspace-D7ORj358.js} +1 -1
  6. package/client/dist/assets/DashboardPanel-BV7ybUDe.js +1 -0
  7. package/client/dist/assets/FileTree-5qfhBqdE.js +1 -0
  8. package/client/dist/assets/{GitPanel-RtyZUIWS.js → GitPanel-C_xFM-N2.js} +1 -1
  9. package/client/dist/assets/{LoginModal-BWep8a6g.js → LoginModal-CImJHRjX.js} +3 -3
  10. package/client/dist/assets/{MarkdownPreview-DHmk3qzu.js → MarkdownPreview-CESjI261.js} +1 -1
  11. package/client/dist/assets/{MermaidBlock-BuBc_G-F.js → MermaidBlock-BFM21cwe.js} +2 -2
  12. package/client/dist/assets/Onboarding-B3cteLu2.js +1 -0
  13. package/client/dist/assets/SetupForm-P6dsYgHO.js +1 -0
  14. package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +1 -0
  15. package/client/dist/assets/index-46kkVu2i.css +1 -0
  16. package/client/dist/assets/{index-C5ptjuTl.js → index-HaY-3pK1.js} +20 -20
  17. package/client/dist/assets/{vendor-canvas-D39yWul6.js → vendor-canvas-DvHJ_Pn2.js} +1 -1
  18. package/client/dist/assets/{vendor-codemirror-CbtmxxaB.js → vendor-codemirror-D2ALgpaX.js} +1 -1
  19. package/client/dist/assets/{vendor-icons-BaD0x9SL.js → vendor-icons-GyYE35HP.js} +178 -138
  20. package/client/dist/assets/{vendor-mermaid-CH7SGc99.js → vendor-mermaid-DucWyDEe.js} +3 -3
  21. package/client/dist/assets/{vendor-syntax-DuHI9Ok6.js → vendor-syntax-LS_Nt30I.js} +1 -1
  22. package/client/dist/clear-cache.html +85 -85
  23. package/client/dist/index.html +17 -17
  24. package/client/dist/manifest.json +3 -3
  25. package/client/dist/mcp-docs.html +108 -108
  26. package/client/dist/offline.html +84 -84
  27. package/client/dist/sw.js +82 -82
  28. package/package.json +1 -1
  29. package/server/browser.js +131 -0
  30. package/server/database/db.js +102 -6
  31. package/server/index.js +27 -28
  32. package/server/middleware/auth.js +5 -2
  33. package/server/routes/browser.js +419 -0
  34. package/server/routes/projects.js +118 -19
  35. package/server/routes/vapi-chat.js +1 -1
  36. package/server/services/browser-ai.js +154 -0
  37. package/client/dist/assets/DashboardPanel-CblJfTGi.js +0 -1
  38. package/client/dist/assets/FileTree-BDUnBheV.js +0 -1
  39. package/client/dist/assets/Onboarding-Drnlt75a.js +0 -1
  40. package/client/dist/assets/SetupForm-CtCKitZG.js +0 -1
  41. package/client/dist/assets/WorkflowsPanel-B2mIXDvD.js +0 -1
  42. package/client/dist/assets/index-BFuqS0tY.css +0 -1
@@ -1,84 +1,84 @@
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, viewport-fit=cover" />
6
- <title>Upfyn-Code — Offline</title>
7
- <meta name="theme-color" content="#0a0f1e" />
8
- <style>
9
- * { margin: 0; padding: 0; box-sizing: border-box; }
10
- body {
11
- background: #0a0f1e;
12
- color: #e2e8f0;
13
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14
- min-height: 100vh;
15
- display: flex;
16
- align-items: center;
17
- justify-content: center;
18
- padding: 2rem;
19
- }
20
- .container {
21
- text-align: center;
22
- max-width: 400px;
23
- }
24
- .icon {
25
- width: 64px;
26
- height: 64px;
27
- margin: 0 auto 1.5rem;
28
- background: #0d1117;
29
- border-radius: 16px;
30
- display: flex;
31
- align-items: center;
32
- justify-content: center;
33
- border: 1px solid rgba(59, 130, 246, 0.2);
34
- box-shadow: 0 4px 24px rgba(59, 130, 246, 0.15);
35
- }
36
- .icon svg {
37
- width: 32px;
38
- height: 32px;
39
- color: #3b82f6;
40
- }
41
- h1 {
42
- font-size: 1.5rem;
43
- font-weight: 700;
44
- margin-bottom: 0.75rem;
45
- }
46
- p {
47
- color: #94a3b8;
48
- font-size: 0.9rem;
49
- line-height: 1.5;
50
- margin-bottom: 1.5rem;
51
- }
52
- .retry-btn {
53
- display: inline-block;
54
- padding: 12px 32px;
55
- background: #3b82f6;
56
- color: #fff;
57
- border: none;
58
- border-radius: 10px;
59
- font-size: 0.95rem;
60
- font-weight: 600;
61
- cursor: pointer;
62
- transition: background 0.2s;
63
- }
64
- .retry-btn:hover {
65
- background: #2563eb;
66
- }
67
- .retry-btn:active {
68
- transform: scale(0.97);
69
- }
70
- </style>
71
- </head>
72
- <body>
73
- <div class="container">
74
- <div class="icon">
75
- <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
76
- <path stroke-linecap="round" stroke-linejoin="round" d="M18.364 5.636a9 9 0 010 12.728M5.636 18.364a9 9 0 010-12.728m2.828 9.9a5 5 0 010-7.072m7.072 0a5 5 0 010 7.072M13 12a1 1 0 11-2 0 1 1 0 012 0z" />
77
- </svg>
78
- </div>
79
- <h1>You're offline</h1>
80
- <p>Check your internet connection and try again. Upfyn-Code needs a network connection to work.</p>
81
- <button class="retry-btn" onclick="location.reload()">Retry</button>
82
- </div>
83
- </body>
84
- </html>
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, viewport-fit=cover" />
6
+ <title>UpfynAI — Offline</title>
7
+ <meta name="theme-color" content="#0a0f1e" />
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body {
11
+ background: #0a0f1e;
12
+ color: #e2e8f0;
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14
+ min-height: 100vh;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ padding: 2rem;
19
+ }
20
+ .container {
21
+ text-align: center;
22
+ max-width: 400px;
23
+ }
24
+ .icon {
25
+ width: 64px;
26
+ height: 64px;
27
+ margin: 0 auto 1.5rem;
28
+ background: #0d1117;
29
+ border-radius: 16px;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ border: 1px solid rgba(59, 130, 246, 0.2);
34
+ box-shadow: 0 4px 24px rgba(59, 130, 246, 0.15);
35
+ }
36
+ .icon svg {
37
+ width: 32px;
38
+ height: 32px;
39
+ color: #3b82f6;
40
+ }
41
+ h1 {
42
+ font-size: 1.5rem;
43
+ font-weight: 700;
44
+ margin-bottom: 0.75rem;
45
+ }
46
+ p {
47
+ color: #94a3b8;
48
+ font-size: 0.9rem;
49
+ line-height: 1.5;
50
+ margin-bottom: 1.5rem;
51
+ }
52
+ .retry-btn {
53
+ display: inline-block;
54
+ padding: 12px 32px;
55
+ background: #3b82f6;
56
+ color: #fff;
57
+ border: none;
58
+ border-radius: 10px;
59
+ font-size: 0.95rem;
60
+ font-weight: 600;
61
+ cursor: pointer;
62
+ transition: background 0.2s;
63
+ }
64
+ .retry-btn:hover {
65
+ background: #2563eb;
66
+ }
67
+ .retry-btn:active {
68
+ transform: scale(0.97);
69
+ }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <div class="container">
74
+ <div class="icon">
75
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
76
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18.364 5.636a9 9 0 010 12.728M5.636 18.364a9 9 0 010-12.728m2.828 9.9a5 5 0 010-7.072m7.072 0a5 5 0 010 7.072M13 12a1 1 0 11-2 0 1 1 0 012 0z" />
77
+ </svg>
78
+ </div>
79
+ <h1>You're offline</h1>
80
+ <p>Check your internet connection and try again. UpfynAI needs a network connection to work.</p>
81
+ <button class="retry-btn" onclick="location.reload()">Retry</button>
82
+ </div>
83
+ </body>
84
+ </html>
package/client/dist/sw.js CHANGED
@@ -1,82 +1,82 @@
1
- // Service Worker for Upfyn-Code
2
- // v4 — proper caching: cache-first for static assets, network-first for navigation
3
-
4
- const CACHE_NAME = 'upfyn-v4';
5
- const PRECACHE_ASSETS = [
6
- '/offline.html',
7
- '/favicon.svg',
8
- '/favicon.png',
9
- '/manifest.json',
10
- ];
11
-
12
- self.addEventListener('install', (event) => {
13
- event.waitUntil(
14
- caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_ASSETS))
15
- );
16
- self.skipWaiting();
17
- });
18
-
19
- self.addEventListener('activate', (event) => {
20
- event.waitUntil(
21
- caches.keys().then((names) =>
22
- Promise.all(
23
- names
24
- .filter((n) => n !== CACHE_NAME)
25
- .map((n) => caches.delete(n))
26
- )
27
- )
28
- );
29
- self.clients.claim();
30
- });
31
-
32
- self.addEventListener('fetch', (event) => {
33
- const { request } = event;
34
- const url = new URL(request.url);
35
-
36
- // API calls and WebSocket: pass through, no caching
37
- if (
38
- url.pathname.startsWith('/api/') ||
39
- url.pathname.startsWith('/ws') ||
40
- url.pathname.startsWith('/shell') ||
41
- url.pathname.startsWith('/mcp')
42
- ) {
43
- return;
44
- }
45
-
46
- // Static assets (JS, CSS, images, fonts): cache-first
47
- if (
48
- request.destination === 'script' ||
49
- request.destination === 'style' ||
50
- request.destination === 'image' ||
51
- request.destination === 'font' ||
52
- url.pathname.startsWith('/icons/') ||
53
- url.pathname.startsWith('/assets/')
54
- ) {
55
- event.respondWith(
56
- caches.match(request).then((cached) => {
57
- if (cached) return cached;
58
- return fetch(request).then((response) => {
59
- if (response.ok) {
60
- const clone = response.clone();
61
- caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
62
- }
63
- return response;
64
- }).catch(() => caches.match(request));
65
- })
66
- );
67
- return;
68
- }
69
-
70
- // Navigation requests (HTML): network-first, offline fallback
71
- if (request.mode === 'navigate') {
72
- event.respondWith(
73
- fetch(request).catch(() => caches.match('/offline.html'))
74
- );
75
- return;
76
- }
77
-
78
- // Everything else: network with cache fallback
79
- event.respondWith(
80
- fetch(request).catch(() => caches.match(request))
81
- );
82
- });
1
+ // Service Worker for UpfynAI
2
+ // v4 — proper caching: cache-first for static assets, network-first for navigation
3
+
4
+ const CACHE_NAME = 'upfyn-v4';
5
+ const PRECACHE_ASSETS = [
6
+ '/offline.html',
7
+ '/favicon.svg',
8
+ '/favicon.png',
9
+ '/manifest.json',
10
+ ];
11
+
12
+ self.addEventListener('install', (event) => {
13
+ event.waitUntil(
14
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_ASSETS))
15
+ );
16
+ self.skipWaiting();
17
+ });
18
+
19
+ self.addEventListener('activate', (event) => {
20
+ event.waitUntil(
21
+ caches.keys().then((names) =>
22
+ Promise.all(
23
+ names
24
+ .filter((n) => n !== CACHE_NAME)
25
+ .map((n) => caches.delete(n))
26
+ )
27
+ )
28
+ );
29
+ self.clients.claim();
30
+ });
31
+
32
+ self.addEventListener('fetch', (event) => {
33
+ const { request } = event;
34
+ const url = new URL(request.url);
35
+
36
+ // API calls and WebSocket: pass through, no caching
37
+ if (
38
+ url.pathname.startsWith('/api/') ||
39
+ url.pathname.startsWith('/ws') ||
40
+ url.pathname.startsWith('/shell') ||
41
+ url.pathname.startsWith('/mcp')
42
+ ) {
43
+ return;
44
+ }
45
+
46
+ // Static assets (JS, CSS, images, fonts): cache-first
47
+ if (
48
+ request.destination === 'script' ||
49
+ request.destination === 'style' ||
50
+ request.destination === 'image' ||
51
+ request.destination === 'font' ||
52
+ url.pathname.startsWith('/icons/') ||
53
+ url.pathname.startsWith('/assets/')
54
+ ) {
55
+ event.respondWith(
56
+ caches.match(request).then((cached) => {
57
+ if (cached) return cached;
58
+ return fetch(request).then((response) => {
59
+ if (response.ok) {
60
+ const clone = response.clone();
61
+ caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
62
+ }
63
+ return response;
64
+ }).catch(() => caches.match(request));
65
+ })
66
+ );
67
+ return;
68
+ }
69
+
70
+ // Navigation requests (HTML): network-first, offline fallback
71
+ if (request.mode === 'navigate') {
72
+ event.respondWith(
73
+ fetch(request).catch(() => caches.match('/offline.html'))
74
+ );
75
+ return;
76
+ }
77
+
78
+ // Everything else: network with cache fallback
79
+ event.respondWith(
80
+ fetch(request).catch(() => caches.match(request))
81
+ );
82
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upfynai-code",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Visual AI coding interface for Claude Code, Cursor & Codex. Canvas whiteboard, multi-agent chat, terminal, git, voice assistant. Self-host locally or connect to cli.upfyn.com for remote access.",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Browser Client — connects the backend to the Steel browser service on Railway.
3
+ * All browser operations are proxied to the Steel service via HTTP.
4
+ * Mirrors the sandbox.js client pattern.
5
+ */
6
+
7
+ const BROWSER_SERVICE_URL = process.env.BROWSER_SERVICE_URL || 'http://localhost:4400';
8
+ const BROWSER_SERVICE_SECRET = process.env.BROWSER_SERVICE_SECRET || '';
9
+
10
+ // Warn if no secret in production — inter-service calls will be unauthenticated
11
+ if (!BROWSER_SERVICE_SECRET && process.env.NODE_ENV === 'production') {
12
+ console.error('[browser] WARNING: BROWSER_SERVICE_SECRET not set — browser service calls are unauthenticated');
13
+ }
14
+
15
+ async function browserFetch(path, userId, body = null, method = null) {
16
+ const opts = {
17
+ method: method || (body ? 'POST' : 'GET'),
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ 'x-browser-secret': BROWSER_SERVICE_SECRET,
21
+ 'x-user-id': String(userId),
22
+ },
23
+ };
24
+ if (body) opts.body = JSON.stringify(body);
25
+
26
+ const res = await fetch(`${BROWSER_SERVICE_URL}${path}`, opts);
27
+ const data = await res.json();
28
+ if (!res.ok) throw new Error(data.error || data.message || `Browser service error: ${res.status}`);
29
+ return data;
30
+ }
31
+
32
+ const browserClient = {
33
+
34
+ /**
35
+ * Check if the browser service is reachable.
36
+ */
37
+ async isAvailable() {
38
+ try {
39
+ const res = await fetch(`${BROWSER_SERVICE_URL}/api/health`, {
40
+ signal: AbortSignal.timeout(3000),
41
+ });
42
+ return res.ok;
43
+ } catch {
44
+ return false;
45
+ }
46
+ },
47
+
48
+ /**
49
+ * Create a new browser session.
50
+ * Returns: { id, wsEndpoint, debugUrl, sessionViewerUrl, ... }
51
+ */
52
+ async createSession(userId, options = {}) {
53
+ const sessionOpts = {
54
+ sessionTimeout: options.timeout || 1800000, // 30 min default
55
+ blockAds: options.blockAds !== false,
56
+ solveCaptchas: options.solveCaptchas || false,
57
+ useProxy: options.useProxy || false,
58
+ dimensions: options.dimensions || { width: 1280, height: 800 },
59
+ userAgent: options.userAgent || undefined,
60
+ // Tag with userId for isolation
61
+ sessionContext: {
62
+ userId: String(userId),
63
+ createdAt: new Date().toISOString(),
64
+ },
65
+ };
66
+
67
+ return browserFetch('/v1/sessions', userId, sessionOpts);
68
+ },
69
+
70
+ /**
71
+ * Get session details.
72
+ */
73
+ async getSession(userId, sessionId) {
74
+ return browserFetch(`/v1/sessions/${sessionId}`, userId);
75
+ },
76
+
77
+ /**
78
+ * Release (close) a browser session.
79
+ */
80
+ async releaseSession(userId, sessionId) {
81
+ return browserFetch(`/v1/sessions/${sessionId}/release`, userId, {}, 'POST');
82
+ },
83
+
84
+ /**
85
+ * List active sessions.
86
+ */
87
+ async listSessions(userId) {
88
+ return browserFetch('/v1/sessions', userId);
89
+ },
90
+
91
+ /**
92
+ * Get the session viewer URL for embedding in an iframe.
93
+ * Steel's built-in viewer provides an interactive browser view.
94
+ */
95
+ getSessionViewerUrl(sessionId) {
96
+ return `${BROWSER_SERVICE_URL}/v1/sessions/${sessionId}/viewer?interactive=true&showControls=true`;
97
+ },
98
+
99
+ /**
100
+ * Get the CDP WebSocket URL for connecting Stagehand/Playwright.
101
+ */
102
+ getCdpWsUrl(sessionId) {
103
+ const wsBase = BROWSER_SERVICE_URL.replace(/^http/, 'ws');
104
+ return `${wsBase}?sessionId=${sessionId}`;
105
+ },
106
+
107
+ /**
108
+ * Capture a screenshot of the current page.
109
+ */
110
+ async screenshot(userId, sessionId) {
111
+ return browserFetch(`/v1/sessions/${sessionId}/screenshot`, userId, {});
112
+ },
113
+
114
+ /**
115
+ * Scrape a URL — returns page content.
116
+ */
117
+ async scrape(userId, sessionId, url) {
118
+ return browserFetch('/v1/scrape', userId, { url, sessionId });
119
+ },
120
+
121
+ /**
122
+ * Get the sandbox dev server URL that the browser can reach internally.
123
+ * The sandbox and browser share Railway's internal network.
124
+ */
125
+ getSandboxPreviewUrl(userId, port) {
126
+ const sandboxUrl = process.env.SANDBOX_SERVICE_URL || 'http://localhost:4300';
127
+ return `${sandboxUrl}/proxy/${userId}/${port}`;
128
+ },
129
+ };
130
+
131
+ export { browserClient, BROWSER_SERVICE_URL, BROWSER_SERVICE_SECRET };
@@ -415,6 +415,22 @@ CREATE TABLE IF NOT EXISTS voice_calls (
415
415
 
416
416
  CREATE INDEX IF NOT EXISTS idx_voice_calls_user ON voice_calls(user_id);
417
417
  CREATE INDEX IF NOT EXISTS idx_voice_calls_vapi ON voice_calls(vapi_call_id);
418
+
419
+ CREATE TABLE IF NOT EXISTS browser_sessions (
420
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
421
+ user_id INTEGER NOT NULL,
422
+ steel_session_id TEXT NOT NULL UNIQUE,
423
+ status TEXT NOT NULL DEFAULT 'active',
424
+ viewer_url TEXT,
425
+ cdp_url TEXT,
426
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
427
+ last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP,
428
+ metadata TEXT DEFAULT '{}',
429
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
430
+ );
431
+
432
+ CREATE INDEX IF NOT EXISTS idx_browser_sessions_user ON browser_sessions(user_id);
433
+ CREATE INDEX IF NOT EXISTS idx_browser_sessions_status ON browser_sessions(status);
418
434
  `;
419
435
 
420
436
  // ─── Migrations ─────────────────────────────────────────────────────────────────
@@ -512,6 +528,15 @@ const runMigrations = async () => {
512
528
  }
513
529
  } catch { /* column may already exist */ }
514
530
 
531
+ // Add github_origin column to user_projects if missing
532
+ try {
533
+ const upCols = await db.execute("PRAGMA table_info(user_projects)");
534
+ const upColNames = upCols.rows.map(r => r.name);
535
+ if (!upColNames.includes('github_origin')) {
536
+ await db.execute('ALTER TABLE user_projects ADD COLUMN github_origin TEXT');
537
+ }
538
+ } catch { /* column may already exist */ }
539
+
515
540
  console.log(`${c.info('[DB]')} Migrations complete`);
516
541
  } catch (error) {
517
542
  console.error('Migration error:', error.message);
@@ -1307,21 +1332,26 @@ const resetTokenDb = {
1307
1332
  // ─── Projects DB (cloud mode) ─────────────────────────────────────────────────
1308
1333
 
1309
1334
  const projectDb = {
1310
- upsert: async (userId, originalPath, displayName = null) => {
1335
+ upsert: async (userId, originalPath, displayName = null, githubOrigin = null) => {
1311
1336
  const projectName = originalPath.replace(/[\\/:\s~_]/g, '-');
1312
1337
  const existing = await db.execute({
1313
1338
  sql: 'SELECT id FROM user_projects WHERE user_id = ? AND original_path = ?',
1314
1339
  args: [userId, originalPath]
1315
1340
  });
1316
1341
  if (existing.rows.length > 0) {
1317
- if (displayName) {
1318
- await db.execute({ sql: 'UPDATE user_projects SET display_name = ? WHERE id = ?', args: [displayName, existing.rows[0].id] });
1342
+ if (displayName || githubOrigin) {
1343
+ const updates = [];
1344
+ const args = [];
1345
+ if (displayName) { updates.push('display_name = ?'); args.push(displayName); }
1346
+ if (githubOrigin) { updates.push('github_origin = ?'); args.push(githubOrigin); }
1347
+ args.push(existing.rows[0].id);
1348
+ await db.execute({ sql: `UPDATE user_projects SET ${updates.join(', ')} WHERE id = ?`, args });
1319
1349
  }
1320
1350
  return { id: Number(existing.rows[0].id), projectName, originalPath, alreadyExists: true };
1321
1351
  }
1322
1352
  const result = await db.execute({
1323
- sql: 'INSERT INTO user_projects (user_id, project_name, original_path, display_name) VALUES (?, ?, ?, ?)',
1324
- args: [userId, projectName, originalPath, displayName]
1353
+ sql: 'INSERT INTO user_projects (user_id, project_name, original_path, display_name, github_origin) VALUES (?, ?, ?, ?, ?)',
1354
+ args: [userId, projectName, originalPath, displayName, githubOrigin]
1325
1355
  });
1326
1356
  return { id: Number(result.lastInsertRowid), projectName, originalPath };
1327
1357
  },
@@ -1348,6 +1378,14 @@ const projectDb = {
1348
1378
  args: [displayName, userId, originalPath]
1349
1379
  });
1350
1380
  return result.rowsAffected > 0;
1381
+ },
1382
+
1383
+ getByName: async (userId, projectName) => {
1384
+ const result = await db.execute({
1385
+ sql: 'SELECT * FROM user_projects WHERE user_id = ? AND project_name = ?',
1386
+ args: [userId, projectName]
1387
+ });
1388
+ return result.rows[0] || null;
1351
1389
  }
1352
1390
  };
1353
1391
 
@@ -1448,4 +1486,62 @@ const voiceCallDb = {
1448
1486
  },
1449
1487
  };
1450
1488
 
1451
- export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, relayTokensDb, githubTokensDb, subscriptionDb, paymentDb, webhookDb, workflowDb, fileVersionDb, sessionUsageDb, connectionDb, canvasDb, resetTokenDb, projectDb, voiceCallDb, PLAN_DURATIONS };
1489
+ // ─── Browser Sessions DB ─────────────────────────────────────────────────────
1490
+
1491
+ const browserSessionDb = {
1492
+ create: async (userId, steelSessionId, viewerUrl = null, cdpUrl = null, metadata = {}) => {
1493
+ const result = await db.execute({
1494
+ sql: 'INSERT INTO browser_sessions (user_id, steel_session_id, viewer_url, cdp_url, metadata) VALUES (?, ?, ?, ?, ?)',
1495
+ args: [userId, steelSessionId, viewerUrl, cdpUrl, JSON.stringify(metadata)]
1496
+ });
1497
+ return { id: Number(result.lastInsertRowid), steelSessionId };
1498
+ },
1499
+
1500
+ getActive: async (userId) => {
1501
+ const result = await db.execute({
1502
+ sql: "SELECT * FROM browser_sessions WHERE user_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1",
1503
+ args: [userId]
1504
+ });
1505
+ return getRow(result);
1506
+ },
1507
+
1508
+ getBySessionId: async (userId, steelSessionId) => {
1509
+ const result = await db.execute({
1510
+ sql: 'SELECT * FROM browser_sessions WHERE user_id = ? AND steel_session_id = ?',
1511
+ args: [userId, steelSessionId]
1512
+ });
1513
+ return getRow(result);
1514
+ },
1515
+
1516
+ updateAccess: async (userId, steelSessionId) => {
1517
+ await db.execute({
1518
+ sql: "UPDATE browser_sessions SET last_accessed = datetime('now') WHERE user_id = ? AND steel_session_id = ?",
1519
+ args: [userId, steelSessionId]
1520
+ });
1521
+ },
1522
+
1523
+ deactivate: async (userId, steelSessionId) => {
1524
+ await db.execute({
1525
+ sql: "UPDATE browser_sessions SET status = 'closed' WHERE user_id = ? AND steel_session_id = ?",
1526
+ args: [userId, steelSessionId]
1527
+ });
1528
+ },
1529
+
1530
+ listByUser: async (userId) => {
1531
+ const result = await db.execute({
1532
+ sql: 'SELECT * FROM browser_sessions WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',
1533
+ args: [userId]
1534
+ });
1535
+ return result.rows;
1536
+ },
1537
+
1538
+ cleanupStale: async (maxAgeMinutes = 30) => {
1539
+ const result = await db.execute({
1540
+ sql: `UPDATE browser_sessions SET status = 'expired' WHERE status = 'active' AND last_accessed < datetime('now', '-' || ? || ' minutes')`,
1541
+ args: [maxAgeMinutes]
1542
+ });
1543
+ return result.rowsAffected || 0;
1544
+ },
1545
+ };
1546
+
1547
+ export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, relayTokensDb, githubTokensDb, subscriptionDb, paymentDb, webhookDb, workflowDb, fileVersionDb, sessionUsageDb, connectionDb, canvasDb, resetTokenDb, projectDb, voiceCallDb, browserSessionDb, PLAN_DURATIONS };