@rubytech/create-maxy 0.4.4 → 0.4.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Install Maxy — your personal AI assistant",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -1,8 +1,53 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { spawn, execFileSync, type ChildProcess } from 'node:child_process'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { loginAnthropic, type OAuthCredentials } from '@mariozechner/pi-ai'
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
6
+ import { homedir } from 'node:os'
3
7
 
4
- // Keep the login process alive it polls Anthropic for completion
5
- let loginProcess: ChildProcess | null = null
8
+ // --- Active OAuth session (same pattern as v1 gateway/server-methods/auth.ts) ---
9
+
10
+ type OAuthSession = {
11
+ authUrl: string | null
12
+ codeResolver: ((code: string) => void) | null
13
+ codePromise: Promise<string> | null
14
+ completed: boolean
15
+ error: string | null
16
+ credentials: OAuthCredentials | null
17
+ }
18
+
19
+ let activeSession: OAuthSession | null = null
20
+
21
+ function resetSession(): void {
22
+ activeSession = null
23
+ }
24
+
25
+ // --- Claude CLI credentials ---
26
+
27
+ const CLAUDE_CREDS_PATH = resolve(homedir(), '.claude/.credentials.json')
28
+
29
+ function writeClaudeCredentials(creds: OAuthCredentials): void {
30
+ const claudeDir = resolve(homedir(), '.claude')
31
+ mkdirSync(claudeDir, { recursive: true })
32
+
33
+ let data: Record<string, unknown> = {}
34
+ if (existsSync(CLAUDE_CREDS_PATH)) {
35
+ try {
36
+ data = JSON.parse(readFileSync(CLAUDE_CREDS_PATH, 'utf-8'))
37
+ } catch { /* start fresh */ }
38
+ }
39
+
40
+ data.claudeAiOauth = {
41
+ ...(typeof data.claudeAiOauth === 'object' && data.claudeAiOauth ? data.claudeAiOauth : {}),
42
+ accessToken: creds.access,
43
+ refreshToken: creds.refresh,
44
+ expiresAt: creds.expires,
45
+ }
46
+
47
+ writeFileSync(CLAUDE_CREDS_PATH, JSON.stringify(data, null, 2), { mode: 0o600 })
48
+ }
49
+
50
+ // --- Endpoints ---
6
51
 
7
52
  /**
8
53
  * GET /api/onboarding/claude-auth
@@ -26,68 +71,118 @@ export async function GET() {
26
71
 
27
72
  /**
28
73
  * POST /api/onboarding/claude-auth
29
- * Start the Claude Code OAuth login flow.
30
- * Returns the auth URL. The user visits it in their browser,
31
- * authenticates with Anthropic, and the process completes automatically
32
- * via PKCE polling. No code pasting needed.
74
+ * Three actions:
75
+ * - {} start OAuth flow, returns authUrl
76
+ * - { code: "..." } — submit the auth code
77
+ * - { action: "wait" } poll for completion
33
78
  */
