diff --git a/cortex/model_registry.py b/cortex/model_registry.py index 557bd8f..b506b40 100644 --- a/cortex/model_registry.py +++ b/cortex/model_registry.py @@ -106,6 +106,18 @@ GOOGLE_CATALOG: list[dict] = [ {"id": "gemini-3.1-flash-lite-preview", "label": "Gemini 3.1 Flash-Lite (preview)", "context_k": 1000}, ] +# Known OpenAI-compatible cloud inference services. +# All use host_type "openai" (/chat/completions + /models paths). +CLOUD_API_CATALOG: list[dict] = [ + {"id": "openrouter", "label": "OpenRouter", "api_url": "https://openrouter.ai/api/v1"}, + {"id": "openai", "label": "OpenAI", "api_url": "https://api.openai.com/v1"}, + {"id": "groq", "label": "Groq", "api_url": "https://api.groq.com/openai/v1"}, + {"id": "xai", "label": "X.ai / Grok", "api_url": "https://api.x.ai/v1"}, + {"id": "together", "label": "Together.ai", "api_url": "https://api.together.xyz/v1"}, + {"id": "fireworks", "label": "Fireworks.ai", "api_url": "https://api.fireworks.ai/inference/v1"}, + {"id": "custom", "label": "Custom", "api_url": ""}, +] + # ── Built-in model definitions ──────────────────────────────────────────────── @@ -148,6 +160,8 @@ _ROLE_LAST_RESORT: dict[str, str] = { PRIORITY_KEYS = ["primary", "backup_1", "backup_2", "backup_3", "backup_4"] +REQUIRED_ROLES: list[str] = ["chat", "orchestrator", "distill"] + # ── Storage ─────────────────────────────────────────────────────────────────── @@ -565,6 +579,8 @@ def get_catalog(provider: str, username: str | None = None) -> list[dict]: return list(ANTHROPIC_CATALOG) if provider == "google": return list(GOOGLE_CATALOG) + if provider == "cloud": + return list(CLOUD_API_CATALOG) return [] @@ -851,6 +867,52 @@ def remove_model(username: str, model_id: str) -> bool: return len(data["models"]) < before +def get_custom_roles(username: str) -> list[str]: + """ + Return the user's custom (non-required) roles. + Falls back to config-defined roles minus required ones for migration. + """ + registry = _load(username) + if "custom_roles" in registry: + return [r for r in registry["custom_roles"] if r and r not in REQUIRED_ROLES] + from config import settings as _cfg + return [r for r in _cfg.get_defined_roles() if r not in REQUIRED_ROLES] + + +def get_all_roles(username: str) -> list[str]: + """Return required roles followed by the user's custom roles.""" + return list(REQUIRED_ROLES) + get_custom_roles(username) + + +def add_custom_role(username: str, role_name: str) -> bool: + """Add a custom role. Returns False if the name is invalid or already a required role.""" + role_name = role_name.strip().lower() + if not role_name or role_name in REQUIRED_ROLES: + return False + data = _load(username) + if "custom_roles" not in data: + from config import settings as _cfg + data["custom_roles"] = [r for r in _cfg.get_defined_roles() if r not in REQUIRED_ROLES] + if role_name not in data["custom_roles"]: + data["custom_roles"].append(role_name) + _save(username, data) + return True + + +def remove_custom_role(username: str, role_name: str) -> bool: + """Remove a custom role. Required roles cannot be removed.""" + if role_name in REQUIRED_ROLES: + return False + data = _load(username) + if "custom_roles" not in data: + from config import settings as _cfg + data["custom_roles"] = [r for r in _cfg.get_defined_roles() if r not in REQUIRED_ROLES] + if role_name in data["custom_roles"]: + data["custom_roles"].remove(role_name) + _save(username, data) + return True + + def set_role(username: str, role: str, priority: str, model_id: str | None) -> bool: """ Assign a model to a role priority slot. diff --git a/cortex/routers/local_llm.py b/cortex/routers/local_llm.py index 3d67125..0669fda 100644 --- a/cortex/routers/local_llm.py +++ b/cortex/routers/local_llm.py @@ -13,6 +13,8 @@ Routes: POST /settings/local/models/add → add a model (any provider) POST /settings/local/models/{id}/edit → edit an existing model entry POST /settings/local/models/{id}/remove → remove a model + POST /settings/local/roles/add → add a custom role (redirects to #roles) + POST /settings/local/roles/remove → remove a custom role (redirects to #roles) POST /api/models/role → AJAX: set a role assignment GET /api/local-llm/fetch-models → proxy to host /api/models (JSON) """ @@ -56,6 +58,70 @@ router = APIRouter() _STATIC = Path(__file__).parent.parent / "static" +def _host_row_html(h: dict) -> str: + """Return the HTML for one host config row (edit form + remove link).""" + api_key = h.get("api_key", "") + key_hint = f"…{api_key[-4:]}" if api_key else "not set" + ht = h.get("host_type", "openwebui") + ow = ' selected' if ht == "openwebui" else '' + ai = ' selected' if ht == "openai" else '' + hid = h["id"] + hlbl = h.get("label", "") + hurl = h.get("api_url", "") + maxc = h.get("max_concurrent", 3) + return f''' +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + +

