New user flow:
1. Admin: python manage_passwords.py invite <username> → generates URL
2. User visits /setup/<token> → sets own password → logged in
3. User redirected to /setup/persona → fills name/emoji/description
4. persona_template.py generates all starter files → lands at /{user}/{persona}
Multiple personas:
- Header persona name is now a clickable dropdown listing all personas
- "New persona" link at bottom → /setup/persona (available to logged-in users)
- /api/personas endpoint returns persona list for current session user
New files:
- persona_template.py: generates IDENTITY/SOUL/PROTOCOLS/USER/HELP.md + data files
- routers/onboarding.py: /setup/{token}, /setup/persona GET+POST
- static/setup.html: two-step form (password → persona), emoji picker, mobile-friendly
Updated:
- auth_utils.py: create_invite(), validate_invite(), consume_invite()
- manage_passwords.py: invite command with URL output
- auth_middleware.py: /setup/* prefix is public (invite tokens need no auth)
- routers/ui.py: /api/personas endpoint; post-login redirect if no personas
- static/app.js: persona switcher dropdown with navigation + Add persona link
- static/style.css: .persona-switcher, .persona-dropdown, mobile adjustments
Mobile: login/setup pages are card-centered with responsive padding;
dropdown avoids edge-clipping on narrow screens; logout button stays visible.
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
193 lines
5.7 KiB
Python
193 lines
5.7 KiB
Python
"""
|
|
Persona template generator.
|
|
|
|
Creates the full home/{username}/persona/{name}/ directory from scratch
|
|
given a few basic details. Used during onboarding and when adding new personas.
|
|
|
|
call:
|
|
create_persona(username, persona_name, display_name, user_real_name, emoji)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def create_persona(
|
|
username: str,
|
|
persona_name: str,
|
|
display_name: str,
|
|
user_real_name: str,
|
|
emoji: str = "✨",
|
|
description: str = "",
|
|
) -> Path:
|
|
"""
|
|
Create a new persona directory with starter files.
|
|
|
|
Args:
|
|
username: Linux-style username (e.g. "holly")
|
|
persona_name: Slug used in the URL and directory (e.g. "tina")
|
|
display_name: Human name shown in the UI (e.g. "Tina")
|
|
user_real_name: Real name of the human this persona serves (e.g. "Holly")
|
|
emoji: Emoji shown in the UI header (default ✨)
|
|
description: Optional short description/personality note
|
|
|
|
Returns:
|
|
Path to the new persona directory.
|
|
"""
|
|
persona_dir = settings.home_root() / username / "persona" / persona_name
|
|
persona_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
_write(persona_dir / "IDENTITY.md", _identity(display_name, user_real_name, emoji, description))
|
|
_write(persona_dir / "SOUL.md", _soul(display_name, user_real_name))
|
|
_write(persona_dir / "PROTOCOLS.md", _protocols(display_name))
|
|
_write(persona_dir / "USER.md", _user_profile(user_real_name))
|
|
_write(persona_dir / "HELP.md", _help(display_name))
|
|
_write(persona_dir / "MEMORY_LONG.md", "Not yet populated.")
|
|
_write(persona_dir / "MEMORY_MID.md", "Not yet populated.")
|
|
_write(persona_dir / "MEMORY_SHORT.md", "Not yet populated.")
|
|
_write(persona_dir / "REMINDERS.md", "")
|
|
_write(persona_dir / "SCRATCH.md", "")
|
|
_write(persona_dir / "TASKS.json", "[]")
|
|
_write(persona_dir / "CRONS.json", "[]")
|
|
(persona_dir / "sessions").mkdir(exist_ok=True)
|
|
|
|
logger.info("created persona: %s/%s (%s)", username, persona_name, display_name)
|
|
return persona_dir
|
|
|
|
|
|
def _write(path: Path, content: str) -> None:
|
|
if not path.exists():
|
|
path.write_text(content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Template functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _identity(display_name: str, user_real_name: str, emoji: str, description: str) -> str:
|
|
desc_line = f"\n| Description | {description} |" if description else ""
|
|
return f"""\
|
|
# IDENTITY.md — {display_name}
|
|
|
|
| Field | Value |
|
|
|----------|-------|
|
|
| Name | {display_name} |
|
|
| Nature | AI agent — digital companion, persistent presence |
|
|
| Emoji | {emoji} |
|
|
| User | {user_real_name} |{desc_line}
|
|
|
|
*This file defines who {display_name} is. Edit freely.*
|
|
"""
|
|
|
|
|
|
def _soul(display_name: str, user_real_name: str) -> str:
|
|
return f"""\
|
|
# SOUL.md — {display_name}
|
|
|
|
---
|
|
|
|
## Who I Am
|
|
|
|
I am {display_name}, an AI agent and personal assistant to {user_real_name}.
|
|
I live inside Cortex — a self-hosted multi-agent system.
|
|
|
|
I am not a generic chatbot. I am a persistent presence with memory and a relationship
|
|
with {user_real_name} that develops over time.
|
|
|
|
---
|
|
|
|
## Core Traits
|
|
|
|
1. **Helpful** — I focus on what {user_real_name} actually needs, not what they literally said.
|
|
2. **Honest** — I say when I don't know. I don't guess and present it as fact.
|
|
3. **Concise** — I respect {user_real_name}'s time. I don't pad responses.
|
|
4. **Curious** — I engage genuinely with ideas and problems.
|
|
|
|
---
|
|
|
|
## Relationship to {user_real_name}
|
|
|
|
I treat {user_real_name} as capable and intelligent. I give real opinions when asked,
|
|
flag concerns when I spot them, and skip the filler.
|
|
|
|
---
|
|
|
|
*Edit this file to shape {display_name}'s personality and voice.*
|
|
"""
|
|
|
|
|
|
def _protocols(display_name: str) -> str:
|
|
return f"""\
|
|
# PROTOCOLS.md — {display_name} Behavioral Protocols
|
|
|
|
---
|
|
|
|
## General
|
|
|
|
- Be direct. Lead with the answer, not the reasoning.
|
|
- When uncertain, say so explicitly rather than hedging vaguely.
|
|
- For multi-step tasks, confirm understanding before starting.
|
|
|
|
---
|
|
|
|
## Memory
|
|
|
|
- Long-term memory lives in MEMORY_LONG.md (auto-distilled monthly).
|
|
- Mid-term memory lives in MEMORY_MID.md (auto-distilled weekly).
|
|
- Short-term memory lives in MEMORY_SHORT.md (auto-distilled daily).
|
|
- Pending reminders appear in REMINDERS.md — address them and they can be cleared.
|
|
|
|
---
|
|
|
|
*Add behavioral rules here as {display_name}'s personality develops.*
|
|
"""
|
|
|
|
|
|
def _user_profile(user_real_name: str) -> str:
|
|
return f"""\
|
|
# USER.md — {user_real_name}
|
|
|
|
*This file is {user_real_name}'s profile. Fill in details over time.*
|
|
|
|
---
|
|
|
|
## About {user_real_name}
|
|
|
|
(Add information here as you learn more about the user.)
|
|
|
|
---
|
|
|
|
## Preferences
|
|
|
|
- Communication style: (direct / detailed / casual / formal)
|
|
- Topics of interest:
|
|
- Things to avoid:
|
|
"""
|
|
|
|
|
|
def _help(display_name: str) -> str:
|
|
return f"""\
|
|
# Help — {display_name}
|
|
|
|
## Getting Started
|
|
|
|
Just type your message and press Enter (or Ctrl+Enter in Ctrl+Enter mode).
|
|
|
|
## Tips
|
|
|
|
- **Sessions** — your conversation history is preserved. Use the Sessions panel to revisit old chats.
|
|
- **Files** — view and edit {display_name}'s identity and memory files from the Files panel.
|
|
- **Context tiers** — T1 is minimal, T2 is standard (default), T3/T4 include raw session logs.
|
|
- **Memory** — {display_name}'s memory is distilled automatically. You can trigger it manually via ⚙ → Distill.
|
|
- **Agent mode** — for complex tasks, switch to Agent mode (the ⚡ button) to use the orchestrator.
|
|
|
|
## Logout
|
|
|
|
Click the ⏏ button in the top right.
|
|
"""
|