agent-relay 2.0.16 → 2.0.18

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 (153) hide show
  1. package/deploy/workspace/entrypoint.sh +35 -19
  2. package/deploy/workspace/git-credential-relay +82 -7
  3. package/dist/dashboard/out/404.html +1 -1
  4. package/dist/dashboard/out/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
  5. package/dist/dashboard/out/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
  6. package/{packages/dashboard/ui-dist/_next/static/chunks/app/app/page-9d6bc8729b429956.js → dist/dashboard/out/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
  7. package/dist/dashboard/out/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
  8. package/dist/dashboard/out/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
  9. package/dist/dashboard/out/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
  10. package/dist/dashboard/out/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
  11. package/dist/dashboard/out/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
  12. package/dist/dashboard/out/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
  13. package/dist/dashboard/out/app/onboarding.html +1 -1
  14. package/dist/dashboard/out/app/onboarding.txt +1 -1
  15. package/dist/dashboard/out/app.html +1 -1
  16. package/dist/dashboard/out/app.txt +2 -2
  17. package/dist/dashboard/out/cloud/link.html +1 -1
  18. package/dist/dashboard/out/cloud/link.txt +1 -1
  19. package/dist/dashboard/out/complete-profile.html +5 -0
  20. package/dist/dashboard/out/complete-profile.txt +7 -0
  21. package/dist/dashboard/out/connect-repos.html +1 -1
  22. package/dist/dashboard/out/connect-repos.txt +1 -1
  23. package/dist/dashboard/out/history.html +1 -1
  24. package/dist/dashboard/out/history.txt +1 -1
  25. package/dist/dashboard/out/index.html +1 -1
  26. package/dist/dashboard/out/index.txt +2 -2
  27. package/dist/dashboard/out/login.html +2 -2
  28. package/dist/dashboard/out/login.txt +2 -2
  29. package/dist/dashboard/out/metrics.html +1 -1
  30. package/dist/dashboard/out/metrics.txt +1 -1
  31. package/dist/dashboard/out/pricing.html +2 -2
  32. package/dist/dashboard/out/pricing.txt +1 -1
  33. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  34. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  35. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  36. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  37. package/dist/dashboard/out/providers/setup/cursor.html +1 -1
  38. package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
  39. package/dist/dashboard/out/providers.html +1 -1
  40. package/dist/dashboard/out/providers.txt +2 -2
  41. package/dist/dashboard/out/signup.html +2 -2
  42. package/dist/dashboard/out/signup.txt +2 -2
  43. package/dist/src/cli/index.js +3 -1
  44. package/package.json +22 -21
  45. package/packages/api-types/package.json +1 -1
  46. package/packages/bridge/package.json +8 -8
  47. package/packages/cloud/dist/api/auth.js +2 -0
  48. package/packages/cloud/dist/api/billing.js +4 -4
  49. package/packages/cloud/dist/api/email-auth.d.ts +11 -0
  50. package/packages/cloud/dist/api/email-auth.js +347 -0
  51. package/packages/cloud/dist/api/nango-auth.js +72 -5
  52. package/packages/cloud/dist/db/drizzle.d.ts +35 -1
  53. package/packages/cloud/dist/db/drizzle.js +136 -0
  54. package/packages/cloud/dist/db/index.d.ts +5 -4
  55. package/packages/cloud/dist/db/index.js +5 -3
  56. package/packages/cloud/dist/db/schema.d.ts +246 -2
  57. package/packages/cloud/dist/db/schema.js +39 -3
  58. package/packages/cloud/dist/provisioner/index.js +5 -1
  59. package/packages/cloud/dist/server.js +134 -24
  60. package/packages/cloud/dist/services/nango.d.ts +18 -0
  61. package/packages/cloud/dist/services/nango.js +32 -0
  62. package/packages/cloud/package.json +6 -6
  63. package/packages/config/package.json +2 -2
  64. package/packages/continuity/package.json +1 -1
  65. package/packages/daemon/package.json +12 -12
  66. package/packages/dashboard/dist/server.js +36 -7
  67. package/packages/dashboard/package.json +13 -13
  68. package/packages/dashboard/ui/app/complete-profile/page.tsx +204 -0
  69. package/packages/dashboard/ui/app/login/page.tsx +182 -38
  70. package/packages/dashboard/ui/app/signup/page.tsx +244 -54
  71. package/packages/dashboard/ui/lib/cloudApi.ts +1 -0
  72. package/packages/dashboard/ui/react-components/App.tsx +1 -1
  73. package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +10 -0
  74. package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +160 -3
  75. package/packages/dashboard/ui-dist/404.html +1 -1
  76. package/packages/dashboard/ui-dist/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
  77. package/packages/dashboard/ui-dist/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
  78. package/{dist/dashboard/out/_next/static/chunks/app/app/page-9d6bc8729b429956.js → packages/dashboard/ui-dist/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
  79. package/packages/dashboard/ui-dist/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
  80. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
  81. package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
  82. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
  83. package/packages/dashboard/ui-dist/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
  84. package/packages/dashboard/ui-dist/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
  85. package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
  86. package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
  87. package/packages/dashboard/ui-dist/app.html +1 -1
  88. package/packages/dashboard/ui-dist/app.txt +2 -2
  89. package/packages/dashboard/ui-dist/cloud/link.html +1 -1
  90. package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
  91. package/packages/dashboard/ui-dist/complete-profile.html +5 -0
  92. package/packages/dashboard/ui-dist/complete-profile.txt +7 -0
  93. package/packages/dashboard/ui-dist/connect-repos.html +1 -1
  94. package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
  95. package/packages/dashboard/ui-dist/history.html +1 -1
  96. package/packages/dashboard/ui-dist/history.txt +1 -1
  97. package/packages/dashboard/ui-dist/index.html +1 -1
  98. package/packages/dashboard/ui-dist/index.txt +2 -2
  99. package/packages/dashboard/ui-dist/login.html +2 -2
  100. package/packages/dashboard/ui-dist/login.txt +2 -2
  101. package/packages/dashboard/ui-dist/metrics.html +1 -1
  102. package/packages/dashboard/ui-dist/metrics.txt +1 -1
  103. package/packages/dashboard/ui-dist/pricing.html +2 -2
  104. package/packages/dashboard/ui-dist/pricing.txt +1 -1
  105. package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
  106. package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
  107. package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
  108. package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
  109. package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
  110. package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
  111. package/packages/dashboard/ui-dist/providers.html +1 -1
  112. package/packages/dashboard/ui-dist/providers.txt +2 -2
  113. package/packages/dashboard/ui-dist/signup.html +2 -2
  114. package/packages/dashboard/ui-dist/signup.txt +2 -2
  115. package/packages/dashboard-server/dist/server.js +36 -7
  116. package/packages/dashboard-server/package.json +12 -12
  117. package/packages/hooks/package.json +4 -4
  118. package/packages/mcp/package.json +2 -2
  119. package/packages/memory/package.json +2 -2
  120. package/packages/policy/package.json +2 -2
  121. package/packages/protocol/package.json +1 -1
  122. package/packages/resiliency/package.json +1 -1
  123. package/packages/sdk/package.json +2 -2
  124. package/packages/spawner/package.json +1 -1
  125. package/packages/state/package.json +1 -1
  126. package/packages/storage/package.json +2 -2
  127. package/packages/telemetry/package.json +1 -1
  128. package/packages/trajectory/package.json +2 -2
  129. package/packages/user-directory/package.json +2 -2
  130. package/packages/utils/package.json +1 -1
  131. package/packages/wrapper/dist/relay-pty-orchestrator.js +17 -3
  132. package/packages/wrapper/package.json +6 -6
  133. package/relay-snippets/agent-policy-snippet.md +40 -0
  134. package/relay-snippets/agent-relay-protocol.md +101 -0
  135. package/relay-snippets/agent-relay-snippet.md +177 -0
  136. package/SESSION_HANDOFF.md +0 -67
  137. package/dist/dashboard/out/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
  138. package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
  139. package/dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
  140. package/dist/dashboard/out/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
  141. package/packages/dashboard/ui-dist/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
  142. package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
  143. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
  144. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
  145. package/test-push.txt +0 -1
  146. /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
  147. /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
  148. /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
  149. /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
  150. /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_buildManifest.js +0 -0
  151. /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_ssgManifest.js +0 -0
  152. /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_buildManifest.js +0 -0
  153. /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_ssgManifest.js +0 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Complete Profile Page
3
+ *
4
+ * Prompts users to provide missing information after GitHub OAuth signup.
5
+ * Currently used to collect email address for GitHub users whose email
6
+ * is not publicly available.
7
+ */
8
+
9
+ 'use client';
10
+
11
+ import React, { useState, useEffect } from 'react';
12
+ import { LogoIcon } from '../../react-components/Logo';
13
+
14
+ export default function CompleteProfilePage() {
15
+ const [email, setEmail] = useState('');
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ const [isSubmitting, setIsSubmitting] = useState(false);
18
+ const [error, setError] = useState('');
19
+ const [user, setUser] = useState<{
20
+ githubUsername?: string;
21
+ avatarUrl?: string;
22
+ email?: string;
23
+ } | null>(null);
24
+
25
+ // Check if user is logged in and needs email
26
+ useEffect(() => {
27
+ const checkSession = async () => {
28
+ try {
29
+ const response = await fetch('/api/auth/me', { credentials: 'include' });
30
+ if (!response.ok) {
31
+ // Not logged in - redirect to login
32
+ window.location.href = '/login';
33
+ return;
34
+ }
35
+
36
+ const data = await response.json();
37
+
38
+ // If user already has email, redirect to app
39
+ if (data.user?.email) {
40
+ window.location.href = '/app';
41
+ return;
42
+ }
43
+
44
+ setUser(data.user);
45
+ setIsLoading(false);
46
+ } catch (err) {
47
+ console.error('Error checking session:', err);
48
+ window.location.href = '/login';
49
+ }
50
+ };
51
+
52
+ checkSession();
53
+ }, []);
54
+
55
+ const handleSubmit = async (e: React.FormEvent) => {
56
+ e.preventDefault();
57
+ setError('');
58
+ setIsSubmitting(true);
59
+
60
+ try {
61
+ // Get CSRF token first
62
+ const csrfResponse = await fetch('/api/auth/session', { credentials: 'include' });
63
+ const csrfToken = csrfResponse.headers.get('x-csrf-token');
64
+
65
+ const response = await fetch('/api/auth/email/set-email', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ ...(csrfToken && { 'x-csrf-token': csrfToken }),
70
+ },
71
+ credentials: 'include',
72
+ body: JSON.stringify({ email }),
73
+ });
74
+
75
+ const data = await response.json();
76
+
77
+ if (!response.ok) {
78
+ setError(data.error || 'Failed to set email');
79
+ setIsSubmitting(false);
80
+ return;
81
+ }
82
+
83
+ // Success - redirect to app
84
+ window.location.href = '/app';
85
+ } catch (err) {
86
+ console.error('Error setting email:', err);
87
+ setError('Failed to connect. Please try again.');
88
+ setIsSubmitting(false);
89
+ }
90
+ };
91
+
92
+ if (isLoading) {
93
+ return (
94
+ <div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex flex-col items-center justify-center p-4">
95
+ <div className="relative z-10 w-full max-w-md">
96
+ <div className="flex flex-col items-center mb-8">
97
+ <LogoIcon size={48} withGlow={true} />
98
+ <h1 className="mt-4 text-2xl font-bold text-white">Agent Relay</h1>
99
+ <p className="mt-2 text-text-muted">Loading...</p>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
105
+
106
+ return (
107
+ <div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex flex-col items-center justify-center p-4">
108
+ {/* Background grid */}
109
+ <div className="fixed inset-0 opacity-10">
110
+ <div
111
+ className="absolute inset-0"
112
+ style={{
113
+ backgroundImage: `linear-gradient(rgba(0, 217, 255, 0.1) 1px, transparent 1px),
114
+ linear-gradient(90deg, rgba(0, 217, 255, 0.1) 1px, transparent 1px)`,
115
+ backgroundSize: '50px 50px',
116
+ }}
117
+ />
118
+ </div>
119
+
120
+ {/* Content */}
121
+ <div className="relative z-10 w-full max-w-md">
122
+ {/* Logo */}
123
+ <div className="flex flex-col items-center mb-8">
124
+ <LogoIcon size={48} withGlow={true} />
125
+ <h1 className="mt-4 text-2xl font-bold text-white">Almost there!</h1>
126
+ <p className="mt-2 text-text-muted text-center">
127
+ Please provide your email to complete your account setup
128
+ </p>
129
+ </div>
130
+
131
+ {/* Card */}
132
+ <div className="bg-bg-primary/80 backdrop-blur-sm border border-border-subtle rounded-2xl p-8 shadow-xl">
133
+ {/* User info */}
134
+ {user && (
135
+ <div className="flex items-center gap-4 mb-6 pb-6 border-b border-border-subtle">
136
+ {user.avatarUrl ? (
137
+ <img
138
+ src={user.avatarUrl}
139
+ alt={user.githubUsername || 'User'}
140
+ className="w-12 h-12 rounded-full"
141
+ />
142
+ ) : (
143
+ <div className="w-12 h-12 rounded-full bg-bg-secondary flex items-center justify-center">
144
+ <svg className="w-6 h-6 text-text-muted" fill="currentColor" viewBox="0 0 24 24">
145
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
146
+ </svg>
147
+ </div>
148
+ )}
149
+ <div>
150
+ <p className="font-medium text-white">{user.githubUsername}</p>
151
+ <p className="text-sm text-text-muted">Connected via GitHub</p>
152
+ </div>
153
+ </div>
154
+ )}
155
+
156
+ {error && (
157
+ <div className="mb-4 p-3 bg-error/10 border border-error/20 rounded-lg">
158
+ <p className="text-error text-sm">{error}</p>
159
+ </div>
160
+ )}
161
+
162
+ <form onSubmit={handleSubmit} className="space-y-4">
163
+ <div>
164
+ <label htmlFor="email" className="block text-sm font-medium text-text-secondary mb-2">
165
+ Email Address
166
+ </label>
167
+ <input
168
+ type="email"
169
+ id="email"
170
+ value={email}
171
+ onChange={(e) => setEmail(e.target.value)}
172
+ required
173
+ disabled={isSubmitting}
174
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
175
+ placeholder="you@example.com"
176
+ />
177
+ <p className="mt-2 text-xs text-text-muted">
178
+ We'll use this email to notify you about important updates and account activity.
179
+ </p>
180
+ </div>
181
+
182
+ <button
183
+ type="submit"
184
+ disabled={isSubmitting || !email}
185
+ className="w-full py-4 px-6 bg-accent-cyan hover:bg-accent-cyan/90 rounded-xl text-black font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
186
+ >
187
+ {isSubmitting ? (
188
+ <>
189
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
190
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
191
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
192
+ </svg>
193
+ <span>Saving...</span>
194
+ </>
195
+ ) : (
196
+ <span>Continue</span>
197
+ )}
198
+ </button>
199
+ </form>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ );
204
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Login Page - GitHub OAuth via Nango
2
+ * Login Page - GitHub OAuth via Nango or Email/Password
3
3
  *
4
4
  * Key: Initialize Nango on page load, not on click.
5
5
  * This avoids popup blockers by ensuring openConnectUI is synchronous.
@@ -28,14 +28,21 @@ function LoginLoading() {
28
28
  );
29
29
  }
30
30
 
31
+ type AuthMethod = 'github' | 'email';
32
+
31
33
  // Main login content that uses useSearchParams
32
34
  function LoginContent() {
33
35
  const searchParams = useSearchParams();
36
+ const [authMethod, setAuthMethod] = useState<AuthMethod>('github');
34
37
  const [isReady, setIsReady] = useState(false);
35
38
  const [isAuthenticating, setIsAuthenticating] = useState(false);
36
39
  const [authStatus, setAuthStatus] = useState<string>('');
37
40
  const [error, setError] = useState('');
38
-
41
+
42
+ // Email form state
43
+ const [email, setEmail] = useState('');
44
+ const [password, setPassword] = useState('');
45
+
39
46
  // Get return URL from query params (used by cloud link flow)
40
47
  const returnUrl = searchParams.get('return');
41
48
 
@@ -56,7 +63,8 @@ function LoginContent() {
56
63
  if (!mounted) return;
57
64
 
58
65
  if (!response.ok || !data.sessionToken) {
59
- setError('Failed to initialize. Please refresh the page.');
66
+ // Don't set error - email login doesn't need Nango
67
+ setIsReady(true);
60
68
  return;
61
69
  }
62
70
 
@@ -66,7 +74,8 @@ function LoginContent() {
66
74
  } catch (err) {
67
75
  if (mounted) {
68
76
  console.error('Init error:', err);
69
- setError('Failed to initialize. Please refresh the page.');
77
+ // Still allow email login even if Nango fails
78
+ setIsReady(true);
70
79
  }
71
80
  }
72
81
  };
@@ -75,7 +84,7 @@ function LoginContent() {
75
84
  return () => { mounted = false; };
76
85
  }, []);
77
86
 
78
- const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean; hasRepos?: boolean }> => {
87
+ const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean; hasRepos?: boolean; needsEmail?: boolean }> => {
79
88
  const response = await fetch(`/api/auth/nango/login-status/${connectionId}`, {
80
89
  credentials: 'include',
81
90
  });
@@ -103,7 +112,12 @@ function LoginContent() {
103
112
  try {
104
113
  const result = await checkAuthStatus(connectionId);
105
114
  if (result && result.ready) {
106
- // Redirect to return URL if provided (e.g., cloud link flow),
115
+ // If user needs to provide email, redirect to complete-profile
116
+ if (result.needsEmail) {
117
+ window.location.href = '/complete-profile';
118
+ return;
119
+ }
120
+ // Redirect to return URL if provided (e.g., cloud link flow),
107
121
  // otherwise to connect-repos if no repos, or to app
108
122
  if (returnUrl) {
109
123
  window.location.href = returnUrl;
@@ -133,7 +147,7 @@ function LoginContent() {
133
147
  // Use nango.auth() instead of openConnectUI to avoid popup blocker issues
134
148
  const handleGitHubAuth = async () => {
135
149
  if (!nangoRef.current) {
136
- setError('Not ready. Please refresh the page.');
150
+ setError('GitHub login not available. Please use email login or refresh the page.');
137
151
  return;
138
152
  }
139
153
 
@@ -174,6 +188,58 @@ function LoginContent() {
174
188
  }
175
189
  };
176
190
 
191
+ // Handle email login
192
+ const handleEmailLogin = async (e: React.FormEvent) => {
193
+ e.preventDefault();
194
+ setError('');
195
+ setIsAuthenticating(true);
196
+ setAuthStatus('Signing in...');
197
+
198
+ try {
199
+ // Get CSRF token first
200
+ const csrfResponse = await fetch('/api/auth/session', { credentials: 'include' });
201
+ const csrfToken = csrfResponse.headers.get('x-csrf-token');
202
+
203
+ const response = await fetch('/api/auth/email/login', {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Content-Type': 'application/json',
207
+ ...(csrfToken && { 'x-csrf-token': csrfToken }),
208
+ },
209
+ credentials: 'include',
210
+ body: JSON.stringify({ email, password }),
211
+ });
212
+
213
+ const data = await response.json();
214
+
215
+ if (!response.ok) {
216
+ // If user has GitHub account, suggest that
217
+ if (data.code === 'GITHUB_ACCOUNT') {
218
+ setError(data.error);
219
+ setAuthMethod('github');
220
+ } else {
221
+ setError(data.error || 'Login failed');
222
+ }
223
+ setIsAuthenticating(false);
224
+ setAuthStatus('');
225
+ return;
226
+ }
227
+
228
+ // Success - redirect
229
+ setAuthStatus('Login successful! Redirecting...');
230
+ if (returnUrl) {
231
+ window.location.href = returnUrl;
232
+ } else {
233
+ window.location.href = '/app';
234
+ }
235
+ } catch (err) {
236
+ console.error('Email login error:', err);
237
+ setError('Failed to connect. Please try again.');
238
+ setIsAuthenticating(false);
239
+ setAuthStatus('');
240
+ }
241
+ };
242
+
177
243
  const isLoading = !isReady || isAuthenticating;
178
244
 
179
245
  return (
@@ -201,6 +267,32 @@ function LoginContent() {
201
267
 
202
268
  {/* Login Card */}
203
269
  <div className="bg-bg-primary/80 backdrop-blur-sm border border-border-subtle rounded-2xl p-8 shadow-xl">
270
+ {/* Auth method tabs */}
271
+ <div className="flex mb-6 bg-bg-secondary/50 rounded-lg p-1">
272
+ <button
273
+ type="button"
274
+ onClick={() => setAuthMethod('github')}
275
+ className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
276
+ authMethod === 'github'
277
+ ? 'bg-bg-primary text-white shadow-sm'
278
+ : 'text-text-muted hover:text-white'
279
+ }`}
280
+ >
281
+ GitHub
282
+ </button>
283
+ <button
284
+ type="button"
285
+ onClick={() => setAuthMethod('email')}
286
+ className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
287
+ authMethod === 'email'
288
+ ? 'bg-bg-primary text-white shadow-sm'
289
+ : 'text-text-muted hover:text-white'
290
+ }`}
291
+ >
292
+ Email
293
+ </button>
294
+ </div>
295
+
204
296
  <div>
205
297
  {error && (
206
298
  <div className="mb-4 p-3 bg-error/10 border border-error/20 rounded-lg">
@@ -208,37 +300,89 @@ function LoginContent() {
208
300
  </div>
209
301
  )}
210
302
 
211
- <button
212
- type="button"
213
- onClick={handleGitHubAuth}
214
- disabled={isLoading}
215
- className="w-full py-4 px-6 bg-[#24292e] hover:bg-[#2f363d] border border-[#444d56] rounded-xl text-white font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
216
- >
217
- {!isReady ? (
218
- <>
219
- <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
220
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
221
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
222
- </svg>
223
- <span>Loading...</span>
224
- </>
225
- ) : isAuthenticating ? (
226
- <>
227
- <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
228
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
229
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
230
- </svg>
231
- <span>{authStatus || 'Connecting...'}</span>
232
- </>
233
- ) : (
234
- <>
235
- <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
236
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
237
- </svg>
238
- <span>Continue with GitHub</span>
239
- </>
240
- )}
241
- </button>
303
+ {authMethod === 'github' ? (
304
+ <button
305
+ type="button"
306
+ onClick={handleGitHubAuth}
307
+ disabled={isLoading}
308
+ className="w-full py-4 px-6 bg-[#24292e] hover:bg-[#2f363d] border border-[#444d56] rounded-xl text-white font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
309
+ >
310
+ {!isReady ? (
311
+ <>
312
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
313
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
314
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
315
+ </svg>
316
+ <span>Loading...</span>
317
+ </>
318
+ ) : isAuthenticating ? (
319
+ <>
320
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
321
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
322
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
323
+ </svg>
324
+ <span>{authStatus || 'Connecting...'}</span>
325
+ </>
326
+ ) : (
327
+ <>
328
+ <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
329
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
330
+ </svg>
331
+ <span>Continue with GitHub</span>
332
+ </>
333
+ )}
334
+ </button>
335
+ ) : (
336
+ <form onSubmit={handleEmailLogin} className="space-y-4">
337
+ <div>
338
+ <label htmlFor="email" className="block text-sm font-medium text-text-secondary mb-2">
339
+ Email
340
+ </label>
341
+ <input
342
+ type="email"
343
+ id="email"
344
+ value={email}
345
+ onChange={(e) => setEmail(e.target.value)}
346
+ required
347
+ disabled={isAuthenticating}
348
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
349
+ placeholder="you@example.com"
350
+ />
351
+ </div>
352
+ <div>
353
+ <label htmlFor="password" className="block text-sm font-medium text-text-secondary mb-2">
354
+ Password
355
+ </label>
356
+ <input
357
+ type="password"
358
+ id="password"
359
+ value={password}
360
+ onChange={(e) => setPassword(e.target.value)}
361
+ required
362
+ disabled={isAuthenticating}
363
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
364
+ placeholder="Enter your password"
365
+ />
366
+ </div>
367
+ <button
368
+ type="submit"
369
+ disabled={isLoading || !email || !password}
370
+ className="w-full py-4 px-6 bg-accent-cyan hover:bg-accent-cyan/90 rounded-xl text-black font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
371
+ >
372
+ {isAuthenticating ? (
373
+ <>
374
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
375
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
376
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
377
+ </svg>
378
+ <span>{authStatus || 'Signing in...'}</span>
379
+ </>
380
+ ) : (
381
+ <span>Sign in with Email</span>
382
+ )}
383
+ </button>
384
+ </form>
385
+ )}
242
386
 
243
387
  <p className="mt-6 text-center text-text-muted text-sm">
244
388
  By signing in, you agree to our{' '}