@rubytech/create-maxy 0.3.9 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "Install Maxy — your personal AI assistant",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -1,20 +1,27 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { existsSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
+ import { execFileSync } from 'node:child_process'
4
5
 
5
- const INSTALL_DIR = resolve(process.cwd(), '..')
6
- const PIN_FILE = resolve(INSTALL_DIR, '.maxy', '.admin-pin')
7
- const HOME_PIN_FILE = resolve(process.env.HOME ?? '/root', '.maxy', '.admin-pin')
6
+ const PERSISTENT_DIR = resolve(process.env.HOME ?? '/root', '.maxy')
7
+ const PIN_FILE = resolve(PERSISTENT_DIR, '.admin-pin')
8
8
 
9
9
  /**
10
10
  * GET /api/health
11
11
  * Returns platform state for the onboarding flow.
12
12
  */
13
13
  export async function GET() {
14
- const pinConfigured = existsSync(PIN_FILE) || existsSync(HOME_PIN_FILE) || !!process.env.ADMIN_PIN
14
+ const pinConfigured = existsSync(PIN_FILE) || !!process.env.ADMIN_PIN
15
15
 
16
- // TODO: check Claude Code auth status
17
- const claudeAuthenticated = false
16
+ let claudeAuthenticated = false
17
+ try {
18
+ const output = execFileSync('claude', ['auth', 'status', '--json'], {
19
+ encoding: 'utf-8',
20
+ timeout: 5000,
21
+ })
22
+ const status = JSON.parse(output)
23
+ claudeAuthenticated = status.loggedIn === true
24
+ } catch { /* not authenticated */ }
18
25
 
19
26
  return NextResponse.json({
20
27
  pin_configured: pinConfigured,
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { spawn, execFileSync } from 'node:child_process'
3
+
4
+ /**
5
+ * GET /api/onboarding/claude-auth
6
+ * Check Claude Code auth status.
7
+ */
8
+ export async function GET() {
9
+ try {
10
+ const output = execFileSync('claude', ['auth', 'status', '--json'], {
11
+ encoding: 'utf-8',
12
+ timeout: 10000,
13
+ env: { ...process.env, BROWSER: 'echo' },
14
+ })
15
+ const status = JSON.parse(output)
16
+ return NextResponse.json({
17
+ authenticated: status.loggedIn === true,
18
+ email: status.email ?? null,
19
+ })
20
+ } catch {
21
+ return NextResponse.json({ authenticated: false, email: null })
22
+ }
23
+ }
24
+
25
+ /**
26
+ * POST /api/onboarding/claude-auth
27
+ * Start the Claude Code login flow. Returns the auth URL.
28
+ */
29
+ export async function POST() {
30
+ return new Promise<Response>((resolve) => {
31
+ const child = spawn('claude', ['auth', 'login'], {
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ env: { ...process.env, BROWSER: 'echo' },
34
+ })
35
+
36
+ let output = ''
37
+ let resolved = false
38
+
39
+ function tryResolve() {
40
+ if (resolved) return
41
+
42
+ // Look for the auth URL in the output
43
+ const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
44
+ if (match) {
45
+ resolved = true
46
+ resolve(NextResponse.json({ authUrl: match[1] }))
47
+ }
48
+ }
49
+
50
+ child.stdout?.on('data', (chunk: Buffer) => {
51
+ output += chunk.toString()
52
+ tryResolve()
53
+ })
54
+
55
+ child.stderr?.on('data', (chunk: Buffer) => {
56
+ output += chunk.toString()
57
+ tryResolve()
58
+ })
59
+
60
+ // Timeout after 10 seconds
61
+ setTimeout(() => {
62
+ if (!resolved) {
63
+ resolved = true
64
+ child.kill()
65
+ resolve(NextResponse.json(
66
+ { error: 'Timed out waiting for auth URL.' },
67
+ { status: 500 },
68
+ ))
69
+ }
70
+ }, 10000)
71
+
72
+ child.on('exit', () => {
73
+ if (!resolved) {
74
+ resolved = true
75
+ resolve(NextResponse.json(
76
+ { error: 'Login process exited without providing auth URL.' },
77
+ { status: 500 },
78
+ ))
79
+ }
80
+ })
81
+ })
82
+ }
@@ -1376,6 +1376,45 @@ a:hover {
1376
1376
  border-color: var(--sage);
1377
1377
  }
1378
1378
 
1379
+ .btn-primary {
1380
+ padding: 12px 24px;
1381
+ background: var(--sage);
1382
+ color: white;
1383
+ border: none;
1384
+ border-radius: 24px;
1385
+ font-family: var(--font-body);
1386
+ font-size: 15px;
1387
+ cursor: pointer;
1388
+ transition: background 0.15s;
1389
+ }
1390
+
1391
+ .btn-primary:hover {
1392
+ background: var(--sage-hover);
1393
+ }
1394
+
1395
+ .btn-primary:disabled {
1396
+ opacity: 0.5;
1397
+ cursor: not-allowed;
1398
+ }
1399
+
1400
+ .btn-secondary {
1401
+ padding: 10px 20px;
1402
+ background: none;
1403
+ color: var(--text-secondary);
1404
+ border: 1px solid var(--border-strong);
1405
+ border-radius: 24px;
1406
+ font-family: var(--font-body);
1407
+ font-size: 14px;
1408
+ cursor: pointer;
1409
+ text-decoration: none;
1410
+ text-align: center;
1411
+ transition: border-color 0.15s;
1412
+ }
1413
+
1414
+ .btn-secondary:hover {
1415
+ border-color: var(--sage);
1416
+ }
1417
+
1379
1418
  .admin-pin-error {
1380
1419
  color: #c44;
1381
1420
  font-size: 14px;
@@ -17,14 +17,14 @@ const dmSans = DM_Sans({
17
17
  })
18
18
 
19
19
  export const metadata: Metadata = {
20
- title: 'Maxy — No Stress. Quiet Life.',
20
+ title: 'Maxy — Convenience as standard.',
21
21
  description: 'A personal AI assistant that lives in your home. Your data never leaves.',
22
22
  icons: {
23
23
  icon: '/favicon.ico',
24
24
  apple: '/apple-icon.png',
25
25
  },
26
26
  openGraph: {
27
- title: 'Maxy — No Stress. Quiet Life.',
27
+ title: 'Maxy — Convenience as standard.',
28
28
  description: 'A personal AI assistant that lives in your home. Your data never leaves.',
29
29
  siteName: 'Maxy',
30
30
  type: 'website',
@@ -33,13 +33,13 @@ export const metadata: Metadata = {
33
33
  url: '/og-landscape.png',
34
34
  width: 1200,
35
35
  height: 628,
36
- alt: 'Maxy — No Stress. Quiet Life.',
36
+ alt: 'Maxy — Convenience as standard.',
37
37
  },
38
38
  ],
39
39
  },
40
40
  twitter: {
41
41
  card: 'summary_large_image',
42
- title: 'Maxy — No Stress. Quiet Life.',
42
+ title: 'Maxy — Convenience as standard.',
43
43
  description: 'A personal AI assistant that lives in your home. Your data never leaves.',
44
44
  images: ['/og-landscape.png'],
45
45
  },
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import { useState, useRef, useEffect, FormEvent } from 'react'
4
+ import { Eye, EyeOff } from 'lucide-react'
4
5
  import { ActivityEvent, type AdminEvent } from './admin/components/ActivityEvent'
5
6
 
6
- type AppState = 'loading' | 'set-pin' | 'enter-pin' | 'chat'
7
+ type AppState = 'loading' | 'set-pin' | 'enter-pin' | 'connect-claude' | 'chat'
7
8
 
8
9
  interface Message {
9
10
  role: 'admin' | 'maxy'
@@ -17,6 +18,8 @@ export default function AdminPage() {
17
18
  const [confirmPin, setConfirmPin] = useState('')
18
19
  const [pinError, setPinError] = useState('')
19
20
  const [showPin, setShowPin] = useState(false)
21
+ const [authUrl, setAuthUrl] = useState<string | null>(null)
22
+ const [authLoading, setAuthLoading] = useState(false)
20
23
  const [sessionKey, setSessionKey] = useState<string | null>(null)
21
24
  const [messages, setMessages] = useState<Message[]>([])
22
25
  const [input, setInput] = useState('')
@@ -39,7 +42,13 @@ export default function AdminPage() {
39
42
  return
40
43
  }
41
44
  const health = await res.json()
42
- setAppState(health.pin_configured ? 'enter-pin' : 'set-pin')
45
+ if (!health.pin_configured) {
46
+ setAppState('set-pin')
47
+ } else if (!health.claude_authenticated) {
48
+ setAppState('connect-claude')
49
+ } else {
50
+ setAppState('enter-pin')
51
+ }
43
52
  } catch {
44
53
  setAppState('set-pin')
45
54
  }
@@ -82,8 +91,10 @@ export default function AdminPage() {
82
91
  return
83
92
  }
84
93
 
85
- // PIN set — now log in with it
86
- await doLogin(pin)
94
+ // PIN set — move to Claude auth
95
+ setPin('')
96
+ setConfirmPin('')
97
+ setAppState('connect-claude')
87
98
  } catch {
88
99
  setPinError('Could not connect.')
89
100
  }
@@ -227,7 +238,7 @@ export default function AdminPage() {
227
238
  autoFocus
228
239
  />
229
240
  <button type="button" className="pin-toggle" onClick={() => setShowPin(!showPin)} aria-label={showPin ? 'Hide' : 'Show'}>
230
- {showPin ? '◉' : '○'}
241
+ {showPin ? <EyeOff size={18} /> : <Eye size={18} />}
231
242
  </button>
232
243
  </div>
233
244
  <div className="pin-input-row">
@@ -252,6 +263,63 @@ export default function AdminPage() {
252
263
  )
253
264
  }
254
265
 
266
+ // --- Connect Claude ---
267
+ if (appState === 'connect-claude') {
268
+ async function startAuth() {
269
+ setAuthLoading(true)
270
+ try {
271
+ const res = await fetch('/api/onboarding/claude-auth', { method: 'POST' })
272
+ const data = await res.json()
273
+ if (data.authUrl) {
274
+ setAuthUrl(data.authUrl)
275
+ window.open(data.authUrl, '_blank')
276
+ }
277
+ } catch { /* */ }
278
+ setAuthLoading(false)
279
+ }
280
+
281
+ async function checkAuth() {
282
+ const res = await fetch('/api/onboarding/claude-auth')
283
+ const data = await res.json()
284
+ if (data.authenticated) {
285
+ setAppState('enter-pin')
286
+ }
287
+ }
288
+
289
+ return (
290
+ <div className="chat-page admin-page">
291
+ <header className="chat-header">
292
+ <img src="/brand/maxy-black.png" alt="Maxy" className="chat-logo" />
293
+ <h1 className="chat-tagline">Connect Claude</h1>
294
+ <p className="chat-intro">Sign in with your Anthropic account to power Maxy.</p>
295
+ </header>
296
+ <div className="admin-pin-form">
297
+ {!authUrl ? (
298
+ <button
299
+ className="btn-primary"
300
+ onClick={startAuth}
301
+ disabled={authLoading}
302
+ >
303
+ {authLoading ? 'Starting...' : 'Sign in with Claude'}
304
+ </button>
305
+ ) : (
306
+ <>
307
+ <p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
308
+ A browser window has opened. Sign in with Anthropic, then come back here.
309
+ </p>
310
+ <a href={authUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">
311
+ Open sign-in page again
312
+ </a>
313
+ <button className="btn-primary" onClick={checkAuth} style={{ marginTop: '12px' }}>
314
+ I{'\u2019'}ve signed in
315
+ </button>
316
+ </>
317
+ )}
318
+ </div>
319
+ </div>
320
+ )
321
+ }
322
+
255
323
  // --- Enter PIN (returning user) ---
256
324
  if (appState === 'enter-pin') {
257
325
  return (
@@ -274,7 +342,7 @@ export default function AdminPage() {
274
342
  autoFocus
275
343
  />
276
344
  <button type="button" className="pin-toggle" onClick={() => setShowPin(!showPin)} aria-label={showPin ? 'Hide' : 'Show'}>
277
- {showPin ? '◉' : '○'}
345
+ {showPin ? <EyeOff size={18} /> : <Eye size={18} />}
278
346
  </button>
279
347
  <button type="submit" className="chat-send" disabled={!pin}>
280
348
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">