@myrialabs/clopen 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +23 -1
  2. package/backend/index.ts +25 -1
  3. package/backend/lib/auth/auth-service.ts +484 -0
  4. package/backend/lib/auth/index.ts +4 -0
  5. package/backend/lib/auth/permissions.ts +63 -0
  6. package/backend/lib/auth/rate-limiter.ts +145 -0
  7. package/backend/lib/auth/tokens.ts +53 -0
  8. package/backend/lib/chat/stream-manager.ts +4 -1
  9. package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
  10. package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
  11. package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
  12. package/backend/lib/database/migrations/index.ts +21 -0
  13. package/backend/lib/database/queries/auth-queries.ts +201 -0
  14. package/backend/lib/database/queries/index.ts +2 -1
  15. package/backend/lib/database/queries/session-queries.ts +13 -0
  16. package/backend/lib/database/queries/snapshot-queries.ts +1 -1
  17. package/backend/lib/engine/adapters/opencode/server.ts +9 -1
  18. package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
  19. package/backend/lib/mcp/config.ts +13 -18
  20. package/backend/lib/mcp/index.ts +9 -0
  21. package/backend/lib/mcp/remote-server.ts +132 -0
  22. package/backend/lib/mcp/servers/helper.ts +49 -3
  23. package/backend/lib/mcp/servers/index.ts +3 -2
  24. package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
  25. package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
  26. package/backend/lib/preview/browser/browser-pool.ts +73 -176
  27. package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
  28. package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
  29. package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
  30. package/backend/lib/snapshot/helpers.ts +22 -49
  31. package/backend/lib/snapshot/snapshot-service.ts +148 -83
  32. package/backend/lib/utils/ws.ts +65 -1
  33. package/backend/ws/auth/index.ts +17 -0
  34. package/backend/ws/auth/invites.ts +84 -0
  35. package/backend/ws/auth/login.ts +269 -0
  36. package/backend/ws/auth/status.ts +41 -0
  37. package/backend/ws/auth/users.ts +32 -0
  38. package/backend/ws/chat/stream.ts +13 -0
  39. package/backend/ws/engine/claude/accounts.ts +3 -1
  40. package/backend/ws/engine/utils.ts +38 -6
  41. package/backend/ws/index.ts +4 -4
  42. package/backend/ws/preview/browser/interact.ts +27 -5
  43. package/backend/ws/snapshot/restore.ts +111 -12
  44. package/backend/ws/snapshot/timeline.ts +56 -29
  45. package/bin/clopen.ts +56 -1
  46. package/bun.lock +113 -51
  47. package/frontend/App.svelte +47 -29
  48. package/frontend/lib/components/auth/InvitePage.svelte +215 -0
  49. package/frontend/lib/components/auth/LoginPage.svelte +129 -0
  50. package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
  51. package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
  52. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
  53. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
  54. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
  55. package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
  56. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
  57. package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
  58. package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
  59. package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
  60. package/frontend/lib/components/git/CommitForm.svelte +6 -4
  61. package/frontend/lib/components/history/HistoryModal.svelte +1 -1
  62. package/frontend/lib/components/history/HistoryView.svelte +1 -1
  63. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
  64. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
  65. package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
  66. package/frontend/lib/components/settings/SettingsView.svelte +21 -7
  67. package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
  68. package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
  69. package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
  70. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
  71. package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
  72. package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
  73. package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
  74. package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
  75. package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
  76. package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
  77. package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
  78. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
  79. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
  80. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
  81. package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
  82. package/frontend/lib/stores/features/auth.svelte.ts +296 -0
  83. package/frontend/lib/stores/features/settings.svelte.ts +53 -9
  84. package/frontend/lib/stores/features/user.svelte.ts +26 -68
  85. package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
  86. package/frontend/lib/stores/ui/update.svelte.ts +2 -14
  87. package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
  88. package/package.json +8 -6
  89. package/shared/types/stores/settings.ts +16 -2
  90. package/shared/utils/logger.ts +1 -0
  91. package/shared/utils/ws-client.ts +30 -13
  92. package/shared/utils/ws-server.ts +42 -4
  93. package/backend/lib/mcp/stdio-server.ts +0 -103
  94. package/backend/ws/mcp/index.ts +0 -61
