Commit Graph

36 Commits

Author SHA1 Message Date
Scott Idem
8aec6aafcc feat: Google OAuth sign-in + per-user Gemini API key
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>
2026-03-27 21:01:52 -04:00
Scott Idem
62fde62653 feat: persona-specific favicon + fix favicon.ico 404
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>
2026-03-26 23:45:36 -04:00
Scott Idem
f8d89bc272 fix: close SSE connection cleanly on page navigation
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>
2026-03-26 23:44:12 -04:00
Scott Idem
92350f7a7b feat: persist active session across page navigation with inactivity TTL
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>
2026-03-26 23:38:04 -04:00
Scott Idem
4f09823afe feat: Lucide icons on edit/del/copy and inline edit save/cancel buttons
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>
2026-03-26 23:32:19 -04:00
Scott Idem
c3507f8e11 fix: help page back link preserves active persona
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>
2026-03-26 23:13:52 -04:00
Scott Idem
65548ebf36 feat: Lucide SVG icons throughout main UI
Replace all emoji/unicode icons with Lucide SVG icons:
- Mode select dropdown: message-circle / pencil / lock / bot
- Send button: arrow-up (chat/OTR), pencil (note), zap (agent)
- Stop button: square icon
- Header nav already had Lucide SVGs; render_icons() now called at init

Add icon_html() + render_icons() helpers; update update_mode_ui() and
open_mode_dropdown() to use innerHTML + lucide.createIcons(). CSS: .btn-icon
alignment, inline-flex on .hdr-btn / .hdr-dd-item / #send / #stop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:06:01 -04:00
Scott Idem
24c9f52b49 fix: agent mode icon — 🥸 in dropdown, on Run button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:51:51 -04:00
Scott Idem
3bada5f311 fix: font sizes 21/25/17px, icon labels on send btn, no-wrap fix
- 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>
2026-03-26 22:43:34 -04:00
Scott Idem
cf277f822e feat: Inter font, weight 450, bumped base sizes across all pages
- 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>
2026-03-26 22:20:10 -04:00
Scott Idem
6cf10d2755 feat: UI redesign — compact mode select, consolidated header nav
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>
2026-03-26 22:08:33 -04:00
Scott Idem
fa04b5e6b0 feat: off the record mode (OTR)
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>
2026-03-26 21:07:21 -04:00
Scott Idem
b3f40cf437 fix: message input placeholder uses active persona name
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>
2026-03-26 20:59:37 -04:00
Scott Idem
8487645224 fix: claude auth expiry warning — correct field name and smarter threshold
- 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>
2026-03-25 23:22:18 -04:00
Scott Idem
0cf0d65e9e feat: session naming, username/persona rename, help page, contrast fixes
- 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>
2026-03-23 23:10:12 -04:00
Scott Idem
1b425a539f feat: account settings page + dedicated help page
- 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>
2026-03-23 21:41:18 -04:00
Scott Idem
c01ef663f5 fix: per-persona session/file isolation + onboarding route order
- 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>
2026-03-22 00:01:07 -04:00
Scott Idem
46b65d087c feat: persona onboarding — invite tokens, self-service setup, persona creation, switcher
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>
2026-03-20 23:10:32 -04:00
Scott Idem
a9bbb668b5 feat: session auth + per-user/persona UI at /{user}/{persona}
Replaces nginx basic auth with a proper per-user session system:

- auth_utils.py: bcrypt password hashing, JWT cookie creation/decode
- auth_middleware.py: validates JWT cookie on all routes except /login,
  /health, /static/, and webhook endpoints (/channels/, /webhook/)
- routers/ui.py: GET /login, POST /login, POST /logout,
  GET /{username}/{persona} — serves index.html with CORTEX_CONFIG injected
- static/login.html: minimal login form (dark theme, matches UI)
- main.py: registers SessionAuthMiddleware + ui.router
- config.py: jwt_secret, jwt_expire_days settings
- manage_passwords.py: CLI tool to set/check/list user passwords
- app.js: reads window.CORTEX_CONFIG (user + persona), sends both on
  every /chat and /orchestrate request; persona name shown in header;
  logout button (⏏) added to header