Current: {key_hint}

+
+
+ + +
+
+ + +
+
+
+ + + +
+
+
+ +
+
''' + + # ── Auth helper ─────────────────────────────────────────────────────────────── def _get_user(request: Request) -> str | None: @@ -97,66 +163,16 @@ def _render(username: str, request: Request | None = None, success: str = "", er if not google_account_rows: google_account_rows = '

No accounts configured yet.

' - # ── Local host rows ─────────────────────────────────────────────────────── - host_rows = "" - for h in hosts: - key_hint = f"…{h['api_key'][-4:]}" if h.get("api_key") else "not set" - ht = h.get("host_type", "openwebui") - ow = ' selected' if ht == "openwebui" else '' - ai = ' selected' if ht == "openai" else '' - host_rows += f''' -
-
- -
-
- - -
-
- - -
-
-
-
- - -

Current: {key_hint}

-
-
- - -
-
- - -
-
-
- - - -
-
-
- -
-
''' - if not host_rows: - host_rows = '

No hosts configured yet. Add one below.

' + # ── Host rows — split cloud (openai) vs local (openwebui) ───────────────── + cloud_hosts = [h for h in hosts if h.get("host_type") == "openai"] + local_hosts = [h for h in hosts if h.get("host_type", "openwebui") != "openai"] + + cloud_host_rows = "".join(_host_row_html(h) for h in cloud_hosts) + local_host_rows = "".join(_host_row_html(h) for h in local_hosts) + if not cloud_host_rows: + cloud_host_rows = '

No cloud API services configured yet. Add one below.

' + if not local_host_rows: + local_host_rows = '

No local hosts configured yet. Add one below.

' host_options = "".join( f'' @@ -360,15 +376,35 @@ def _render(username: str, request: Request | None = None, success: str = "", er model_opts += f' \n' model_opts += '\n' + all_roles = reg.get_all_roles(username) + role_rows = "" - for role in app_settings.get_defined_roles(): + for role in all_roles: + is_required = role in reg.REQUIRED_ROLES role_cfg = roles.get(role, {}) + role_title = role.replace("_", " ").title() + required_badge = ( + 'required' + if is_required else '' + ) + rcp_danger = ( + '' if is_required else + f'
' + f'
' + f'' + f'' + f'
' + f'
' + ) role_rows += ( f'
' - f'{role.title()}' + f'
' + f'{role_title}' + f'{required_badge}' + f'
' f'
' ) - for slot in reg.PRIORITY_KEYS[:3]: + for slot in reg.PRIORITY_KEYS[:2]: slot_label = slot.replace("_", " ").title() sel = ( f'' f'
' - f'
' + f'
' + f'
' f'' - f'
' + f'

Disable both for pure processing roles (summarizer, classifier, translator).

' f'
' f'
' f'
' + f'{rcp_danger}' f'
' ) role_data_js = _json.dumps({ - role: {slot: (roles.get(role, {}).get(slot) or "") for slot in reg.PRIORITY_KEYS[:3]} - for role in app_settings.get_defined_roles() + role: {slot: (roles.get(role, {}).get(slot) or "") for slot in reg.PRIORITY_KEYS[:2]} + for role in all_roles }) role_config_data_js = _json.dumps({ @@ -421,14 +459,15 @@ def _render(username: str, request: Request | None = None, success: str = "", er "inject_datetime": roles.get(role, {}).get("inject_datetime", True), "inject_mode": roles.get(role, {}).get("inject_mode", True), } - for role in app_settings.get_defined_roles() + for role in all_roles }) tool_categories_js = _json.dumps(TOOL_CATEGORIES) # ── Catalog data + Google accounts for JS ───────────────────────────────── - google_accounts_js = _json.dumps(reg.get_google_accounts(username)) - google_catalog_js = _json.dumps(reg.get_catalog("google")) + google_accounts_js = _json.dumps(reg.get_google_accounts(username)) + google_catalog_js = _json.dumps(reg.get_catalog("google")) anthropic_catalog_js = _json.dumps(reg.get_catalog("anthropic")) + cloud_catalog_js = _json.dumps(reg.get_catalog("cloud")) has_hosts = "true" if hosts else "false" html = (_STATIC / "local_llm.html").read_text() @@ -436,7 +475,8 @@ def _render(username: str, request: Request | None = None, success: str = "", er "{{ username }}": username, "{{ google_account_rows }}": google_account_rows, "{{ anthropic_key_rows }}": anthropic_key_rows, - "{{ host_rows }}": host_rows, + "{{ cloud_host_rows }}": cloud_host_rows, + "{{ local_host_rows }}": local_host_rows, "{{ model_rows }}": model_rows, "{{ host_options }}": host_options, "{{ role_rows }}": role_rows, @@ -447,6 +487,7 @@ def _render(username: str, request: Request | None = None, success: str = "", er "{{ anthropic_keys_js }}": anthropic_keys_js, "{{ google_catalog_js }}": google_catalog_js, "{{ anthropic_catalog_js }}": anthropic_catalog_js, + "{{ cloud_catalog_js }}": cloud_catalog_js, "{{ has_hosts }}": has_hosts, } for key, val in replacements.items(): @@ -669,6 +710,40 @@ async def remove_model(request: Request, model_id: str): return HTMLResponse(_render(username, request, success="Model removed.")) +@router.post("/settings/local/roles/add", include_in_schema=False) +async def add_custom_role_route( + request: Request, + role_name: str = Form(""), +): + username = _get_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + name = role_name.strip().lower() + if not name or not name[0].isalpha(): + return HTMLResponse(_render(username, request, error="Role name must start with a letter.")) + ok = reg.add_custom_role(username, name) + if not ok: + return HTMLResponse(_render(username, request, error=f'"{name}" is a required role and cannot be re-added.')) + logger.info("custom role added: %s / %s", username, name) + return RedirectResponse("/settings/models#roles", status_code=303) + + +@router.post("/settings/local/roles/remove", include_in_schema=False) +async def remove_custom_role_route( + request: Request, + role_name: str = Form(""), +): + username = _get_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + name = role_name.strip() + ok = reg.remove_custom_role(username, name) + if not ok: + return HTMLResponse(_render(username, request, error=f'"{name}" is a required role and cannot be removed.')) + logger.info("custom role removed: %s / %s", username, name) + return RedirectResponse("/settings/models#roles", status_code=303) + + @router.post("/api/models/role") async def set_role(request: Request) -> JSONResponse: """AJAX: assign a model to a role priority slot. diff --git a/cortex/static/HELP.md b/cortex/static/HELP.md index c44cbe9..7b0005c 100644 --- a/cortex/static/HELP.md +++ b/cortex/static/HELP.md @@ -268,17 +268,24 @@ The label and context window size auto-fill from the catalog — edit them if yo ### Step 3 — Assign models to roles -Scroll to **Role Assignments** at the bottom of the page. Each role has **Primary**, **Backup 1**, and **Backup 2** slots — Primary is tried first, then backups in order. Changes save automatically. +Scroll to **Role Assignments** at the bottom of the page. Each role has **Primary** and **Backup** slots — Primary is tried first, then backup. Changes save automatically. + +**Required roles** (always present, cannot be removed): | Role | Used for | |---|---| | **Chat** | Regular conversation | | **Orchestrator** | Agent mode tool loop | | **Distill** | Memory distillation (short / mid / long) | -| **Coder** | Code-focused tasks | -| **Research** | Long-context research tasks | -Leave all slots empty to use the server default. +**Custom roles** — Click **+ Add custom role** to create your own. Each custom role gets its own model selection, tool set, and system prompt addition. Good examples: + +| Example | Purpose | +|---|---| +| **Coder** | Code-focused tasks — larger context window, code-aware model | +| **Research** | Long-context research — high-token model, web tools prioritized | + +Switch roles via the **Role** selector in the Context & Memory panel (⚙). Leave all slots empty to use the server default. **Per-role tool sets:** Expand any role card to configure which tool categories the orchestrator can use when that role is active. Unchecked categories are hidden from the model entirely — reducing token overhead on every orchestrated call. Leaving all categories unchecked means all tools the user has access to are available (the default). @@ -288,7 +295,7 @@ Leave all slots empty to use the server default. ## Nextcloud Talk Bot -Inara is registered as a bot in Nextcloud Talk. +The Cortex bot is registered in Nextcloud Talk. - Messages sent in enabled Talk conversations are received by Cortex, processed, and replied to. - The webhook returns `200 OK` immediately; the reply happens asynchronously. @@ -299,12 +306,12 @@ Inara is registered as a bot in Nextcloud Talk. ## Google Chat Bot -Inara is available as a bot in Google Chat (One Sky IT Workspace). +The Cortex bot is available in Google Chat (One Sky IT Workspace). -- Send Inara a direct message in Google Chat to start a conversation. +- Send the bot a direct message in Google Chat to start a conversation. - Each DM thread is its own session (`gc_spaces/*` prefix) — history persists across messages. - Responses are synchronous — Google Chat displays the reply directly in the thread. -- To add Inara to a space: open the space, add a person/app, search for **Inara**. +- To add the bot to a space: open the space, click **Add people & apps**, and search for the Cortex bot. - Sessions from Google Chat appear as `gc_*` prefixed IDs in the Sessions panel. --- @@ -339,9 +346,9 @@ Cortex can send browser push notifications — even when the tab is closed. - Open **☰ → Enable notifications** and accept the browser permission prompt. - Once enabled, the button shows **Notifications on** (in accent colour). - Click again to disable. Subscriptions are stored per-device. -- The orchestrator's `web_push` tool lets Inara send you a push proactively (e.g. when a long task completes). +- The orchestrator's `web_push` tool lets your persona send you a push proactively (e.g. when a long task completes). -**Notification channel settings:** ☰ → **Account** → **Notification settings →** — choose Browser Push, Email, Nextcloud Talk, or Google Chat as the channel Inara uses for scheduled reminders, cron job completions, and memory digests. Use the **Send Test Notification** button to verify your setup, or **Check Reminders Now** to trigger the reminder check immediately. +**Notification channel settings:** ☰ → **Account** → **Notification settings →** — choose Browser Push, Email, Nextcloud Talk, or Google Chat as the channel your persona uses for scheduled reminders, cron job completions, and memory digests. Use the **Send Test Notification** button to verify your setup, or **Check Reminders Now** to trigger the reminder check immediately. --- @@ -389,7 +396,7 @@ Distillation builds up the memory layers from raw session logs. Runs automatical ## Scheduled Jobs -Cortex can run recurring jobs on a schedule — reminders, daily briefings, automated research, and more. Manage them by asking Inara to set them up, or go directly to **☰ → Account → Schedules**. +Cortex can run recurring jobs on a schedule — reminders, daily briefings, automated research, and more. Manage them by asking your persona to set them up, or go directly to **☰ → Account → Schedules**. ### Job Types @@ -424,7 +431,7 @@ Schedules take effect immediately when added or edited — no restart needed. Pa ### Home Assistant Integration -HA automations can trigger Inara via webhook. Configure in **Notifications → Home Assistant → Inbound webhook**: +HA automations can trigger your persona via webhook. Configure in **Notifications → Home Assistant → Inbound webhook**: - Set a **Webhook ID** (long random string — this is your secret URL component) - Your endpoint: `https://cortex.dgrzone.com/webhook/ha/{username}/{webhook_id}` diff --git a/cortex/static/crons.html b/cortex/static/crons.html index d80a7e6..38d1a5f 100644 --- a/cortex/static/crons.html +++ b/cortex/static/crons.html @@ -7,9 +7,36 @@ + + @@ -79,6 +100,7 @@ ← Chat Help Settings + Models Notifications Tools Schedules @@ -99,11 +121,11 @@ {{ cron_list_html }} - +

Add schedule

-
+
- +
diff --git a/cortex/static/help.html b/cortex/static/help.html index b9c5bdc..504fe8f 100644 --- a/cortex/static/help.html +++ b/cortex/static/help.html @@ -8,38 +8,40 @@ + + @@ -92,6 +92,7 @@ ← Chat Help Settings + Models Notifications Tools Schedules @@ -99,23 +100,33 @@ Sign out -
-
-

Help & Reference

-

-
-
- - - +
+
+

Help & Reference

+

-

Loading…

-

Loading…

-

Loading…

+ +
+ + + +
+ +

Loading…

+

Loading…

+

Loading…

+ + + @@ -44,6 +50,7 @@ ← Chat Help Settings + Models Notifications Tools Schedules @@ -60,7 +67,6 @@
-

Aether Platform Database

@@ -69,14 +75,18 @@ Only SELECT, SHOW, DESCRIBE, and EXPLAIN are permitted — no writes possible.

-
- Connection -
-

- Use the same credentials as agents_sync/mcp/scripts/sql_inspector.py. - The password field is left blank in the form — leave it blank to keep the stored value. +

+ + Connection + +
+

+ Use the same credentials as + agents_sync/mcp/scripts/sql_inspector.py. + Leave the password blank to keep the stored value.

-
+
-
+
- +
diff --git a/cortex/static/local_llm.html b/cortex/static/local_llm.html index ff9774b..1177437 100644 --- a/cortex/static/local_llm.html +++ b/cortex/static/local_llm.html @@ -8,246 +8,74 @@ + + @@ -263,304 +91,561 @@ Sign out +

Model Registry

Configure providers, hosts, and model assignments.

- -
-

Cloud Providers

+ +
+ + + + +
-
-
-
A
-
-
Anthropic
-
Claude via CLI (OAuth) or direct API key
+ +
-
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +

Informational labels — used for display and future filtering.

+
- -
-

Role Assignments

-

- Map each task type to a model. Primary is tried first; backups are used if primary fails or is unavailable. -

- {{ role_rows }} -
+
+ + + +
+ +
+ +
+ +
+ + +
+ +
+

Role Assignments

+

+ Map each task type to a model. Primary is tried first; Backup is the config-resolution fallback + if the primary model is missing from the registry. Backup does not fire on inference failures. +

+ {{ role_rows }} +
+ + Add custom role +
+
+
+
+ + +
+ +
+
+
+
+
+ +
- + @@ -58,6 +54,7 @@ ← Chat Help Settings + Models Notifications Tools Schedules @@ -67,7 +64,7 @@

Notifications

-

How Inara reaches out proactively — reminders, cron jobs, and memory digests.

+

How your persona reaches out proactively — reminders, cron jobs, and memory digests.

@@ -90,8 +87,9 @@

Used for reminder alerts, distillation summaries, and cron job notifications.

-
@@ -317,7 +302,9 @@ try { const d = await fetch('/backend').then(r => r.json()); if ((d.available_roles || []).length === 0) { - document.getElementById('openrouter-quickstart').style.display = 'block'; + const el = document.getElementById('openrouter-quickstart'); + el.classList.remove('hidden'); + el.style.display = 'block'; } } catch (_) {} })(); @@ -375,10 +362,12 @@ const n = data.named ?? 0; ok.textContent = `Named ${n} session${n !== 1 ? 's' : ''}.`; ok.style.display = 'inline'; + ok.classList.remove('hidden'); } catch (e) { ok.textContent = 'Error — check console.'; ok.style.color = '#f87171'; ok.style.display = 'inline'; + ok.classList.remove('hidden'); } btn.textContent = 'Auto-name old sessions'; btn.disabled = false; diff --git a/cortex/static/tools_settings.html b/cortex/static/tools_settings.html index c7dbc12..7a3e5c3 100644 --- a/cortex/static/tools_settings.html +++ b/cortex/static/tools_settings.html @@ -7,42 +7,36 @@ + + @@ -105,6 +93,7 @@ ← Chat Help Settings + Models Notifications Tools Schedules @@ -125,55 +114,55 @@
- -
-

Risk Policy

-
- Max risk level -
-

- Low tools are read-only and sandboxed (web search, project file reads, HA status checks).
- Medium tools write to local data or send notifications to you (cron jobs, scratch, task management).
- High tools affect external systems or the host (shell exec, email, device control, service restart). +

+ Low tools are read-only and sandboxed (web search, project file reads, HA status checks).
+ Medium tools write to local data or send notifications to you (cron jobs, scratch, task management).
+ High tools affect external systems or the host (shell exec, email, device control, service restart).

-

+

The Auto column below shows each tool's status at your current max risk level. Use the override column to force-include or force-exclude individual tools.

-
- Auto-included by risk level - Auto-excluded by risk level +
+ Auto-included by risk level + Auto-excluded by risk level
- + {{ tool_table_html }} - -
-

Confirmation Gate

-

+ +

+

Confirmation Gate

+

Some tools require explicit confirmation before executing. Override the defaults here.
- Tools requiring confirmation by default: {{ confirm_required_tools }} + Tools requiring confirmation by default: {{ confirm_required_tools }}

-
-
- +
+
+

One tool name per line. These tools skip the confirmation prompt.

-
- +
+ @@ -182,8 +171,8 @@
-
- +
+
diff --git a/documentation/MASTER.md b/documentation/MASTER.md index 848bbd4..d009f23 100644 --- a/documentation/MASTER.md +++ b/documentation/MASTER.md @@ -1,4 +1,4 @@ -# Cortex / Inara — Master Index +# Cortex — Master Index > Start here. This document is a map, not a manual. > Last updated: 2026-05-13 @@ -10,7 +10,7 @@ ## What It Is -Cortex is a self-hosted personal AI platform. It routes messages from any input channel to AI backends, manages a resident agent (Inara) with persistent memory, and coordinates across a fleet of machines. It is infrastructure, not a product. +Cortex is a self-hosted personal AI platform. It routes messages from any input channel to AI backends, manages per-user AI personas with persistent memory, and coordinates across a fleet of machines. It is infrastructure, not a product. **Running at:** `https://cortex.dgrzone.com` | `systemctl --user restart cortex` @@ -43,7 +43,7 @@ Cortex is a self-hosted personal AI platform. It routes messages from any input | Distill safety | ✅ Live | Per-persona asyncio lock, per-endpoint cooldowns, Rebuild option | | Guided onboarding | ✅ Live | Setup Step 3 for OpenRouter; existing-user banner; settings quick-link | -**65 orchestrator tools** across 17 domain modules — added 2026-05-12: `file_diff`, `git_status` / `git_log` / `git_diff` (read-only git inspection), `ae_db_query` / `ae_db_describe` / `ae_db_show_view` (SELECT-only Aether MariaDB access, admin, per-user credentials). `/settings/integrations` page added (admin-only). File attachments in chat (images for vision-capable local models; text/code files for all backends). Settings pages unified under `pg.css`. Added 2026-05-13: `task` cron type (full orchestrator loop on a schedule); monthly/yearly schedule formats (`monthly`, `monthly:DD:HH:MM`, `yearly:MM:DD:HH:MM`); Schedules web UI at `/settings/crons` (list, add, edit, pause, delete); HA inbound webhook tools toggle (orchestrator vs. direct LLM); Anthropic API key backend (`anthropic_api` model type via Anthropic SDK — alternative to CLI OAuth). +**65 orchestrator tools** across 17 domain modules — added 2026-05-12: `file_diff`, `git_status` / `git_log` / `git_diff` (read-only git inspection), `ae_db_query` / `ae_db_describe` / `ae_db_show_view` (SELECT-only Aether MariaDB access, admin, per-user credentials). `/settings/integrations` page added (admin-only). File attachments in chat (images for vision-capable local models; text/code files for all backends). Settings pages unified under `pg.css`. Added 2026-05-13: `task` cron type (full orchestrator loop on a schedule); monthly/yearly schedule formats (`monthly`, `monthly:DD:HH:MM`, `yearly:MM:DD:HH:MM`); Schedules web UI at `/settings/crons` (list, add, edit, pause, delete); HA inbound webhook tools toggle (orchestrator vs. direct LLM); Anthropic API key backend (`anthropic_api` model type via Anthropic SDK — alternative to CLI OAuth); Cloud APIs catalog in Model Registry — named provider picker (OpenRouter, OpenAI, Groq, X.ai/Grok, Together.ai, Fireworks.ai, Custom) with auto-filled URLs; hosts split into Cloud APIs / Local Hosts sections. Added 2026-05-15: Per-user custom roles — three required roles (`chat`, `orchestrator`, `distill`) are always present; users can add/remove custom roles (e.g. `coder`, `research`) via the Model Registry UI; existing `.env`-defined roles auto-migrated. Settings pages (`local_llm.html` + all settings pages) migrated to Tailwind CSS CDN (no build step); `preflight: false` preserves `pg.css` base styles; `input[type=checkbox/radio]` global width fix in `pg.css`; `btn-submit` now responsive (`w-full md:w-96`). **Active users / personas:** scott/inara, holly/tina, brian/wintermute diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index 0502797..471ca12 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -1,4 +1,4 @@ -# Cortex / Inara — Agent Task List +# Cortex — Agent Task List > Read this file before starting any work on this project. > **Status:** Active development — ongoing.