@@ -0,0 +1,269 @@
1
+ import { t } from 'elysia';
2
+ import { createRouter } from '$shared/utils/ws-server';
3
+ import {
4
+ createAdmin,
5
+ loginWithToken,
6
+ createUserFromInvite,
7
+ logout,
8
+ logoutAllSessions,
9
+ validateInviteToken,
10
+ regeneratePAT,
11
+ updateUserName,
12
+ createOrGetNoAuthAdmin,
13
+ needsSetup
14
+ } from '$backend/lib/auth/auth-service';
15
+ import { settingsQueries } from '$backend/lib/database/queries';
16
+ import { getTokenType } from '$backend/lib/auth/tokens';
17
+ import { authRateLimiter } from '$backend/lib/auth/rate-limiter';
18
+ import { ws } from '$backend/lib/utils/ws';
19
+
20
+ const authUserSchema = t.Object({
21
+ id: t.String(),
22
+ name: t.String(),
23
+ role: t.Union([t.Literal('admin'), t.Literal('member')]),
24
+ color: t.String(),
25
+ avatar: t.String(),
26
+ createdAt: t.String()
27
+ });
28
+
29
+ export const loginHandler = createRouter()
30
+ // Setup — create first admin (only works when no users exist)
31
+ .http('auth:setup', {
32
+ data: t.Object({
33
+ name: t.String({ minLength: 1 })
34
+ }),
35
+ response: t.Object({
36
+ user: authUserSchema,
37
+ sessionToken: t.String(),
38
+ personalAccessToken: t.String(),
39
+ expiresAt: t.String()
40
+ })
41
+ }, async ({ data, conn }) => {
42
+ const result = createAdmin(data.name);
43
+
44
+ // Save authMode to system settings
45
+ const currentSettings = settingsQueries.get('system:settings');
46
+ const parsed = currentSettings?.value
47
+ ? (typeof currentSettings.value === 'string' ? JSON.parse(currentSettings.value) : currentSettings.value)
48
+ : {};
49
+ parsed.authMode = 'required';
50
+ settingsQueries.set('system:settings', JSON.stringify(parsed));
51
+
52
+ // Set auth on connection
53
+ const tokenHash = (await import('$backend/lib/auth/tokens')).hashToken(result.sessionToken);
54
+ ws.setAuth(conn, result.user.id, result.user.role, tokenHash);
55
+
56
+ return result;
57
+ })
58
+
59
+ // Setup no-auth mode — create default admin, save authMode setting
60
+ .http('auth:setup-no-auth', {
61
+ data: t.Object({}),
62
+ response: t.Object({
63
+ user: authUserSchema,
64
+ sessionToken: t.String(),
65
+ expiresAt: t.String()
66
+ })
67
+ }, async ({ conn }) => {
68
+ if (!needsSetup()) {
69
+ throw new Error('Setup already completed.');
70
+ }
71
+
72
+ // Save authMode to system settings
73
+ const currentSettings = settingsQueries.get('system:settings');
74
+ const parsed = currentSettings?.value
75
+ ? (typeof currentSettings.value === 'string' ? JSON.parse(currentSettings.value) : currentSettings.value)
76
+ : {};
77
+ parsed.authMode = 'none';
78
+ settingsQueries.set('system:settings', JSON.stringify(parsed));
79
+
80
+ // Create or get default admin
81
+ const result = createOrGetNoAuthAdmin();
82
+
83
+ // Set auth on connection
84
+ const tokenHash = (await import('$backend/lib/auth/tokens')).hashToken(result.sessionToken);
85
+ ws.setAuth(conn, result.user.id, result.user.role, tokenHash);
86
+
87
+ return result;
88
+ })
89
+
90
+ // Auto-login for no-auth mode (returning visitors)
91
+ .http('auth:auto-login-no-auth', {
92
+ data: t.Object({}),
93
+ response: t.Object({
94
+ user: authUserSchema,
95
+ sessionToken: t.String(),
96
+ expiresAt: t.String()
97
+ })
98
+ }, async ({ conn }) => {
99
+ const result = createOrGetNoAuthAdmin();
100
+
101
+ // Set auth on connection
102
+ const tokenHash = (await import('$backend/lib/auth/tokens')).hashToken(result.sessionToken);
103
+ ws.setAuth(conn, result.user.id, result.user.role, tokenHash);
104
+
105
+ return result;
106
+ })
107
+
108
+ // Login with token (PAT or session token)
109
+ .http('auth:login', {
110
+ data: t.Object({
111
+ token: t.String({ minLength: 1 })
112
+ }),
113
+ response: t.Object({
114
+ user: authUserSchema,
115
+ sessionToken: t.String(),
116
+ expiresAt: t.String()
117
+ })
118
+ }, async ({ data, conn }) => {
119
+ const ip = ws.getRemoteAddress(conn);
120
+ const tokenType = getTokenType(data.token);
121
+
122
+ // Session tokens are re-authentication (reconnect) — skip rate limit entirely.
123
+ // Only rate-limit PAT and unknown tokens (brute-force targets).
124
+ const isRateLimited = tokenType !== 'session';
125
+
126
+ if (isRateLimited) {
127
+ const rateLimitError = authRateLimiter.check(ip, 'auth:login');
128
+ if (rateLimitError) {
129
+ throw new Error(rateLimitError);
130
+ }
131
+ }
132
+
133
+ try {
134
+ const result = loginWithToken(data.token);
135
+
136
+ // Success — clear any rate limit record for this IP
137
+ if (isRateLimited) {
138
+ authRateLimiter.recordSuccess(ip);
139
+ }
140
+
141
+ // Set auth on connection
142
+ ws.setAuth(conn, result.user.id, result.user.role, result.tokenHash);
143
+
144
+ return {
145
+ user: result.user,
146
+ sessionToken: result.sessionToken,
147
+ expiresAt: result.expiresAt
148
+ };
149
+ } catch (err) {
150
+ // Record failure for rate limiting (only non-session tokens)
151
+ if (isRateLimited) {
152
+ authRateLimiter.recordFailure(ip, 'auth:login');
153
+ }
154
+ throw err;
155
+ }
156
+ })
157
+
158
+ // Accept invite — create user from invite token
159
+ .http('auth:accept-invite', {
160
+ data: t.Object({
161
+ inviteToken: t.String({ minLength: 1 }),
162
+ name: t.String({ minLength: 1 })
163
+ }),
164
+ response: t.Object({
165
+ user: authUserSchema,
166
+ sessionToken: t.String(),
167
+ personalAccessToken: t.String(),
168
+ expiresAt: t.String()
169
+ })
170
+ }, async ({ data, conn }) => {
171
+ const ip = ws.getRemoteAddress(conn);
172
+
173
+ // Rate limit check
174
+ const rateLimitError = authRateLimiter.check(ip, 'auth:accept-invite');
175
+ if (rateLimitError) {
176
+ throw new Error(rateLimitError);
177
+ }
178
+
179
+ try {
180
+ const result = createUserFromInvite(data.inviteToken, data.name);
181
+
182
+ authRateLimiter.recordSuccess(ip);
183
+
184
+ // Set auth on connection
185
+ const tokenHash = (await import('$backend/lib/auth/tokens')).hashToken(result.sessionToken);
186
+ ws.setAuth(conn, result.user.id, result.user.role, tokenHash);
187
+
188
+ return result;
189
+ } catch (err) {
190
+ authRateLimiter.recordFailure(ip, 'auth:accept-invite');
191
+ throw err;
192
+ }
193
+ })
194
+
195
+ // Validate invite token (for UI, doesn't consume the invite)
196
+ .http('auth:validate-invite', {
197
+ data: t.Object({
198
+ inviteToken: t.String({ minLength: 1 })
199
+ }),
200
+ response: t.Object({
201
+ valid: t.Boolean(),
202
+ error: t.Optional(t.String())
203
+ })
204
+ }, async ({ data, conn }) => {
205
+ const ip = ws.getRemoteAddress(conn);
206
+
207
+ const rateLimitError = authRateLimiter.check(ip, 'auth:validate-invite');
208
+ if (rateLimitError) {
209
+ return { valid: false, error: rateLimitError };
210
+ }
211
+
212
+ const result = validateInviteToken(data.inviteToken);
213
+
214
+ // Record failure if invalid token (probing)
215
+ if (!result.valid) {
216
+ authRateLimiter.recordFailure(ip, 'auth:validate-invite');
217
+ }
218
+
219
+ return { valid: result.valid, error: result.error };
220
+ })
221
+
222
+ // Logout — clear session
223
+ .http('auth:logout', {
224
+ data: t.Object({}),
225
+ response: t.Object({
226
+ success: t.Boolean()
227
+ })
228
+ }, async ({ conn }) => {
229
+ const state = ws.getConnectionState(conn);
230
+ if (state?.sessionTokenHash) {
231
+ logout(state.sessionTokenHash);
232
+ }
233
+ ws.clearAuth(conn);
234
+ return { success: true };
235
+ })
236
+
237
+ // Regenerate Personal Access Token
238
+ .http('auth:regenerate-pat', {
239
+ data: t.Object({}),
240
+ response: t.Object({
241
+ personalAccessToken: t.String()
242
+ })
243
+ }, async ({ conn }) => {
244
+ const userId = ws.getUserId(conn);
245
+ const pat = regeneratePAT(userId);
246
+ return { personalAccessToken: pat };
247
+ })
248
+
249
+ // Logout all sessions (admin only — used when switching auth mode)
250
+ .http('auth:logout-all', {
251
+ data: t.Object({}),
252
+ response: t.Object({
253
+ count: t.Number()
254
+ })
255
+ }, async () => {
256
+ const count = logoutAllSessions();
257
+ return { count };
258
+ })
259
+
260
+ // Update display name (authenticated user)
261
+ .http('auth:update-name', {
262
+ data: t.Object({
263
+ newName: t.String({ minLength: 1 })
264
+ }),
265
+ response: authUserSchema
266
+ }, async ({ data, conn }) => {
267
+ const userId = ws.getUserId(conn);
268
+ return updateUserName(userId, data.newName);
269
+ });
@@ -0,0 +1,41 @@
1
+ import { t } from 'elysia';
2
+ import { createRouter } from '$shared/utils/ws-server';
3
+ import { needsSetup, getUserById, getAuthMode, isOnboardingComplete } from '$backend/lib/auth/auth-service';
4
+ import { ws } from '$backend/lib/utils/ws';
5
+
6
+ export const statusHandler = createRouter()
7
+ .http('auth:status', {
8
+ data: t.Object({}),
9
+ response: t.Object({
10
+ needsSetup: t.Boolean(),
11
+ onboardingComplete: t.Boolean(),
12
+ authenticated: t.Boolean(),
13
+ authMode: t.Union([t.Literal('none'), t.Literal('required')]),
14
+ user: t.Optional(t.Object({
15
+ id: t.String(),
16
+ name: t.String(),
17
+ role: t.Union([t.Literal('admin'), t.Literal('member')]),
18
+ color: t.String(),
19
+ avatar: t.String(),
20
+ createdAt: t.String()
21
+ }))
22
+ })
23
+ }, async ({ conn }) => {
24
+ const setup = needsSetup();
25
+ const onboardingDone = isOnboardingComplete();
26
+ const authMode = getAuthMode();
27
+ const authenticated = ws.isAuthenticated(conn);
28
+
29
+ let user = undefined;
30
+ if (authenticated) {
31
+ const state = ws.getConnectionState(conn);
32
+ if (state?.userId) {
33
+ const dbUser = getUserById(state.userId);
34
+ if (dbUser) {
35
+ user = dbUser;
36
+ }
37
+ }
38
+ }
39
+
40
+ return { needsSetup: setup, onboardingComplete: onboardingDone, authenticated, authMode, user };
41
+ });
@@ -0,0 +1,32 @@
1
+ import { t } from 'elysia';
2
+ import { createRouter } from '$shared/utils/ws-server';
3
+ import { listUsers, removeUser } from '$backend/lib/auth/auth-service';
4
+
5
+ export const usersHandler = createRouter()
6
+ // List all users (admin only — enforced by auth gate)
7
+ .http('auth:list-users', {
8
+ data: t.Object({}),
9
+ response: t.Array(t.Object({
10
+ id: t.String(),
11
+ name: t.String(),
12
+ role: t.Union([t.Literal('admin'), t.Literal('member')]),
13
+ color: t.String(),
14
+ avatar: t.String(),
15
+ createdAt: t.String()
16
+ }))
17
+ }, async () => {
18
+ return listUsers();
19
+ })
20
+
21
+ // Remove user (admin only)
22
+ .http('auth:remove-user', {
23
+ data: t.Object({
24
+ userId: t.String({ minLength: 1 })
25
+ }),
26
+ response: t.Object({
27
+ success: t.Boolean()
28
+ })
29
+ }, async ({ data }) => {
30
+ removeUser(data.userId);
31
+ return { success: true };
32
+ });
@@ -49,6 +49,14 @@ streamManager.on('stream:lifecycle', (event: { status: string; streamId: string;
49
49
  broadcastPresence().catch(() => {});
50
50
  });
51
51
 
52
+ // Notify project members when a snapshot is captured (so the timeline modal can refresh stats)
53
+ streamManager.on('snapshot:captured', (event: { projectId: string; chatSessionId: string }) => {
54
+ const { projectId, chatSessionId } = event;
55
+ if (!projectId) return;
56
+
57
+ ws.emit.projectMembers(projectId, 'snapshot:captured', { projectId, chatSessionId });
58
+ });
59
+
52
60
  // In-memory store for latest chat input state per chat session (keyed by chatSessionId)
53
61
  const chatSessionInputState = new Map<string, { text: string; senderId: string; attachments?: any[] }>();
54
62
 
@@ -789,4 +797,9 @@ export const streamHandler = createRouter()
789
797
  chatSessionId: t.String(),
790
798
  toolUseId: t.String(),
791
799
  timestamp: t.String()
800
+ }))
801
+
802
+ .emit('snapshot:captured', t.Object({
803
+ projectId: t.String(),
804
+ chatSessionId: t.String()
792
805
  }));