- requirements.txt: bcrypt, PyJWT, python-multipart
- .env.default: JWT_SECRET, JWT_EXPIRE_DAYS documented
- tests: client fixture injects JWT cookie; security test assertions
  updated for URL-normalized path traversal paths (still secure, codes differ)

All 80 tests pass.

Setup for a new user:
  python manage_passwords.py set scott
  python manage_passwords.py set holly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:54:12 -04:00
Scott Idem
aaac3e1353 feat: persist orchestrator sessions + user service + docs update
- 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>
2026-03-18 23:08:38 -04:00
Scott Idem
9b818aa5c7 feat: orchestrator Agent mode UI + claude_allow_dir tool + fix DDG search
- 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>
2026-03-18 22:42:44 -04:00
Scott Idem
0b10558f80 fix: session delete button always visible, session name truncates properly
- Remove opacity:0/hover-reveal on session × — always shown in the panel
- session-id: flex:1 + text-overflow:ellipsis prevents overflow pushing × offscreen
- min-width on delete btn ensures tap target is always reachable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 19:45:05 -04:00
Scott Idem
f935fc4a7f feat: session delete + touch-friendly message controls
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>
2026-03-18 19:43:20 -04:00
Scott Idem
23f8659aaa UI: fix mobile input area layout
- Stack textarea above button row on mobile (flex-direction: column)
- font-size: 16px on textarea prevents iOS Safari auto-zoom on focus
- body height: 100dvh adjusts dynamically as soft keyboard opens/closes
- Right col goes horizontal (row) with full width on mobile
- Hide height-row and enter-toggle (desktop-only concepts)
- Larger touch targets for Send/Stop/Note
- Hide session-id to reclaim vertical space

Desktop layout unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:44:21 -04:00
Scott Idem
fe6561bf6a feat: add Gemini auth check to token warning banner
/auth/status now returns per-backend status: Claude warns on <24h expiry,
Gemini warns only when oauth_creds.json is missing or has no refresh_token
(access token rotates automatically so expiry_date is not a useful signal).
Banner shows warnings for both backends when needed, and the hint text
names the specific CLI commands to run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:29:25 -04:00
Scott Idem
1127610752 UI: show distill schedule next-run times in settings panel
Fetches /distill/status when the ⚙ panel opens and renders next run
times below the distill buttons (monospace, muted). Shows "today",
"tomorrow", or "Mar 18" format depending on how far away.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:22:47 -04:00
Scott Idem
2144d7c2c0 UI: collapsible sections in help modal
Post-processes rendered markdown: each H2 becomes a <details>/<summary>.
Top 4 sections (Header Controls, Chat, Sessions, Notes) open by default;
remaining sections (Backends, Talk, Files, Context, Shortcuts, API,
Planned) start collapsed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:16:11 -04:00
Scott Idem
2ca02006dd UI: auth banner — add re-auth hint for multi-user context
Banner now shows a second line explaining how to fix it: SSH to the
Cortex host, run `claude`, follow the login prompt, restart Cortex.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:09:11 -04:00
Scott Idem
48a6734ec3 feat: Claude CLI OAuth token expiry warning
New GET /auth/status endpoint reads ~/.claude/.credentials.json and
returns hours remaining + warning flag. UI shows a dismissible amber
banner when < 24h remain, turning red if expired. Checked on page load
and every 30 minutes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:06:28 -04:00
Scott Idem
7b51e7cc44 UI: mobile-friendly header — move backend/display into settings panel
Header trimmed to 4 buttons (Sessions, Files, ⚙, ?). Backend toggle,
font size, and theme moved into the ⚙ settings panel under new Backend
and Display sections. Panels use responsive widths to avoid overflow on
small screens. Mobile breakpoints tighten padding and hide subtitle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 23:03:55 -04:00
Scott Idem
0ebfbc6590 Refactor UI into separate CSS/JS, add help modal and HELP.md
- static/index.html: reduced to 127-line HTML shell
- static/style.css: all styles extracted (~900 lines) + help modal styles
  + shared markdown rendering for file-preview and help-modal-body
  including tables (previously missing)
