Every persona now knows: direct chat has no tools, Agent mode (⚡) has
the full tool suite. If asked to write a reminder/task/etc in chat mode,
tell the user to switch modes rather than silently failing.
Updated: inara, tina, donut, wintermute, developer, cleo PROTOCOLS.md
Updated: persona_template.py so all future personas get this by default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NameError: name 'user' is not defined in orchestrator._run_job —
user was resolved in the endpoint but not forwarded to the background task.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New cortex/tools/reminders.py with reminders_add, reminders_list, reminders_clear
- reminders_clear moved here from cron.py (cron still imports from same file)
- __init__.py: wired up new callables and Gemini declarations
- Inara can now add/read reminders in Agent mode via the orchestrator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use dgr_zone_nextcloud-app-1 throughout (actual container name)
- talk:bot:uninstall (not remove — wrong command in previous version)
- Added Logs section: occ log:tail + journalctl
- Bruteforce reset command now includes full docker exec form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New endpoints: POST /channels/google-chat/{username} and /webhook/nextcloud/{username}
- Channel secrets/config live in home/{username}/channels.json (gitignored)
- auth_utils: get_user_channels() helper reads channels.json
- Both routers load persona, audience/secret, backend, timeout per user;
set_context() wires the correct persona before building the system prompt
- Removed server-level channel settings from config.py and .env —
no user gets a channel until they create their own channels.json
- .gitignore: home/**/channels.json added
To migrate: update Google Chat Add-on webhook URL to /channels/google-chat/{username}
and re-register NC Talk bot at /webhook/nextcloud/{username}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Removed orphaned root .env and .env.default (values already in cortex/.env,
which is what the systemd service actually loads)
- Replaced outdated cortex/.env.example with the comprehensive .env.default content
- Also tracks: tested/persona/cleo/ (new test persona), Inara memory updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cortex/static/HELP.md: shared Help & Reference base served to all users
- help.html: loads shared base + appends persona-specific HELP.md if present
- inara/HELP.md: cleared (content moved to shared base)
- Google OAuth: registered scott.idem@oneskyit.com; flow now working end-to-end
- .gitignore: exclude home/**/sessions/ (runtime logs)
- New personas tracked: home/holly/persona/donut/, home/scott/persona/developer/
- Removed orphans: holly/, personas/, cortex-holly.service
- CLAUDE.md: updated current state and recently completed list to 2026-03-27
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change type="password" to type="text" — the main signal password
managers use. Also add autocomplete="off", data-lpignore, data-1p-ignore
for broader coverage across Bitwarden, 1Password, LastPass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The invite command reads email from profile.json, not auth.json.
google-add was only writing to auth.json so invite had no address
to send to. Now calls set_email() as well.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings page gains two new sections:
- Connected Accounts: shows linked Google email (read-only)
- Gemini API Key: paste personal key from aistudio.google.com,
shows masked hint of saved key, remove link to revert to server key
POST /settings/gemini-key saves/clears gemini_api_key in auth.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users with Google accounts can now sign in without a password.
Auth flow:
- GET /auth/google → Google consent page (CSRF state cookie)
- GET /auth/google/callback → exchange code, lookup user, set JWT
- auth.json gains google_sub + google_email fields
- set_password() no longer overwrites unrelated auth.json fields
Admin setup:
python manage_passwords.py google-add <username> <email>
# add GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET to .env
Per-user Gemini key:
- get_user_gemini_key() reads gemini_api_key from auth.json
- orchestrator_engine.run() accepts gemini_api_key param
- orchestrator router passes user's key, falls back to server key
login.html: "Sign in with Google" button above the password form.
manage_passwords.py list: now shows auth method columns (pw / google).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.js updates the <link rel="icon"> to the active persona's emoji on
load (CORTEX_EMOJI is already injected server-side). /favicon.ico route
added as a fallback for login/settings/help pages that don't have
persona context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
beforeunload closes the EventSource explicitly so the browser doesn't
log "connection interrupted while page was loading". onerror handler
suppresses auto-reconnect noise if the connection temporarily drops.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Session ID is stored in localStorage keyed to user+persona. On page load
it's silently restored if within 30 min of last activity. Timestamp
updates on every sent message. New session / delete session clears the
stored ID so the TTL logic stays consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pencil → edit, trash-2 → del, copy → copy, check → copied feedback,
check → Save, x → Cancel. All small action buttons get inline-flex
alignment for consistent icon+label layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Visiting /scott (or any user root) now shows a clean card page listing
all their personas with emoji + name, each linking to /{user}/{persona}.
Previously the route was unhandled (404 or wildcard match).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass ?persona= query param on the help link so the server knows which
persona to return to. Previously always defaulted to personas[0], causing
navigation back to the wrong persona.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sync preload script font sizes to match app.js (21/25/17px)
- Send button variants now show icons: ↑ Send, 📝 Note, ⚡ Run
- Remove fixed width on send-col; add white-space:nowrap + padding
so "📝 Note" never wraps regardless of font size
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Load Inter variable font from Google Fonts on all 5 HTML pages
- font-weight: 450 on body (between regular and medium — fixes thin feel)
- -webkit-font-smoothing: antialiased for cleaner screen rendering
- Base font size: normal 16→17px, large 18→19px, small 14→15px
- Applies consistently to main UI, login, setup, settings, and help pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Header:
- Sessions, ⚙ context panel, ≡ settings dropdown (Files, Account,
Sign Out), and ❓ help — down from 6+ individual buttons
- Responsive: flex-row on desktop, wraps on mobile with labels hidden
Footer (input area):
- 4-way mode select replaces the button row — shows only the active
mode as [icon] [label] ▲; click opens an upward dropdown
- Options sorted by MRU: most recently used floats to the bottom
(closest to the trigger button) for quick re-selection
- Current mode marked with ✓
- Note mode shows a small prv/pub sub-toggle below the select button
- Mobile: textarea on top (full width), mode select + send on one row
Mode state consolidated from 3 booleans into a single current_mode
variable with localStorage persistence and MRU tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a third input mode toggle alongside Note and Agent. When active:
- Textarea gets a subtle purple tint with dashed border
- OTR button highlights purple
- Placeholder reads "Off the record — not logged or distilled…"
- off_record=True is sent to /chat; session_logger is skipped
- In-memory session context is preserved within the session
Switching to Note or Agent mode deactivates OTR, and vice versa.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded 'Inara' replaced with CORTEX_PERSONA in all placeholder
strings (chat mode and agent task mode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix 'undefined' in auth banner: read access_token_hours_remaining (not hours_remaining)
- Fix false-positive warning on fresh tokens: when refresh token present, only warn
within 1 hour of expiry (not 24h) since the CLI should auto-rotate but sometimes misses
- Emit claude_auth_expired SSE event on 401 so UI shows inline red banner immediately
- app.js: handle claude_auth_expired SSE event with persistent top banner + dismiss button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Session name field: PATCH /sessions/{id} endpoint, inline rename button in UI
- Persona rename: inline ✏ toggle form in settings, POST /settings/persona/rename
- Username rename: inline form in settings, POST /settings/username (renames home dir, forces re-login)
- Help page: dedicated /help route replacing modal, collapsible sections
- Per-persona isolation: files.py and session_store.py now scope to correct user/persona
- Contrast/visibility: muted text bumped to slate-400+, session rename btn at 0.4 opacity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /settings page with password change form and personas list
- Add /help dedicated page (replaces help modal); renders HELP.md with
collapsible sections, dark theme, back link to active persona
- Add 👤 account button and convert ? button to link in header
- Remove help modal HTML and ~55 lines of modal JS from main app
- Register settings and help routers in main.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- session_store: store sessions under home/{user}/persona/{name}/session_data/
instead of the shared cortex/data/sessions/ bucket
- chat endpoints: add user/persona query params to /sessions, /history/*,
/sessions/*, /note so they resolve the correct persona context
- files router: add user/persona query params to /files and /files/{name}
so the file browser loads the right persona's files
- app.js: pass user/persona on all session, history, and file fetches;
move _fileParams to top-level scope so it is available everywhere
- onboarding: fix FastAPI route ordering — register /persona before /{token}
so the literal path wins and does not get captured as a token value
- ui.py: read Emoji field from IDENTITY.md and inject into CORTEX_CONFIG
so the header icon reflects each persona's chosen emoji
- .gitignore: exclude home/**/session_data/ (runtime state)
- migrate scott/inara sessions from cortex/data/sessions/ to session_data/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lifelong passion — Jurassic Park seen countless times. Tina should
engage with this genuinely, not just acknowledge it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ui.router's /{username}/{persona} wildcard was matching /static/style.css
(username="static", persona="style.css") because app.mount("/static") was
registered after app.include_router(ui.router). FastAPI processes routes in
registration order, so /static must be mounted first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- email_utils.py: send_email() via smtplib.SMTP_SSL (port 465, same server
as AE API); send_invite_email() renders plain-text + HTML invite template
- config.py: smtp_server, smtp_port, smtp_username, smtp_password,
smtp_from_email, smtp_from_name, cortex_base_url settings
- manage_passwords.py:
- profile.json helpers (get/set email stored in home/{username}/profile.json)
- invite command now accepts optional email arg, sends invite automatically;
falls back to stored email; prints link either way
- new 'email' command to store/update a user's email address
- 'list' command now shows email alongside password status
- .env.default: SMTP_* and CORTEX_BASE_URL documented
Usage after adding SMTP_PASSWORD to .env:
python manage_passwords.py invite holly holly@example.com
→ generates token, stores email, sends invite, prints link as fallback
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Restructures persona storage from a flat personas/{name}/ layout to
home/{username}/persona/{name}/, mirroring Linux home directories.
Changes:
- persona.py: two ContextVars (user + persona), Linux-style name validation,
set_context(), get_user(), get_persona(), validate(), list_users(),
list_user_personas(); persona_path() takes (username, name)
- config.py: replaces personas_dir with home_dir + home_root()
- git mv personas/inara → home/scott/persona/inara (history preserved)
- home/holly/persona/tina/: Holly's persona stub added
- cron_runner.py: all storage functions take (username, persona) params
- tools/cron.py: stamps user + persona on jobs; APScheduler IDs are
{user}:{persona}:{job_id} to prevent collisions across users
- memory_distiller.py: distill_short/mid/long take (username, persona);
added missing Path + settings imports
- scheduler.py: _load_user_crons() iterates home/*/persona/* (two-level)
- routers/chat.py, orchestrator.py: user field added; set_context() called
- tests/conftest.py: home_root fixture with two-level structure;
patches home_dir instead of personas_dir
- tests/test_persona.py: fully rewritten for two-level API
- tests/test_api_files.py: updated fixture name and path
- .env.default: documents HOME_DIR setting; scrubs stale API key
- CLAUDE.md, README.md: directory maps updated for new layout
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cortex/persona.py: ContextVar-based per-request routing with
path traversal protection and persona validation
- Migrate inara/ → personas/inara/ (git history preserved via git mv)
- config.py: add personas_root(), inara_path() delegates to personas/inara
- All 14 settings.inara_path() call sites replaced with persona_path()
- ChatRequest + OrchestrateRequest: add persona field (default: "inara")
with validation at request entry before any processing
- memory_distiller: add optional persona param for future per-persona distill
- cron_runner/tools/cron: stamp persona on jobs, prefix APScheduler IDs
(persona:job_id) to prevent collisions across personas
- scheduler: _load_user_crons() iterates all personas at startup
Adding a new persona: create personas/<name>/ with IDENTITY.md + SOUL.md.
Auth: handled at nginx level (inject X-Cortex-Persona header per subdomain).
Future: persona maps to Aether account_id_random for full integration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cortex/tools/scratch.py with scratch_read/write/append/clear tools
- Register all four scratch tools in the orchestrator tool registry
- Create inara/SCRATCH.md as the backing file (never distilled/archived)
- Fix auth.py: expiresAt reflects short-lived access token (~8h) not the
1-year refresh token — suppress expiry warning when refreshToken is present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Orchestrator now saves turns to session store so history survives page refresh
- UI session_id updated from job result; history controls attached to agent turns
- Cortex migrated from system service to systemd user service (no more sudo)
- Update README.md and CLAUDE.md with correct service commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Agent mode toggle to web UI input row — routes through POST /orchestrate
instead of /chat; polls for result with live tool-call count in thinking bubble
- Add cortex/tools/system.py with claude_allow_dir tool; registers in tool registry
- Fix web search: duckduckgo_search renamed to ddgs, update import + requirements.txt
- Allow WebSearch and WebFetch in ~/.claude/settings.json for Claude CLI fallback
- Add claude-allow-dir script docs and security note to CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All hardcoded "Inara"/"Scott" strings replaced with settings.agent_name
and settings.user_name, read from .env at startup:
- config.py: AGENT_NAME and USER_NAME settings (defaults: Inara / Scott)
- llm_client.py: conversation labels in prompt builder
- session_logger.py: **Name:** labels in session log markdown
- memory_distiller.py: distillation system prompts (mid + long)
- routers/nextcloud_talk.py: @mention prefix strip
- routers/google_chat.py: greeting message
Second instance scaffolding:
- holly/: identity directory with placeholder files (USER_NAME=Holly,
AGENT_NAME to be chosen by Holly)
- cortex/.env.holly: config for Holly's instance on port 8001
- cortex-holly.service: systemd unit for the second instance
No behavioural change to the Inara/Scott instance — defaults unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Session delete:
- DELETE /sessions/{session_id} endpoint (chat.py + session_store.py)
- × button on each session item in the panel (hover-reveal on desktop)
- Clears UI if the active session is deleted
Touch accessibility:
- @media (hover: none) rule makes msg-actions always visible on touch devices
- msg-act-btn tap targets enlarged to 36px min-height, readable font size
- session-delete-btn also always visible and finger-sized on touch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>