@@ -21,6 +21,7 @@ import { engineQueries } from '../../../lib/database/queries';
21
21
  import { resetEnvironment, getClaudeUserConfigDir } from '../../../lib/engine/adapters/claude/environment';
22
22
  import { debug } from '$shared/utils/logger';
23
23
  import { getCleanSpawnEnv } from '../../../lib/shared/env';
24
+ import { resolveCommand } from '../utils';
24
25
 
25
26
  // ── Helpers ──
26
27
 
@@ -190,9 +191,10 @@ export const accountsHandler = createRouter()
190
191
  ptyEnv['CLAUDE_CONFIG_DIR'] = getClaudeUserConfigDir();
191
192
  ptyEnv['BROWSER'] = 'false';
192
193
 
194
+ const claudeCmd = await resolveCommand('claude');
193
195
  let pty: ReturnType<typeof spawn>;
194
196
  try {
195
- pty = spawn('claude', ['setup-token'], {
197
+ pty = spawn(claudeCmd, ['setup-token'], {
196
198
  name: 'xterm-256color',
197
199
  cols: 1000,
198
200
  rows: 30,
@@ -4,6 +4,8 @@
4
4
  * Shared helpers used by engine status handlers.
5
5
  */
6
6
 
7
+ type EngineCommand = 'claude' | 'opencode';
8
+
7
9
  export function getBackendOS(): 'windows' | 'macos' | 'linux' {
8
10
  switch (process.platform) {
9
11
  case 'win32': return 'windows';
@@ -12,22 +14,52 @@ export function getBackendOS(): 'windows' | 'macos' | 'linux' {
12
14
  }
13
15
  }
14
16
 
15
- export async function detectCLI(command: string): Promise<{ installed: boolean; version: string | null }> {
17
+ const resolvedCommands = new Map<EngineCommand, string>();
18
+
19
+ /**
20
+ * Resolves the correct CLI command for the current platform.
21
+ * On Windows, tries the base command first, then falls back to
22
+ * command.cmd for older CLI installations. Result is cached.
23
+ */
24
+ export async function resolveCommand(command: EngineCommand): Promise<string> {
25
+ const cached = resolvedCommands.get(command);
26
+ if (cached) return cached;
27
+
28
+ let resolved: string = command;
29
+
30
+ if (!await trySpawn(command) && process.platform === 'win32') {
31
+ const fallback = command + '.cmd';
32
+ if (await trySpawn(fallback)) resolved = fallback;
33
+ }
34
+
35
+ resolvedCommands.set(command, resolved);
36
+ return resolved;
37
+ }
38
+
39
+ async function trySpawn(command: string): Promise<boolean> {
40
+ try {
41
+ const proc = Bun.spawn([command, '--version'], { stdout: 'pipe', stderr: 'pipe' });
42
+ return (await proc.exited) === 0;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export async function detectCLI(command: EngineCommand): Promise<{ installed: boolean; version: string | null }> {
49
+ const resolved = await resolveCommand(command);
50
+
16
51
  try {
17
- const proc = Bun.spawn([command, '--version'], {
52
+ const proc = Bun.spawn([resolved, '--version'], {
18
53
  stdout: 'pipe',
19
54
  stderr: 'pipe'
20
55
  });
21
56
 
22
57
  const exitCode = await proc.exited;
23
- if (exitCode !== 0) {
24
- return { installed: false, version: null };
25
- }
58
+ if (exitCode !== 0) return { installed: false, version: null };
26
59
 
27
60
  const stdout = await new Response(proc.stdout).text();
28
61
  const raw = stdout.trim();
29
62
  // Extract only the version token (e.g. "2.1.52" from "2.1.52 (Claude Code)")
30
- // Takes everything before the first whitespace or parenthesis
31
63
  const version = raw.split(/[\s(]/)[0] || raw || null;
32
64
  return { installed: true, version };
33
65
  } catch {
@@ -13,6 +13,7 @@
13
13
  import { createRouter } from '$shared/utils/ws-server';
14
14
 
15
15
  // Import all module routers
16
+ import { authRouter } from './auth';
16
17
  import { chatRouter } from './chat';
17
18
  import { terminalRouter } from './terminal';
18
19
  import { previewRouter } from './preview';
@@ -26,7 +27,6 @@ import { filesRouter } from './files';
26
27
  import { systemRouter } from './system';
27
28
  import { tunnelRouter } from './tunnel';
28
29
  import { gitRouter } from './git';
29
- import { mcpRouter } from './mcp';
30
30
  import { engineRouter } from './engine';
31
31
 
32
32
  // ============================================
@@ -34,6 +34,9 @@ import { engineRouter } from './engine';
34
34
  // ============================================
35
35
 
36
36
  export const wsRouter = createRouter()
37
+ // Authentication
38
+ .merge(authRouter)
39
+
37
40
  // Terminal System
38
41
  .merge(terminalRouter)
39
42
 
@@ -59,9 +62,6 @@ export const wsRouter = createRouter()
59
62
  // Git Source Control
60
63
  .merge(gitRouter)
61
64
 
62
- // MCP (bridge for Open Code stdio server)
63
- .merge(mcpRouter)
64
-
65
65
  // AI Engine Management
66
66
  .merge(engineRouter);
67
67
 
@@ -243,13 +243,35 @@ export const interactPreviewHandler = createRouter()
243
243
  await session.page.mouse.move(action.x!, action.y!, {
244
244
  steps: action.steps || 1 // Reduced from 5 to 1 for faster response
245
245
  });
246
- // Update cursor position in browser context (fire-and-forget, don't await)
246
+ // Update cursor position and detect cursor type in browser context (fire-and-forget, don't await)
247
+ // This replaces the disabled cursor-tracking script (blocked by CloudFlare)
247
248
  session.page.evaluate((data) => {
248
249
  const { x, y } = data;
249
- if ((window as any).__cursorInfo) {
250
- (window as any).__cursorInfo.x = x;
251
- (window as any).__cursorInfo.y = y;
252
- (window as any).__cursorInfo.timestamp = Date.now();
250
+ // Detect cursor type from element under mouse
251
+ let cursor = 'default';
252
+ try {
253
+ const el = document.elementFromPoint(x, y);
254
+ if (el) {
255
+ cursor = window.getComputedStyle(el).cursor || 'default';
256
+ }
257
+ } catch {}
258
+
259
+ // Initialize or update __cursorInfo
260
+ const existing = (window as any).__cursorInfo;
261
+ if (existing) {
262
+ existing.cursor = cursor;
263
+ existing.x = x;
264
+ existing.y = y;
265
+ existing.timestamp = Date.now();
266
+ existing.hasRecentInteraction = true;
267
+ } else {
268
+ (window as any).__cursorInfo = {
269
+ cursor,
270
+ x,
271
+ y,
272
+ timestamp: Date.now(),
273
+ hasRecentInteraction: true
274
+ };
253
275
  }
254
276
  }, { x: action.x!, y: action.y! }).catch(() => { /* Ignore evaluation errors */ });
255
277
  } catch (error) {