@rubytech/create-maxy 0.4.0 → 0.4.2
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.
Potentially problematic release.
This version of @rubytech/create-maxy might be problematic. Click here for more details.
- 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/onboarding/claude-auth/route.ts +47 -17
- package/payload/maxy/app/globals.css +12 -0
- package/payload/maxy/app/page.tsx +50 -13
- 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,5 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { spawn, execFileSync } from 'node:child_process'
|
|
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
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* GET /api/onboarding/claude-auth
|
|
@@ -10,7 +13,6 @@ export async function GET() {
|
|
|
10
13
|
const output = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
11
14
|
encoding: 'utf-8',
|
|
12
15
|
timeout: 10000,
|
|
13
|
-
env: { ...process.env, BROWSER: 'echo' },
|
|
14
16
|
})
|
|
15
17
|
const status = JSON.parse(output)
|
|
16
18
|
return NextResponse.json({
|
|
@@ -24,22 +26,60 @@ export async function GET() {
|
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* POST /api/onboarding/claude-auth
|
|
27
|
-
*
|
|
29
|
+
* Body: {} — starts the login flow, returns auth URL
|
|
30
|
+
* Body: { code: "..." } — feeds the auth code to the running login process
|
|
28
31
|
*/
|
|
29
|
-
export async function POST() {
|
|
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
|
+
|
|
30
70
|
return new Promise<Response>((resolve) => {
|
|
31
71
|
const child = spawn('claude', ['auth', 'login'], {
|
|
32
72
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
73
|
env: { ...process.env, BROWSER: 'echo' },
|
|
34
74
|
})
|
|
35
75
|
|
|
76
|
+
loginProcess = child
|
|
77
|
+
|
|
36
78
|
let output = ''
|
|
37
79
|
let resolved = false
|
|
38
80
|
|
|
39
81
|
function tryResolve() {
|
|
40
82
|
if (resolved) return
|
|
41
|
-
|
|
42
|
-
// Look for the auth URL in the output
|
|
43
83
|
const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
|
|
44
84
|
if (match) {
|
|
45
85
|
resolved = true
|
|
@@ -57,26 +97,16 @@ export async function POST() {
|
|
|
57
97
|
tryResolve()
|
|
58
98
|
})
|
|
59
99
|
|
|
60
|
-
// Timeout after 10 seconds
|
|
61
100
|
setTimeout(() => {
|
|
62
101
|
if (!resolved) {
|
|
63
102
|
resolved = true
|
|
64
103
|
child.kill()
|
|
104
|
+
loginProcess = null
|
|
65
105
|
resolve(NextResponse.json(
|
|
66
106
|
{ error: 'Timed out waiting for auth URL.' },
|
|
67
107
|
{ status: 500 },
|
|
68
108
|
))
|
|
69
109
|
}
|
|
70
110
|
}, 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
111
|
})
|
|
82
112
|
}
|
|
@@ -1415,6 +1415,18 @@ a:hover {
|
|
|
1415
1415
|
border-color: var(--sage);
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
|
+
.auth-retry-link {
|
|
1419
|
+
font-family: var(--font-body);
|
|
1420
|
+
font-size: 13px;
|
|
1421
|
+
color: var(--text-tertiary);
|
|
1422
|
+
text-decoration: none;
|
|
1423
|
+
margin-top: 12px;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.auth-retry-link:hover {
|
|
1427
|
+
color: var(--sage);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1418
1430
|
.admin-pin-error {
|
|
1419
1431
|
color: #c44;
|
|
1420
1432
|
font-size: 14px;
|
|
@@ -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[]>([])
|
|
@@ -267,29 +268,49 @@ export default function AdminPage() {
|
|
|
267
268
|
if (appState === 'connect-claude') {
|
|
268
269
|
async function startAuth() {
|
|
269
270
|
setAuthLoading(true)
|
|
271
|
+
setPinError('')
|
|
270
272
|
try {
|
|
271
273
|
const res = await fetch('/api/onboarding/claude-auth', { method: 'POST' })
|
|
272
274
|
const data = await res.json()
|
|
273
275
|
if (data.authUrl) {
|
|
274
276
|
setAuthUrl(data.authUrl)
|
|
275
277
|
window.open(data.authUrl, '_blank')
|
|
278
|
+
} else if (data.error) {
|
|
279
|
+
setPinError(data.error)
|
|
276
280
|
}
|
|
277
|
-
} catch {
|
|
281
|
+
} catch {
|
|
282
|
+
setPinError('Could not start auth flow.')
|
|
283
|
+
}
|
|
278
284
|
setAuthLoading(false)
|
|
279
285
|
}
|
|
280
286
|
|
|
281
|
-
async function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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.')
|
|
286
306
|
}
|
|
307
|
+
setAuthLoading(false)
|
|
287
308
|
}
|
|
288
309
|
|
|
289
310
|
return (
|
|
290
311
|
<div className="chat-page admin-page">
|
|
291
312
|
<header className="chat-header">
|
|
292
|
-
<img src="/brand/
|
|
313
|
+
<img src="/brand/claude.png" alt="Claude" className="chat-logo" />
|
|
293
314
|
<h1 className="chat-tagline">Connect Claude</h1>
|
|
294
315
|
<p className="chat-intro">Sign in with your Anthropic account to power Maxy.</p>
|
|
295
316
|
</header>
|
|
@@ -305,16 +326,32 @@ export default function AdminPage() {
|
|
|
305
326
|
) : (
|
|
306
327
|
<>
|
|
307
328
|
<p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
|
|
308
|
-
|
|
329
|
+
Sign in on the Anthropic page, then paste the code here.
|
|
309
330
|
</p>
|
|
310
|
-
<
|
|
311
|
-
|
|
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="auth-retry-link">
|
|
350
|
+
Try again
|
|
312
351
|
</a>
|
|
313
|
-
<button className="btn-primary" onClick={checkAuth} style={{ marginTop: '12px' }}>
|
|
314
|
-
I{'\u2019'}ve signed in
|
|
315
|
-
</button>
|
|
316
352
|
</>
|
|
317
353
|
)}
|
|
354
|
+
{pinError && <p className="admin-pin-error">{pinError}</p>}
|
|
318
355
|
</div>
|
|
319
356
|
</div>
|
|
320
357
|
)
|
|
Binary file
|