@rubytech/create-maxy 0.3.9 → 0.4.1
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 +1 -1
- package/payload/.claude/references/principles.md +12 -1
- package/payload/docs/deployment.md +9 -4
- package/payload/docs/hooks.md +2 -1
- package/payload/docs/mcp-servers.md +8 -7
- package/payload/docs/neo4j.md +6 -6
- package/payload/docs/platform.md +2 -2
- package/payload/docs/skills.md +1 -1
- package/payload/docs/web-chat.md +9 -5
- package/payload/maxy/app/api/health/route.ts +13 -6
- package/payload/maxy/app/api/onboarding/claude-auth/route.ts +112 -0
- package/payload/maxy/app/globals.css +39 -0
- package/payload/maxy/app/layout.tsx +4 -4
- package/payload/maxy/app/page.tsx +111 -6
- package/payload/maxy/public/brand/claude.png +0 -0
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ Everything an end user does must be achievable via the chat interface. No CLI. N
|
|
|
12
12
|
- Conversational onboarding (IDENTITY.md placeholder population) is the configuration mechanism
|
|
13
13
|
- Setup scripts install infrastructure only — product configuration happens via chat
|
|
14
14
|
- If there is no conversational path to use a feature, the feature is incomplete
|
|
15
|
-
-
|
|
15
|
+
- Product knowledge is ingested as markdown via the admin agent — there are no seed scripts.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -48,6 +48,17 @@ Infrastructure endpoints (`localhost:7687`, `localhost:11434`) may have defaults
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
+
## No Environment Variables for Credentials
|
|
52
|
+
|
|
53
|
+
Credentials are stored as files in `~/.maxy/` (persistent across upgrades):
|
|
54
|
+
- Neo4j password: `~/.maxy/.neo4j-password` (generated by installer)
|
|
55
|
+
- Admin PIN: `~/.maxy/.admin-pin` (set via web UI on first boot)
|
|
56
|
+
- Claude auth: OAuth flow in the web UI (managed by Claude Code)
|
|
57
|
+
|
|
58
|
+
There is no `.env.local`. MCP servers read the password file directly.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
51
62
|
## PRD Is Source of Truth
|
|
52
63
|
|
|
53
64
|
`MAXY-PRD.md` is the single authority for all standards, data models, and architecture. Do not rely on patterns found in existing skill code — they may diverge from the PRD.
|
|
@@ -21,12 +21,17 @@ When complete, open `http://maxy.local:19200` from any device on the same networ
|
|
|
21
21
|
7. Creates the initial account from templates
|
|
22
22
|
8. Applies the Neo4j schema
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Onboarding (all via web UI)
|
|
25
25
|
|
|
26
|
-
Open `http://maxy.local:19200` from any device on the same network
|
|
26
|
+
Open `http://maxy.local:19200` from any device on the same network. The onboarding flow is:
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
1. **Set PIN** — choose an admin PIN (stored at `~/.maxy/.admin-pin`)
|
|
29
|
+
2. **Connect Claude** — OAuth flow in the web UI (no API keys, no environment variables)
|
|
30
|
+
3. **Chat** — the admin agent is ready
|
|
31
|
+
|
|
32
|
+
From there, tell the admin agent what you need:
|
|
33
|
+
|
|
34
|
+
- "Ingest the product knowledge" — loads the knowledge base (ingested as markdown, not cypher)
|
|
30
35
|
- "Set up Telegram" — walks through BotFather bot creation
|
|
31
36
|
- "Set up Cloudflare Tunnel" — configures remote access on your domain
|
|
32
37
|
- "Check system status" — verifies all services are running
|
package/payload/docs/hooks.md
CHANGED
|
@@ -16,8 +16,9 @@ Fires at the beginning of each agent session. Loads the agent's IDENTITY.md from
|
|
|
16
16
|
|
|
17
17
|
Fires before every tool call. Enforces the public agent lockdown.
|
|
18
18
|
|
|
19
|
-
**Public agent:**
|
|
19
|
+
**Public agent:** Two tools allowed:
|
|
20
20
|
- `memory-search` / `mcp__maxy-memory__memory-search`
|
|
21
|
+
- `skill-read` / `mcp__maxy-admin__skill-read`
|
|
21
22
|
|
|
22
23
|
All other tools are blocked with exit code 2 and an error message. The public agent has zero write surface.
|
|
23
24
|
|
|
@@ -13,11 +13,11 @@ Graph-backed knowledge retrieval. The backbone of RAG (Retrieval-Augmented Gener
|
|
|
13
13
|
|
|
14
14
|
**Dependencies:** Neo4j (bolt://localhost:7687), Ollama (http://localhost:11434)
|
|
15
15
|
|
|
16
|
-
**
|
|
17
|
-
-
|
|
18
|
-
- `
|
|
19
|
-
-
|
|
20
|
-
-
|
|
16
|
+
**Configuration:**
|
|
17
|
+
- Neo4j password read from `~/.maxy/.neo4j-password` (generated by installer)
|
|
18
|
+
- Ollama endpoint: `http://localhost:11434` (local default)
|
|
19
|
+
- Account ID passed by the runtime per session
|
|
20
|
+
- Read-only mode: when enabled, memory-write and memory-reindex are not registered (used for public agent)
|
|
21
21
|
|
|
22
22
|
## maxy-contacts
|
|
23
23
|
|
|
@@ -40,7 +40,8 @@ Multi-channel message delivery. Phase 0: Telegram only.
|
|
|
40
40
|
- `message-history` — Query conversation log (Communication nodes in Neo4j).
|
|
41
41
|
|
|
42
42
|
**Dependencies:** Telegram Bot API
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
**Configuration:** Bot token stored in the account config directory (`~/maxy/platform/config/accounts/{uuid}/`), set via conversational onboarding.
|
|
44
45
|
|
|
45
46
|
## maxy-admin
|
|
46
47
|
|
|
@@ -52,7 +53,7 @@ System status and configuration reading.
|
|
|
52
53
|
- `account-manage` — Read account config (tier, domains, settings).
|
|
53
54
|
- `logs-read` — Read system, session, or error logs (tail).
|
|
54
55
|
|
|
55
|
-
**
|
|
56
|
+
**Configuration:** Platform root and account ID passed by the runtime per session.
|
|
56
57
|
|
|
57
58
|
### Cloudflare Tunnel Management
|
|
58
59
|
|
package/payload/docs/neo4j.md
CHANGED
|
@@ -35,14 +35,14 @@ Neo4j's built-in HNSW vector index is used for semantic search:
|
|
|
35
35
|
|
|
36
36
|
The `memory-search` tool embeds the query via Ollama, runs vector similarity search against the index, then expands results via 1-hop graph traversal to include related nodes (e.g., Service → PriceSpecification).
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Credentials
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
- `schema.cypher` — constraints, indexes, vector index creation
|
|
42
|
-
- `seed-product-knowledge.cypher` — all product data (tiers, features, FAQs, comparisons)
|
|
40
|
+
The Neo4j password is generated by the installer and stored at `~/.maxy/.neo4j-password`. MCP servers read from this file. There are no environment variables for credentials.
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
## Product Knowledge
|
|
43
|
+
|
|
44
|
+
Product knowledge is ingested as markdown via the admin agent using `memory-write`. There are no seed scripts or pre-configured cypher files for product data. The schema (constraints, indexes, vector index) is applied by the installer via `platform/neo4j/schema.cypher`.
|
|
45
45
|
|
|
46
46
|
## Embedding Pipeline
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
When the admin agent writes knowledge via `memory-write`, embeddings are computed automatically. The `memory-reindex` tool rebuilds embeddings for any nodes missing them (e.g., after a bulk import). This calls Ollama's `/api/embed` endpoint with `nomic-embed-text` and stores the resulting 768-dimensional vectors on each node.
|
package/payload/docs/platform.md
CHANGED
|
@@ -36,9 +36,9 @@ platform/
|
|
|
36
36
|
|
|
37
37
|
| Agent | Access | Model | Tools | Use |
|
|
38
38
|
|-------|--------|-------|-------|-----|
|
|
39
|
-
| Public | Read-only | claude-haiku-4-5 | memory-search | Sales, product enquiries |
|
|
39
|
+
| Public | Read-only | claude-haiku-4-5 | memory-search, skill-read | Sales, product enquiries |
|
|
40
40
|
| Admin | Full | claude-opus-4-6 | All MCP tools + Claude Code native tools | Content management, monitoring |
|
|
41
41
|
|
|
42
42
|
## Security Model
|
|
43
43
|
|
|
44
|
-
The public agent is locked down via the `pre-tool-use.sh` hook. It can only call `memory-search` —
|
|
44
|
+
The public agent is locked down via the `pre-tool-use.sh` hook. It can only call `memory-search` and `skill-read` — two read-only tools. All other tool calls are blocked with exit code 2. The public agent has zero write surface; waitlist signups captured in conversation are extracted by the admin cron review job. The admin agent has no restrictions.
|
package/payload/docs/skills.md
CHANGED
|
@@ -10,7 +10,7 @@ Skills are domain-specific behaviour modules that teach agents how to handle spe
|
|
|
10
10
|
| `business-assistant` | CRM, scheduling, quoting, invoicing, memory-first operations | Admin agent |
|
|
11
11
|
| `telegram` | Telegram bot setup guide (BotFather, webhook, admin vs public) | Admin agent |
|
|
12
12
|
| `cloudflare` | Cloudflare Tunnel setup guide (installation, DNS, domain) | Admin agent |
|
|
13
|
-
| `anthropic` | Claude connection setup (OAuth
|
|
13
|
+
| `anthropic` | Claude connection setup (OAuth flow via web UI) | Admin agent |
|
|
14
14
|
|
|
15
15
|
## Skill Format
|
|
16
16
|
|
package/payload/docs/web-chat.md
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
The web chat is a Next.js 16 application in `maxy/` that serves both the public sales agent and the admin agent.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Routing
|
|
6
6
|
|
|
7
|
-
|
|
|
7
|
+
| Access | Route | Page |
|
|
8
8
|
|--------|-------|------|
|
|
9
|
-
| `
|
|
10
|
-
| `
|
|
9
|
+
| Local (`maxy.local:19200`) | `/` | Admin chat (PIN-gated, SSE streaming, full activity) |
|
|
10
|
+
| Local (`maxy.local:19200`) | `/public` | Public chat (SSE streaming, text only) |
|
|
11
|
+
| `public.maxy.bot` (Cloudflare Tunnel) | `/public` | Public chat |
|
|
12
|
+
| `admin.maxy.bot` (Cloudflare Tunnel) | `/` | Admin chat |
|
|
11
13
|
| `getmaxy.com` | `/bot` | Marketing landing page |
|
|
12
14
|
| `maxy.bot` | redirect | → `public.maxy.bot` |
|
|
13
15
|
| `maxy.chat` | redirect | → `public.maxy.bot` |
|
|
14
16
|
|
|
17
|
+
The root page (`/`) is the admin interface. Local access on the same network = admin (with PIN). The public chat is at `/public`, served to the internet only via `public.maxy.bot` through Cloudflare Tunnel.
|
|
18
|
+
|
|
15
19
|
Routing is handled by Next.js middleware (`maxy/middleware.ts`).
|
|
16
20
|
|
|
17
21
|
## Public Chat API
|
|
@@ -53,7 +57,7 @@ The admin UI (`maxy/app/admin/page.tsx`) renders these as a Claude Code-style ac
|
|
|
53
57
|
Both endpoints use the shared `maxy/app/lib/claude-agent.ts` module which invokes `@anthropic-ai/claude-agent-sdk`'s `query()` function with:
|
|
54
58
|
- Appropriate MCP server configurations
|
|
55
59
|
- System prompts tailored to agent type
|
|
56
|
-
- Tool allowlists (public:
|
|
60
|
+
- Tool allowlists (public: 2 tools — memory-search + skill-read, admin: all)
|
|
57
61
|
- Permission modes (public: dontAsk, admin: acceptEdits)
|
|
58
62
|
- Model selection (public: haiku-4-5, admin: opus-4-6)
|
|
59
63
|
|
|
@@ -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
|
|
6
|
-
const PIN_FILE = resolve(
|
|
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) ||
|
|
14
|
+
const pinConfigured = existsSync(PIN_FILE) || !!process.env.ADMIN_PIN
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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,112 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { spawn, execFileSync, type ChildProcess } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
// Keep the login process alive between requests
|
|
5
|
+
let loginProcess: ChildProcess | null = null
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/onboarding/claude-auth
|
|
9
|
+
* Check Claude Code auth status.
|
|
10
|
+
*/
|
|
11
|
+
export async function GET() {
|
|
12
|
+
try {
|
|
13
|
+
const output = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
timeout: 10000,
|
|
16
|
+
})
|
|
17
|
+
const status = JSON.parse(output)
|
|
18
|
+
return NextResponse.json({
|
|
19
|
+
authenticated: status.loggedIn === true,
|
|
20
|
+
email: status.email ?? null,
|
|
21
|
+
})
|
|
22
|
+
} catch {
|
|
23
|
+
return NextResponse.json({ authenticated: false, email: null })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POST /api/onboarding/claude-auth
|
|
29
|
+
* Body: {} — starts the login flow, returns auth URL
|
|
30
|
+
* Body: { code: "..." } — feeds the auth code to the running login process
|
|
31
|
+
*/
|
|
32
|
+
export async function POST(req: Request) {
|
|
33
|
+
let body: { code?: string } = {}
|
|
34
|
+
try {
|
|
35
|
+
body = await req.json()
|
|
36
|
+
} catch { /* empty body = start flow */ }
|
|
37
|
+
|
|
38
|
+
// If code is provided, feed it to the running login process
|
|
39
|
+
if (body.code && loginProcess && loginProcess.stdin) {
|
|
40
|
+
loginProcess.stdin.write(body.code + '\n')
|
|
41
|
+
loginProcess.stdin.end()
|
|
42
|
+
|
|
43
|
+
// Wait a moment for the process to complete
|
|
44
|
+
await new Promise((r) => setTimeout(r, 3000))
|
|
45
|
+
|
|
46
|
+
// Check if auth succeeded
|
|
47
|
+
try {
|
|
48
|
+
const output = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
49
|
+
encoding: 'utf-8',
|
|
50
|
+
timeout: 5000,
|
|
51
|
+
})
|
|
52
|
+
const status = JSON.parse(output)
|
|
53
|
+
loginProcess = null
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
authenticated: status.loggedIn === true,
|
|
56
|
+
email: status.email ?? null,
|
|
57
|
+
})
|
|
58
|
+
} catch {
|
|
59
|
+
loginProcess = null
|
|
60
|
+
return NextResponse.json({ authenticated: false, error: 'Authentication failed. Check the code and try again.' })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Start a new login flow
|
|
65
|
+
if (loginProcess) {
|
|
66
|
+
loginProcess.kill()
|
|
67
|
+
loginProcess = null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Promise<Response>((resolve) => {
|
|
71
|
+
const child = spawn('claude', ['auth', 'login'], {
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
+
env: { ...process.env, BROWSER: 'echo' },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
loginProcess = child
|
|
77
|
+
|
|
78
|
+
let output = ''
|
|
79
|
+
let resolved = false
|
|
80
|
+
|
|
81
|
+
function tryResolve() {
|
|
82
|
+
if (resolved) return
|
|
83
|
+
const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
|
|
84
|
+
if (match) {
|
|
85
|
+
resolved = true
|
|
86
|
+
resolve(NextResponse.json({ authUrl: match[1] }))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
91
|
+
output += chunk.toString()
|
|
92
|
+
tryResolve()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
96
|
+
output += chunk.toString()
|
|
97
|
+
tryResolve()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (!resolved) {
|
|
102
|
+
resolved = true
|
|
103
|
+
child.kill()
|
|
104
|
+
loginProcess = null
|
|
105
|
+
resolve(NextResponse.json(
|
|
106
|
+
{ error: 'Timed out waiting for auth URL.' },
|
|
107
|
+
{ status: 500 },
|
|
108
|
+
))
|
|
109
|
+
}
|
|
110
|
+
}, 10000)
|
|
111
|
+
})
|
|
112
|
+
}
|
|
@@ -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 —
|
|
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 —
|
|
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 —
|
|
36
|
+
alt: 'Maxy — Convenience as standard.',
|
|
37
37
|
},
|
|
38
38
|
],
|
|
39
39
|
},
|
|
40
40
|
twitter: {
|
|
41
41
|
card: 'summary_large_image',
|
|
42
|
-
title: 'Maxy —
|
|
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,9 @@ 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 [authCode, setAuthCode] = useState('')
|
|
23
|
+
const [authLoading, setAuthLoading] = useState(false)
|
|
20
24
|
const [sessionKey, setSessionKey] = useState<string | null>(null)
|
|
21
25
|
const [messages, setMessages] = useState<Message[]>([])
|
|
22
26
|
const [input, setInput] = useState('')
|
|
@@ -39,7 +43,13 @@ export default function AdminPage() {
|
|
|
39
43
|
return
|
|
40
44
|
}
|
|
41
45
|
const health = await res.json()
|
|
42
|
-
|
|
46
|
+
if (!health.pin_configured) {
|
|
47
|
+
setAppState('set-pin')
|
|
48
|
+
} else if (!health.claude_authenticated) {
|
|
49
|
+
setAppState('connect-claude')
|
|
50
|
+
} else {
|
|
51
|
+
setAppState('enter-pin')
|
|
52
|
+
}
|
|
43
53
|
} catch {
|
|
44
54
|
setAppState('set-pin')
|
|
45
55
|
}
|
|
@@ -82,8 +92,10 @@ export default function AdminPage() {
|
|
|
82
92
|
return
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
// PIN set —
|
|
86
|
-
|
|
95
|
+
// PIN set — move to Claude auth
|
|
96
|
+
setPin('')
|
|
97
|
+
setConfirmPin('')
|
|
98
|
+
setAppState('connect-claude')
|
|
87
99
|
} catch {
|
|
88
100
|
setPinError('Could not connect.')
|
|
89
101
|
}
|
|
@@ -227,7 +239,7 @@ export default function AdminPage() {
|
|
|
227
239
|
autoFocus
|
|
228
240
|
/>
|
|
229
241
|
<button type="button" className="pin-toggle" onClick={() => setShowPin(!showPin)} aria-label={showPin ? 'Hide' : 'Show'}>
|
|
230
|
-
{showPin ?
|
|
242
|
+
{showPin ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
231
243
|
</button>
|
|
232
244
|
</div>
|
|
233
245
|
<div className="pin-input-row">
|
|
@@ -252,6 +264,99 @@ export default function AdminPage() {
|
|
|
252
264
|
)
|
|
253
265
|
}
|
|
254
266
|
|
|
267
|
+
// --- Connect Claude ---
|
|
268
|
+
if (appState === 'connect-claude') {
|
|
269
|
+
async function startAuth() {
|
|
270
|
+
setAuthLoading(true)
|
|
271
|
+
setPinError('')
|
|
272
|
+
try {
|
|
273
|
+
const res = await fetch('/api/onboarding/claude-auth', { method: 'POST' })
|
|
274
|
+
const data = await res.json()
|
|
275
|
+
if (data.authUrl) {
|
|
276
|
+
setAuthUrl(data.authUrl)
|
|
277
|
+
window.open(data.authUrl, '_blank')
|
|
278
|
+
} else if (data.error) {
|
|
279
|
+
setPinError(data.error)
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
setPinError('Could not start auth flow.')
|
|
283
|
+
}
|
|
284
|
+
setAuthLoading(false)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function submitCode(e: FormEvent) {
|
|
288
|
+
e.preventDefault()
|
|
289
|
+
if (!authCode.trim()) return
|
|
290
|
+
setAuthLoading(true)
|
|
291
|
+
setPinError('')
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch('/api/onboarding/claude-auth', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify({ code: authCode.trim() }),
|
|
297
|
+
})
|
|
298
|
+
const data = await res.json()
|
|
299
|
+
if (data.authenticated) {
|
|
300
|
+
setAppState('enter-pin')
|
|
301
|
+
} else {
|
|
302
|
+
setPinError(data.error || 'Authentication failed. Check the code and try again.')
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
setPinError('Could not verify code.')
|
|
306
|
+
}
|
|
307
|
+
setAuthLoading(false)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div className="chat-page admin-page">
|
|
312
|
+
<header className="chat-header">
|
|
313
|
+
<img src="/brand/claude.png" alt="Claude" className="chat-logo" />
|
|
314
|
+
<h1 className="chat-tagline">Connect Claude</h1>
|
|
315
|
+
<p className="chat-intro">Sign in with your Anthropic account to power Maxy.</p>
|
|
316
|
+
</header>
|
|
317
|
+
<div className="admin-pin-form">
|
|
318
|
+
{!authUrl ? (
|
|
319
|
+
<button
|
|
320
|
+
className="btn-primary"
|
|
321
|
+
onClick={startAuth}
|
|
322
|
+
disabled={authLoading}
|
|
323
|
+
>
|
|
324
|
+
{authLoading ? 'Starting...' : 'Sign in with Claude'}
|
|
325
|
+
</button>
|
|
326
|
+
) : (
|
|
327
|
+
<>
|
|
328
|
+
<p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
|
|
329
|
+
Sign in on the Anthropic page, then paste the code here.
|
|
330
|
+
</p>
|
|
331
|
+
<form onSubmit={submitCode}>
|
|
332
|
+
<div className="pin-input-row">
|
|
333
|
+
<input
|
|
334
|
+
type="text"
|
|
335
|
+
value={authCode}
|
|
336
|
+
onChange={e => setAuthCode(e.target.value)}
|
|
337
|
+
placeholder="Paste auth code"
|
|
338
|
+
className="chat-input"
|
|
339
|
+
autoFocus
|
|
340
|
+
/>
|
|
341
|
+
<button type="submit" className="chat-send" disabled={!authCode.trim() || authLoading}>
|
|
342
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
343
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
344
|
+
<polyline points="12 5 19 12 12 19" />
|
|
345
|
+
</svg>
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
</form>
|
|
349
|
+
<a href={authUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary" style={{ marginTop: '8px' }}>
|
|
350
|
+
Open sign-in page again
|
|
351
|
+
</a>
|
|
352
|
+
</>
|
|
353
|
+
)}
|
|
354
|
+
{pinError && <p className="admin-pin-error">{pinError}</p>}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
255
360
|
// --- Enter PIN (returning user) ---
|
|
256
361
|
if (appState === 'enter-pin') {
|
|
257
362
|
return (
|
|
@@ -274,7 +379,7 @@ export default function AdminPage() {
|
|
|
274
379
|
autoFocus
|
|
275
380
|
/>
|
|
276
381
|
<button type="button" className="pin-toggle" onClick={() => setShowPin(!showPin)} aria-label={showPin ? 'Hide' : 'Show'}>
|
|
277
|
-
{showPin ?
|
|
382
|
+
{showPin ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
278
383
|
</button>
|
|
279
384
|
<button type="submit" className="chat-send" disabled={!pin}>
|
|
280
385
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
Binary file
|