@agent-relay/dashboard 2.0.80 → 2.0.82

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 (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
@@ -0,0 +1,893 @@
1
+ /**
2
+ * Cloud API Client
3
+ *
4
+ * Handles authenticated requests to the Agent Relay Cloud API.
5
+ * Includes automatic session expiration detection and handling.
6
+ */
7
+
8
+ import { setCsrfToken as setApiCsrfToken } from './api';
9
+
10
+ // Session error codes from the backend
11
+ export type SessionErrorCode = 'SESSION_EXPIRED' | 'USER_NOT_FOUND' | 'SESSION_ERROR';
12
+
13
+ export interface SessionError {
14
+ error: string;
15
+ code: SessionErrorCode;
16
+ message: string;
17
+ }
18
+
19
+ export interface SessionStatus {
20
+ authenticated: boolean;
21
+ code?: SessionErrorCode;
22
+ message?: string;
23
+ user?: {
24
+ id: string;
25
+ githubUsername: string;
26
+ email?: string;
27
+ avatarUrl?: string;
28
+ plan: string;
29
+ };
30
+ }
31
+
32
+ export interface CloudUser {
33
+ id: string;
34
+ githubUsername: string;
35
+ email?: string;
36
+ avatarUrl?: string;
37
+ plan: string;
38
+ connectedProviders: Array<{
39
+ provider: string;
40
+ email?: string;
41
+ connectedAt: string;
42
+ }>;
43
+ pendingInvites: number;
44
+ onboardingCompleted: boolean;
45
+ displayName?: string;
46
+ }
47
+
48
+ export type SessionExpiredCallback = (error: SessionError) => void;
49
+
50
+ // Global session expiration listeners
51
+ const sessionExpiredListeners = new Set<SessionExpiredCallback>();
52
+
53
+ // Global CSRF token storage
54
+ let csrfToken: string | null = null;
55
+
56
+ /**
57
+ * Get the current CSRF token
58
+ */
59
+ export function getCsrfToken(): string | null {
60
+ return csrfToken;
61
+ }
62
+
63
+ /**
64
+ * Capture CSRF token from response headers
65
+ * Also syncs with the api.ts library for dashboard requests
66
+ */
67
+ function captureCsrfToken(response: Response): void {
68
+ const token = response.headers.get('X-CSRF-Token');
69
+ if (token) {
70
+ csrfToken = token;
71
+ // Sync with api.ts for dashboard-to-workspace requests
72
+ setApiCsrfToken(token);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Register a callback for when session expires
78
+ */
79
+ export function onSessionExpired(callback: SessionExpiredCallback): () => void {
80
+ sessionExpiredListeners.add(callback);
81
+ return () => sessionExpiredListeners.delete(callback);
82
+ }
83
+
84
+ /**
85
+ * Notify all listeners that session has expired
86
+ */
87
+ function notifySessionExpired(error: SessionError): void {
88
+ for (const listener of sessionExpiredListeners) {
89
+ try {
90
+ listener(error);
91
+ } catch (e) {
92
+ console.error('[cloudApi] Session expired listener error:', e);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check if response indicates session expiration
99
+ */
100
+ function isSessionError(response: Response, data: unknown): data is SessionError {
101
+ if (response.status === 401) {
102
+ return true;
103
+ }
104
+ if (typeof data === 'object' && data !== null) {
105
+ const obj = data as Record<string, unknown>;
106
+ return obj.code === 'SESSION_EXPIRED' || obj.code === 'USER_NOT_FOUND';
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Make an authenticated request to the cloud API
113
+ */
114
+ async function cloudFetch<T>(
115
+ endpoint: string,
116
+ options: RequestInit = {}
117
+ ): Promise<{ success: true; data: T } | { success: false; error: string; sessionExpired?: boolean }> {
118
+ try {
119
+ // Build headers, including CSRF token for non-GET requests
120
+ const headers: Record<string, string> = {
121
+ 'Content-Type': 'application/json',
122
+ ...(options.headers as Record<string, string>),
123
+ };
124
+
125
+ // Include CSRF token for state-changing requests
126
+ if (options.method && options.method !== 'GET' && csrfToken) {
127
+ headers['X-CSRF-Token'] = csrfToken;
128
+ }
129
+
130
+ const response = await fetch(endpoint, {
131
+ ...options,
132
+ credentials: 'include', // Include cookies for session
133
+ headers,
134
+ });
135
+
136
+ // Capture CSRF token from response
137
+ captureCsrfToken(response);
138
+
139
+ const data = await response.json();
140
+
141
+ if (isSessionError(response, data)) {
142
+ const error: SessionError = {
143
+ error: (data as SessionError).error || 'Session expired',
144
+ code: (data as SessionError).code || 'SESSION_EXPIRED',
145
+ message: (data as SessionError).message || 'Your session has expired. Please log in again.',
146
+ };
147
+ notifySessionExpired(error);
148
+ return { success: false, error: error.message, sessionExpired: true };
149
+ }
150
+
151
+ if (!response.ok) {
152
+ return {
153
+ success: false,
154
+ error: (data as { error?: string }).error || `Request failed with status ${response.status}`
155
+ };
156
+ }
157
+
158
+ return { success: true, data: data as T };
159
+ } catch (error) {
160
+ return {
161
+ success: false,
162
+ error: error instanceof Error ? error.message : 'Network error'
163
+ };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Cloud API methods
169
+ */
170
+ // ===== Nango Auth Types =====
171
+
172
+ export interface NangoLoginSession {
173
+ sessionToken: string;
174
+ tempUserId: string;
175
+ }
176
+
177
+ export interface NangoLoginStatus {
178
+ ready: boolean;
179
+ user?: {
180
+ id: string;
181
+ githubUsername: string;
182
+ email?: string;
183
+ avatarUrl?: string;
184
+ plan: string;
185
+ };
186
+ }
187
+
188
+ export interface NangoRepoSession {
189
+ sessionToken: string;
190
+ }
191
+
192
+ export interface NangoRepoStatus {
193
+ ready: boolean;
194
+ pendingApproval?: boolean;
195
+ message?: string;
196
+ repos?: Array<{
197
+ id: string;
198
+ fullName: string;
199
+ isPrivate: boolean;
200
+ defaultBranch: string;
201
+ }>;
202
+ }
203
+
204
+ export const cloudApi = {
205
+ // ===== Nango Auth =====
206
+
207
+ /**
208
+ * Get a Nango connect session for GitHub login
209
+ */
210
+ async getNangoLoginSession(): Promise<{ success: true; data: NangoLoginSession } | { success: false; error: string }> {
211
+ try {
212
+ const response = await fetch('/api/auth/nango/login-session', {
213
+ credentials: 'include',
214
+ });
215
+ // Capture CSRF token from response
216
+ captureCsrfToken(response);
217
+ const data = await response.json();
218
+ if (!response.ok) {
219
+ return { success: false, error: data.error || 'Failed to create login session' };
220
+ }
221
+ return { success: true, data: data as NangoLoginSession };
222
+ } catch (error) {
223
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
224
+ }
225
+ },
226
+
227
+ /**
228
+ * Poll for login completion after Nango connect UI
229
+ */
230
+ async checkNangoLoginStatus(connectionId: string): Promise<{ success: true; data: NangoLoginStatus } | { success: false; error: string }> {
231
+ try {
232
+ const response = await fetch(`/api/auth/nango/login-status/${encodeURIComponent(connectionId)}`, {
233
+ credentials: 'include',
234
+ });
235
+ // Capture CSRF token from response
236
+ captureCsrfToken(response);
237
+ const data = await response.json();
238
+ if (!response.ok) {
239
+ return { success: false, error: data.error || 'Failed to check login status' };
240
+ }
241
+ return { success: true, data: data as NangoLoginStatus };
242
+ } catch (error) {
243
+ return { success: false, error: error instanceof Error ? error.message : 'Network error' };
244
+ }
245
+ },
246
+
247
+ /**
248
+ * Get a Nango connect session for GitHub App OAuth (repo access)
249
+ */
250
+ async getNangoRepoSession(): Promise<{ success: true; data: NangoRepoSession } | { success: false; error: string; sessionExpired?: boolean }> {
251
+ return cloudFetch<NangoRepoSession>('/api/auth/nango/repo-session');
252
+ },
253
+
254
+ /**
255
+ * Poll for repo sync completion after GitHub App OAuth
256
+ */
257
+ async checkNangoRepoStatus(connectionId: string): Promise<{ success: true; data: NangoRepoStatus } | { success: false; error: string; sessionExpired?: boolean }> {
258
+ return cloudFetch<NangoRepoStatus>(`/api/auth/nango/repo-status/${encodeURIComponent(connectionId)}`);
259
+ },
260
+
261
+ /**
262
+ * Check current session status
263
+ */
264
+ async checkSession(): Promise<SessionStatus> {
265
+ try {
266
+ const response = await fetch('/api/auth/session', {
267
+ credentials: 'include',
268
+ });
269
+ // Capture CSRF token from response
270
+ captureCsrfToken(response);
271
+ const data = await response.json();
272
+ return data as SessionStatus;
273
+ } catch {
274
+ return {
275
+ authenticated: false,
276
+ code: 'SESSION_ERROR',
277
+ message: 'Failed to check session status',
278
+ };
279
+ }
280
+ },
281
+
282
+ /**
283
+ * Get current user profile
284
+ */
285
+ async getMe() {
286
+ return cloudFetch<CloudUser>('/api/auth/me');
287
+ },
288
+
289
+ /**
290
+ * Logout current user
291
+ */
292
+ async logout(): Promise<{ success: boolean; error?: string }> {
293
+ try {
294
+ const headers: Record<string, string> = {};
295
+ if (csrfToken) {
296
+ headers['X-CSRF-Token'] = csrfToken;
297
+ }
298
+ const response = await fetch('/api/auth/logout', {
299
+ method: 'POST',
300
+ credentials: 'include',
301
+ headers,
302
+ });
303
+ const data = await response.json();
304
+ return data as { success: boolean; error?: string };
305
+ } catch {
306
+ return { success: false, error: 'Network error' };
307
+ }
308
+ },
309
+
310
+ // ===== Workspace API =====
311
+
312
+ /**
313
+ * Get user's workspaces
314
+ */
315
+ async getWorkspaces() {
316
+ return cloudFetch<{ workspaces: Array<{
317
+ id: string;
318
+ name: string;
319
+ slug: string;
320
+ repositories: number;
321
+ members: number;
322
+ plan: string;
323
+ }> }>('/api/workspaces');
324
+ },
325
+
326
+ /**
327
+ * Get workspace by ID
328
+ */
329
+ async getWorkspace(id: string) {
330
+ return cloudFetch<{
331
+ id: string;
332
+ name: string;
333
+ slug: string;
334
+ config: Record<string, unknown>;
335
+ createdAt: string;
336
+ }>(`/api/workspaces/${encodeURIComponent(id)}`);
337
+ },
338
+
339
+ /**
340
+ * Create workspace
341
+ */
342
+ async createWorkspace(data: { name: string; slug?: string }) {
343
+ return cloudFetch<{ id: string; name: string; slug: string }>('/api/workspaces', {
344
+ method: 'POST',
345
+ body: JSON.stringify(data),
346
+ });
347
+ },
348
+
349
+ /**
350
+ * Get primary workspace with live status
351
+ */
352
+ async getPrimaryWorkspace() {
353
+ return cloudFetch<{
354
+ exists: boolean;
355
+ message?: string;
356
+ workspace?: {
357
+ id: string;
358
+ name: string;
359
+ status: string;
360
+ publicUrl?: string;
361
+ isStopped: boolean;
362
+ isRunning: boolean;
363
+ isProvisioning: boolean;
364
+ hasError: boolean;
365
+ config: {
366
+ providers: string[];
367
+ repositories: string[];
368
+ };
369
+ };
370
+ statusMessage: string;
371
+ actionNeeded?: 'wakeup' | 'check_error' | null;
372
+ }>('/api/workspaces/primary');
373
+ },
374
+
375
+ /**
376
+ * Get workspace summary (all workspaces with status)
377
+ */
378
+ async getWorkspaceSummary() {
379
+ return cloudFetch<{
380
+ workspaces: Array<{
381
+ id: string;
382
+ name: string;
383
+ status: string;
384
+ publicUrl?: string;
385
+ isStopped: boolean;
386
+ isRunning: boolean;
387
+ isProvisioning: boolean;
388
+ hasError: boolean;
389
+ }>;
390
+ summary: {
391
+ total: number;
392
+ running: number;
393
+ stopped: number;
394
+ provisioning: number;
395
+ error: number;
396
+ };
397
+ overallStatus: 'ready' | 'provisioning' | 'stopped' | 'none' | 'error';
398
+ }>('/api/workspaces/summary');
399
+ },
400
+
401
+ /**
402
+ * Get all accessible workspaces (owned + member + contributor via GitHub repos)
403
+ * This is the preferred method for the workspace selector dropdown
404
+ */
405
+ async getAccessibleWorkspaces() {
406
+ return cloudFetch<{
407
+ workspaces: Array<{
408
+ id: string;
409
+ name: string;
410
+ status: string;
411
+ publicUrl?: string;
412
+ providers?: string[];
413
+ repositories?: string[];
414
+ accessType: 'owner' | 'member' | 'contributor';
415
+ permission: 'admin' | 'write' | 'read';
416
+ createdAt: string;
417
+ }>;
418
+ summary: {
419
+ owned: number;
420
+ member: number;
421
+ contributor: number;
422
+ total: number;
423
+ };
424
+ }>('/api/workspaces/accessible');
425
+ },
426
+
427
+ /**
428
+ * Get workspace status (live polling from compute provider)
429
+ */
430
+ async getWorkspaceStatus(id: string) {
431
+ return cloudFetch<{ status: string }>(`/api/workspaces/${encodeURIComponent(id)}/status`);
432
+ },
433
+
434
+ /**
435
+ * Wake up a stopped workspace
436
+ */
437
+ async wakeupWorkspace(id: string) {
438
+ return cloudFetch<{
439
+ status: string;
440
+ wasRestarted: boolean;
441
+ message: string;
442
+ estimatedStartTime?: number;
443
+ publicUrl?: string;
444
+ }>(`/api/workspaces/${encodeURIComponent(id)}/wakeup`, {
445
+ method: 'POST',
446
+ });
447
+ },
448
+
449
+ /**
450
+ * Restart a workspace.
451
+ * Automatically handles error/stuck states by force-destroying and reprovisioning.
452
+ */
453
+ async restartWorkspace(id: string) {
454
+ return cloudFetch<{ success: boolean; action: 'restarted' | 'reprovisioning'; message: string }>(
455
+ `/api/workspaces/${encodeURIComponent(id)}/restart`,
456
+ { method: 'POST' }
457
+ );
458
+ },
459
+
460
+ /**
461
+ * Rebuild/reprovision a workspace from scratch.
462
+ * Destroys existing infrastructure and creates a new machine.
463
+ */
464
+ async rebuildWorkspace(id: string) {
465
+ return cloudFetch<{ success: boolean; message: string }>(
466
+ `/api/workspaces/${encodeURIComponent(id)}/rebuild`,
467
+ { method: 'POST' }
468
+ );
469
+ },
470
+
471
+ // ===== Provider API =====
472
+
473
+ /**
474
+ * Get connected providers for a workspace
475
+ * @param workspaceId - Workspace to get providers for
476
+ */
477
+ async getProviders(workspaceId: string) {
478
+ // Validate workspaceId is a proper UUID, not 'default' or other placeholder
479
+ if (!workspaceId || workspaceId === 'default' || !/^[0-9a-f-]{36}$/i.test(workspaceId)) {
480
+ return { success: false as const, error: 'Invalid workspace ID' };
481
+ }
482
+ return cloudFetch<{ providers: Array<{
483
+ id: string;
484
+ name: string;
485
+ displayName: string;
486
+ description: string;
487
+ color: string;
488
+ authStrategy: string;
489
+ cliCommand?: string;
490
+ isConnected: boolean;
491
+ connectedAs?: string;
492
+ connectedAt?: string;
493
+ }> }>(`/api/providers?workspaceId=${encodeURIComponent(workspaceId)}`);
494
+ },
495
+
496
+ /**
497
+ * Disconnect a provider from a workspace
498
+ * @param provider - Provider ID to disconnect
499
+ * @param workspaceId - Workspace to disconnect from
500
+ */
501
+ async disconnectProvider(provider: string, workspaceId: string) {
502
+ return cloudFetch<{ success: boolean }>(`/api/providers/${encodeURIComponent(provider)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
503
+ method: 'DELETE',
504
+ });
505
+ },
506
+
507
+ // ===== Team API =====
508
+
509
+ /**
510
+ * Get workspace members
511
+ */
512
+ async getWorkspaceMembers(workspaceId: string) {
513
+ return cloudFetch<{ members: Array<{
514
+ id: string;
515
+ userId: string;
516
+ role: string;
517
+ isPending: boolean;
518
+ user?: {
519
+ githubUsername: string;
520
+ email?: string;
521
+ avatarUrl?: string;
522
+ };
523
+ }> }>(`/api/workspaces/${encodeURIComponent(workspaceId)}/members`);
524
+ },
525
+
526
+ /**
527
+ * Get repo collaborators (users with GitHub access who aren't workspace members)
528
+ */
529
+ async getRepoCollaborators(workspaceId: string) {
530
+ return cloudFetch<{
531
+ collaborators: Array<{
532
+ id: number;
533
+ login: string;
534
+ avatarUrl: string;
535
+ permission: 'admin' | 'write' | 'read' | 'none';
536
+ repos: string[];
537
+ }>;
538
+ totalRepos: number;
539
+ message?: string;
540
+ }>(`/api/workspaces/${encodeURIComponent(workspaceId)}/repo-collaborators`);
541
+ },
542
+
543
+ /**
544
+ * Invite user to workspace
545
+ */
546
+ async inviteMember(workspaceId: string, githubUsername: string, role = 'member') {
547
+ return cloudFetch<{ success: boolean; member: unknown }>(
548
+ `/api/workspaces/${encodeURIComponent(workspaceId)}/members`,
549
+ {
550
+ method: 'POST',
551
+ body: JSON.stringify({ githubUsername, role }),
552
+ }
553
+ );
554
+ },
555
+
556
+ /**
557
+ * Get pending invites for current user
558
+ */
559
+ async getPendingInvites() {
560
+ return cloudFetch<{ invites: Array<{
561
+ id: string;
562
+ workspaceId: string;
563
+ workspaceName: string;
564
+ role: string;
565
+ invitedAt: string;
566
+ invitedBy: string;
567
+ }> }>('/api/invites');
568
+ },
569
+
570
+ /**
571
+ * Accept workspace invite
572
+ */
573
+ async acceptInvite(inviteId: string) {
574
+ return cloudFetch<{ success: boolean; workspaceId: string }>(
575
+ `/api/invites/${encodeURIComponent(inviteId)}/accept`,
576
+ { method: 'POST' }
577
+ );
578
+ },
579
+
580
+ /**
581
+ * Decline workspace invite
582
+ */
583
+ async declineInvite(inviteId: string) {
584
+ return cloudFetch<{ success: boolean }>(
585
+ `/api/invites/${encodeURIComponent(inviteId)}/decline`,
586
+ { method: 'POST' }
587
+ );
588
+ },
589
+
590
+ /**
591
+ * Update member role
592
+ */
593
+ async updateMemberRole(workspaceId: string, memberId: string, role: string) {
594
+ return cloudFetch<{ success: boolean; role: string }>(
595
+ `/api/workspaces/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(memberId)}`,
596
+ {
597
+ method: 'PATCH',
598
+ body: JSON.stringify({ role }),
599
+ }
600
+ );
601
+ },
602
+
603
+ /**
604
+ * Remove member from workspace
605
+ */
606
+ async removeMember(workspaceId: string, memberId: string) {
607
+ return cloudFetch<{ success: boolean }>(
608
+ `/api/workspaces/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(memberId)}`,
609
+ { method: 'DELETE' }
610
+ );
611
+ },
612
+
613
+ // ===== Billing API =====
614
+
615
+ /**
616
+ * Get all billing plans
617
+ */
618
+ async getBillingPlans() {
619
+ return cloudFetch<{
620
+ plans: Array<{
621
+ tier: string;
622
+ name: string;
623
+ description: string;
624
+ price: { monthly: number; yearly: number };
625
+ features: string[];
626
+ limits: Record<string, number>;
627
+ recommended?: boolean;
628
+ }>;
629
+ publishableKey: string;
630
+ }>('/api/billing/plans');
631
+ },
632
+
633
+ /**
634
+ * Get current subscription status
635
+ */
636
+ async getSubscription() {
637
+ return cloudFetch<{
638
+ tier: string;
639
+ subscription: {
640
+ id: string;
641
+ tier: string;
642
+ status: string;
643
+ currentPeriodStart: string;
644
+ currentPeriodEnd: string;
645
+ cancelAtPeriodEnd: boolean;
646
+ interval: 'month' | 'year';
647
+ } | null;
648
+ customer: {
649
+ id: string;
650
+ email: string;
651
+ name?: string;
652
+ paymentMethods: Array<{
653
+ id: string;
654
+ type: string;
655
+ last4?: string;
656
+ brand?: string;
657
+ isDefault: boolean;
658
+ }>;
659
+ invoices: Array<{
660
+ id: string;
661
+ number: string;
662
+ amount: number;
663
+ status: string;
664
+ date: string;
665
+ pdfUrl?: string;
666
+ }>;
667
+ } | null;
668
+ }>('/api/billing/subscription');
669
+ },
670
+
671
+ /**
672
+ * Create checkout session for new subscription
673
+ */
674
+ async createCheckoutSession(tier: string, interval: 'month' | 'year' = 'month') {
675
+ return cloudFetch<{
676
+ sessionId: string;
677
+ checkoutUrl: string;
678
+ }>('/api/billing/checkout', {
679
+ method: 'POST',
680
+ body: JSON.stringify({ tier, interval }),
681
+ });
682
+ },
683
+
684
+ /**
685
+ * Create billing portal session
686
+ */
687
+ async createBillingPortal() {
688
+ return cloudFetch<{
689
+ sessionId: string;
690
+ portalUrl: string;
691
+ }>('/api/billing/portal', {
692
+ method: 'POST',
693
+ });
694
+ },
695
+
696
+ /**
697
+ * Change subscription tier
698
+ */
699
+ async changeSubscription(tier: string, interval: 'month' | 'year' = 'month') {
700
+ return cloudFetch<{
701
+ subscription: {
702
+ tier: string;
703
+ status: string;
704
+ };
705
+ }>('/api/billing/change', {
706
+ method: 'POST',
707
+ body: JSON.stringify({ tier, interval }),
708
+ });
709
+ },
710
+
711
+ /**
712
+ * Cancel subscription at period end
713
+ */
714
+ async cancelSubscription() {
715
+ return cloudFetch<{
716
+ subscription: { cancelAtPeriodEnd: boolean; currentPeriodEnd: string };
717
+ message: string;
718
+ }>('/api/billing/cancel', {
719
+ method: 'POST',
720
+ });
721
+ },
722
+
723
+ /**
724
+ * Resume cancelled subscription
725
+ */
726
+ async resumeSubscription() {
727
+ return cloudFetch<{
728
+ subscription: { cancelAtPeriodEnd: boolean };
729
+ message: string;
730
+ }>('/api/billing/resume', {
731
+ method: 'POST',
732
+ });
733
+ },
734
+
735
+ /**
736
+ * Get invoices
737
+ */
738
+ async getInvoices() {
739
+ return cloudFetch<{
740
+ invoices: Array<{
741
+ id: string;
742
+ number: string;
743
+ amount: number;
744
+ status: string;
745
+ date: string;
746
+ pdfUrl?: string;
747
+ }>;
748
+ }>('/api/billing/invoices');
749
+ },
750
+
751
+ // ===== Workspace Management API =====
752
+
753
+ /**
754
+ * Stop workspace
755
+ */
756
+ async stopWorkspace(id: string) {
757
+ return cloudFetch<{ success: boolean; message: string }>(
758
+ `/api/workspaces/${encodeURIComponent(id)}/stop`,
759
+ { method: 'POST' }
760
+ );
761
+ },
762
+
763
+ /**
764
+ * Delete workspace
765
+ */
766
+ async deleteWorkspace(id: string) {
767
+ return cloudFetch<{ success: boolean; message: string }>(
768
+ `/api/workspaces/${encodeURIComponent(id)}`,
769
+ { method: 'DELETE' }
770
+ );
771
+ },
772
+
773
+ /**
774
+ * Add repositories to workspace
775
+ */
776
+ async addReposToWorkspace(workspaceId: string, repositoryIds: string[]) {
777
+ return cloudFetch<{ success: boolean; message: string }>(
778
+ `/api/workspaces/${encodeURIComponent(workspaceId)}/repos`,
779
+ {
780
+ method: 'POST',
781
+ body: JSON.stringify({ repositoryIds }),
782
+ }
783
+ );
784
+ },
785
+
786
+ /**
787
+ * Set custom domain for workspace
788
+ */
789
+ async setCustomDomain(workspaceId: string, domain: string) {
790
+ return cloudFetch<{
791
+ success: boolean;
792
+ domain: string;
793
+ status: string;
794
+ instructions: {
795
+ type: string;
796
+ name: string;
797
+ value: string;
798
+ ttl: number;
799
+ };
800
+ verifyEndpoint: string;
801
+ message: string;
802
+ }>(`/api/workspaces/${encodeURIComponent(workspaceId)}/domain`, {
803
+ method: 'POST',
804
+ body: JSON.stringify({ domain }),
805
+ });
806
+ },
807
+
808
+ /**
809
+ * Verify custom domain
810
+ */
811
+ async verifyCustomDomain(workspaceId: string) {
812
+ return cloudFetch<{
813
+ success: boolean;
814
+ status: string;
815
+ domain?: string;
816
+ message?: string;
817
+ error?: string;
818
+ }>(`/api/workspaces/${encodeURIComponent(workspaceId)}/domain/verify`, {
819
+ method: 'POST',
820
+ });
821
+ },
822
+
823
+ /**
824
+ * Remove custom domain
825
+ */
826
+ async removeCustomDomain(workspaceId: string) {
827
+ return cloudFetch<{ success: boolean; message: string }>(
828
+ `/api/workspaces/${encodeURIComponent(workspaceId)}/domain`,
829
+ { method: 'DELETE' }
830
+ );
831
+ },
832
+
833
+ /**
834
+ * Get detailed workspace info
835
+ */
836
+ async getWorkspaceDetails(id: string) {
837
+ return cloudFetch<{
838
+ id: string;
839
+ name: string;
840
+ status: string;
841
+ publicUrl?: string;
842
+ computeProvider: string;
843
+ config: {
844
+ providers: string[];
845
+ repositories: string[];
846
+ supervisorEnabled?: boolean;
847
+ maxAgents?: number;
848
+ };
849
+ customDomain?: string;
850
+ customDomainStatus?: string;
851
+ errorMessage?: string;
852
+ repositories: Array<{
853
+ id: string;
854
+ fullName: string;
855
+ syncStatus: string;
856
+ lastSyncedAt?: string;
857
+ }>;
858
+ createdAt: string;
859
+ updatedAt: string;
860
+ }>(`/api/workspaces/${encodeURIComponent(id)}`);
861
+ },
862
+
863
+ // ===== GitHub App API =====
864
+
865
+ /**
866
+ * Get user's connected repositories
867
+ */
868
+ async getRepos() {
869
+ return cloudFetch<{ repositories: Array<{
870
+ id: string;
871
+ fullName: string;
872
+ isPrivate: boolean;
873
+ defaultBranch: string;
874
+ syncStatus: string;
875
+ hasNangoConnection: boolean;
876
+ lastSyncedAt?: string;
877
+ }> }>('/api/github-app/repos');
878
+ },
879
+
880
+ /**
881
+ * Sync a repository to its workspace
882
+ * Triggers clone/pull on the workspace container
883
+ */
884
+ async syncRepo(repoId: string) {
885
+ return cloudFetch<{
886
+ message: string;
887
+ syncStatus: string;
888
+ result?: unknown;
889
+ }>(`/api/repos/${encodeURIComponent(repoId)}/sync`, {
890
+ method: 'POST',
891
+ });
892
+ },
893
+ };