34
- export async function POST() {
35
- // Kill any existing login process
36
- if (loginProcess) {
37
- loginProcess.kill()
38
- loginProcess = null
39
- }
79
+ export async function POST(req: Request) {
80
+ let body: { code?: string; action?: string } = {}
81
+ try {
82
+ body = await req.json()
83
+ } catch { /* empty body = start flow */ }
40
84
 
41
- return new Promise<Response>((resolve) => {
42
- // Don't suppress the browser — but on a headless Pi it won't open.
43
- // We capture the auth URL from the output and present it in the web UI.
44
- const child = spawn('claude', ['auth', 'login'], {
45
- stdio: ['pipe', 'pipe', 'pipe'],
46
- env: { ...process.env, BROWSER: '' },
47
- })
85
+ // --- Submit code ---
86
+ if (body.code) {
87
+ if (!activeSession?.codeResolver) {
88
+ return NextResponse.json(
89
+ { error: 'No active OAuth session. Click "Sign in with Claude" to start again.' },
90
+ { status: 400 },
91
+ )
92
+ }
93
+
94
+ activeSession.codeResolver(body.code.trim())
95
+
96
+ return NextResponse.json({ message: 'Code received — verifying...' })
97
+ }
48
98
 
49
- loginProcess = child
99
+ // --- Poll for completion ---
100
+ if (body.action === 'wait') {
101
+ if (!activeSession) {
102
+ return NextResponse.json({ completed: false, error: 'No active session.' })
103
+ }
50
104
 
51
- let output = ''
52
- let resolved = false
105
+ // Poll for up to 5 seconds
106
+ const start = Date.now()
107
+ while (Date.now() - start < 5000) {
108
+ if (activeSession.completed) {
109
+ if (activeSession.error) {
110
+ const error = activeSession.error
111
+ resetSession()
112
+ return NextResponse.json({ completed: true, authenticated: false, error })
113
+ }
53
114
 
54
- function tryResolve() {
55
- if (resolved) return
56
- const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
57
- if (match) {
58
- resolved = true
59
- // Remove code=true& from the URL — that triggers the fallback flow
60
- // which shows an auth code. Without it, the flow completes automatically.
61
- const autoUrl = match[1].replace(/code=true&?/, '').replace(/\?&/, '?')
62
- resolve(NextResponse.json({ authUrl: autoUrl }))
115
+ resetSession()
116
+ return NextResponse.json({ completed: true, authenticated: true })
63
117
  }
118
+ await new Promise(r => setTimeout(r, 500))
64
119
  }
65
120
 
66
- child.stdout?.on('data', (chunk: Buffer) => {
67
- output += chunk.toString()
68
- tryResolve()
69
- })
121
+ return NextResponse.json({ completed: false })
122
+ }
70
123
 
71
- child.stderr?.on('data', (chunk: Buffer) => {
72
- output += chunk.toString()
73
- tryResolve()
74
- })
124
+ // --- Start OAuth flow ---
125
+ resetSession()
75
126
 
76
- // The process stays alive — it polls Anthropic for auth completion.
77
- // Don't kill it. It will exit on its own when auth succeeds.
78
- child.on('exit', () => {
79
- loginProcess = null
80
- })
127
+ activeSession = {
128
+ authUrl: null,
129
+ codeResolver: null,
130
+ codePromise: null,
131
+ completed: false,
132
+ error: null,
133
+ credentials: null,
134
+ }
81
135
 
82
- // Only timeout if we can't even get the URL
83
- setTimeout(() => {
84
- if (!resolved) {
85
- resolved = true
86
- resolve(NextResponse.json(
87
- { error: 'Could not start auth flow. Is Claude Code installed?' },
88
- { status: 500 },
89
- ))
90
- }
91
- }, 15000)
136
+ // Set up code promise (resolved when user submits code via POST { code })
137
+ activeSession.codePromise = new Promise<string>((resolve) => {
138
+ activeSession!.codeResolver = resolve
92
139
  })
140
+
141
+ // Start OAuth flow in background (same as v1)
142
+ const oauthPromise = loginAnthropic(
143
+ // onAuthUrl — called when we have the URL
144
+ (url: string) => {
145
+ if (activeSession) {
146
+ activeSession.authUrl = url
147
+ }
148
+ },
149
+ // onPromptCode — called when the library needs the code
150
+ async (): Promise<string> => {
151
+ if (!activeSession?.codePromise) {
152
+ throw new Error('OAuth session expired')
153
+ }
154
+ return activeSession.codePromise
155
+ },
156
+ )
157
+
158
+ // Handle completion in background
159
+ oauthPromise
160
+ .then((credentials) => {
161
+ if (activeSession) {
162
+ activeSession.credentials = credentials
163
+ activeSession.completed = true
164
+ writeClaudeCredentials(credentials)
165
+ }
166
+ })
167
+ .catch((err) => {
168
+ if (activeSession) {
169
+ activeSession.error = err instanceof Error ? err.message : String(err)
170
+ activeSession.completed = true
171
+ }
172
+ })
173
+
174
+ // Wait for the auth URL to appear
175
+ await new Promise(r => setTimeout(r, 500))
176
+ if (!activeSession?.authUrl) {
177
+ await new Promise(r => setTimeout(r, 1500))
178
+ }
179
+
180
+ if (!activeSession?.authUrl) {
181
+ return NextResponse.json(
182
+ { error: 'Failed to start OAuth flow.' },
183
+ { status: 500 },
184
+ )
185
+ }
186
+
187
+ return NextResponse.json({ authUrl: activeSession.authUrl })
93
188
  }
@@ -19,6 +19,7 @@ export default function AdminPage() {
19
19
  const [pinError, setPinError] = useState('')
20
20
  const [showPin, setShowPin] = useState(false)
21
21
  const [authUrl, setAuthUrl] = useState<string | null>(null)
22
+ const [authCode, setAuthCode] = useState('')
22
23
  const [authLoading, setAuthLoading] = useState(false)
23
24
  const [sessionKey, setSessionKey] = useState<string | null>(null)
24
25
  const [messages, setMessages] = useState<Message[]>([])
@@ -274,8 +275,6 @@ export default function AdminPage() {
274
275
  if (data.authUrl) {
275
276
  setAuthUrl(data.authUrl)
276
277
  window.open(data.authUrl, '_blank')
277
- // Start polling for auth completion
278
- pollForAuth()
279
278
  } else if (data.error) {
280
279
  setPinError(data.error)
281
280
  }
@@ -285,18 +284,45 @@ export default function AdminPage() {
285
284
  setAuthLoading(false)
286
285
  }
287
286
 
288
- async function pollForAuth() {
289
- for (let i = 0; i < 60; i++) { // Poll for up to 2 minutes
290
- await new Promise(r => setTimeout(r, 2000))
291
- try {
292
- const res = await fetch('/api/onboarding/claude-auth')
293
- const data = await res.json()
294
- if (data.authenticated) {
295
- setAppState('enter-pin')
296
- return
287
+ async function submitCode(e: FormEvent) {
288
+ e.preventDefault()
289
+ if (!authCode.trim()) return
290
+ setAuthLoading(true)
291
+ setPinError('')
292
+ try {
293
+ // Submit the code
294
+ await fetch('/api/onboarding/claude-auth', {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ code: authCode.trim() }),
298
+ })
299
+
300
+ // Poll for completion
301
+ for (let i = 0; i < 12; i++) {
302
+ const waitRes = await fetch('/api/onboarding/claude-auth', {
303
+ method: 'POST',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({ action: 'wait' }),
306
+ })
307
+ const waitData = await waitRes.json()
308
+
309
+ if (waitData.completed) {
310
+ if (waitData.authenticated) {
311
+ setAppState('enter-pin')
312
+ return
313
+ } else {
314
+ setPinError(waitData.error || 'Authentication failed. Check the code and try again.')
315
+ setAuthLoading(false)
316
+ return
317
+ }
297
318
  }
298
- } catch { /* keep polling */ }
319
+ }
320
+
321
+ setPinError('Timed out waiting for verification. Try again.')
322
+ } catch {
323
+ setPinError('Could not verify code.')
299
324
  }
325
+ setAuthLoading(false)
300
326
  }
301
327
 
302
328
  return (
@@ -318,15 +344,29 @@ export default function AdminPage() {
318
344
  ) : (
319
345
  <>
320
346
  <p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
321
- Complete sign-in on the Anthropic page. This screen will update automatically.
347
+ Sign in on the Anthropic page, then paste the code here.
322
348
  </p>
323
- <span className="typing-indicator" style={{ margin: '16px auto' }}>
324
- <span className="typing-dot" />
325
- <span className="typing-dot" />
326
- <span className="typing-dot" />
327
- </span>
349
+ <form onSubmit={submitCode}>
350
+ <div className="pin-input-row">
351
+ <input
352
+ type="text"
353
+ value={authCode}
354
+ onChange={e => setAuthCode(e.target.value)}
355
+ placeholder="Paste authentication code"
356
+ className="chat-input"
357
+ autoFocus
358
+ autoComplete="off"
359
+ />
360
+ <button type="submit" className="chat-send" disabled={!authCode.trim() || authLoading}>
361
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
362
+ <line x1="5" y1="12" x2="19" y2="12" />
363
+ <polyline points="12 5 19 12 12 19" />
364
+ </svg>
365
+ </button>
366
+ </div>
367
+ </form>
328
368
  <a href={authUrl} target="_blank" rel="noopener noreferrer" className="auth-retry-link">
329
- Open sign-in page again
369
+ Try again
330
370
  </a>
331
371
  </>
332
372
  )}