- static/app.js: all JS extracted (~900 lines) + help modal fetch/render
- index.html: adds ? help button + help modal HTML
- inara/HELP.md: comprehensive reference doc covering all features,
  keyboard shortcuts, API endpoints, memory system, planned items
- routers/files.py: HELP.md added to ALLOWED set
- context_loader.py: HELP.md loaded at tier 2+ (after PROTOCOLS.md)
  so Inara can reference it when helping Scott with the interface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:52:54 -04:00
Scott Idem
fa96c50935 UI: font size cycle button (Aa / A+ / A−)
Cycles normal (16px) → large (18px) → small (14px) on the root element
so all rem-based text scales together. Persisted in localStorage, applied
before first paint to avoid flash. Also include today's session log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:40:14 -04:00
Scott Idem
c402b97bf3 UI: context panel popover, light/dark theme, contrast improvements
- Light/dark mode: full palette via @media + [data-theme] override;
  theme toggle button (☾/☀) persists to localStorage; no flash on load
- Dark mode softened: #1a1228 bg (was near-pure black), improved muted
  text contrast (#9080a8)
- CSS variables: --shadow, --modal-overlay, --code-bg, --pre-bg,
  --success, --success-dim replace all hardcoded rgba/hex values
- Context panel: ⚙ header button opens dropdown (like Sessions);
  tier badge shows current tier; closes on outside click
- Removed always-visible context bar; controls now in panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:32:54 -04:00
Scott Idem
ce3c1f5f7f Add tiered memory system with manual distillation
- config.py: memory_budget_long/mid/short settings (overridable in .env)
- memory_distiller.py: distill_short (no LLM), distill_mid, distill_long (LLM)
- routers/distill.py: POST /distill/{short,mid,long,all} endpoints
- context_loader.py: rewrote to load long→mid→short order with include_* toggles
- routers/chat.py: ChatRequest gains include_long/mid/short fields
- routers/files.py: MEMORY_LONG/MID/SHORT.md added to ALLOWED set
- main.py: register distill router
- static/index.html: context bar — tier selector, L/M/S memory toggles,
  distill buttons with status feedback; send includes tier + memory flags
- inara/MEMORY_LONG.md: migrated from MEMORY.md + Cortex/Talk bot notes
- inara/MEMORY_MID.md, MEMORY_SHORT.md: stubs ready for distillation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:22:32 -04:00
Scott Idem
8add4ffd02 Add edit/delete history, named sessions, scroll fix, systemd service
- Edit/delete individual messages from session context with inline editing
  (Ctrl+Enter saves, Escape cancels); changes sync to backend via PUT /history
- PUT /history/{session_id} endpoint to replace full message list
- Named sessions: readable slugs (e.g. quiet-spring) instead of UUID fragments
- Scroll no longer snaps to bottom when user has scrolled up to read history
- cortex.service: systemd unit for auto-start and restart-on-failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 23:38:39 -04:00
Scott Idem
2f675ee4bf Initial commit — Cortex API + Inara identity
Cortex: FastAPI backend serving Inara via Claude/Gemini CLI backends.
Includes SSE streaming chat, session persistence, Google Chat webhook
handler, and Docker support.

Inara: Identity files (persona, soul, protocols, memory, context tiers)
mounted read-only into the container at runtime.

Features in initial cut:
- /chat endpoint with SSE keepalive + LLM fallback
- Session store with rolling history window
- Markdown rendering, copy-to-clipboard, links open in new tab
- Stacked right-column input controls (height selector, enter toggle,
  note mode with public/private) — semi-hidden until textarea grows
- /note endpoint for injecting public context into session history
- Docker Compose config (local dev runs natively; Docker for server)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 03:41:00 -05:00