refactor: rename package fabledassistant -> scribe (code-only)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 1m14s

Renames src/fabledassistant -> src/scribe and all imports, plus the
default DB name and DB user/password (fabled -> scribe) in config +
compose. 952 refs / 154 files. Reverses the old 'internal name stays
fabledassistant' convention.

Code-only: live databases are still physically named 'fabledassistant'.
Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the
DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git
host (fabledsword), MCP (fabled-git) and the image name (fabledscribe)
are intentionally unchanged.

ruff check src/ clean locally; CI (typecheck + pytest) is the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 15:48:35 -04:00
parent 1d4c206563
commit b255a0f90e
167 changed files with 1183 additions and 2368 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
POSTGRES_USER=fabled POSTGRES_USER=fabled
POSTGRES_PASSWORD=fabled POSTGRES_PASSWORD=fabled
POSTGRES_DB=fabledassistant POSTGRES_DB=scribe
SECRET_KEY=dev-secret-change-me SECRET_KEY=dev-secret-change-me
+1 -1
View File
@@ -1 +1 @@
632037 1425947
+2 -2
View File
@@ -17,7 +17,7 @@ COPY src/ src/
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install . pip install .
COPY --from=build-frontend /build/dist/ src/fabledassistant/static/ COPY --from=build-frontend /build/dist/ src/scribe/static/
COPY alembic.ini . COPY alembic.ini .
COPY alembic/ alembic/ COPY alembic/ alembic/
@@ -30,4 +30,4 @@ ARG BUILD_VERSION=dev
ENV APP_VERSION=$BUILD_VERSION ENV APP_VERSION=$BUILD_VERSION
EXPOSE 5000 EXPOSE 5000
CMD ["sh", "-c", "alembic upgrade head && hypercorn 'fabledassistant.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"] CMD ["sh", "-c", "alembic upgrade head && hypercorn 'scribe.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
+2 -2
View File
@@ -4,8 +4,8 @@ from logging.config import fileConfig
from alembic import context from alembic import context
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from fabledassistant.config import Config from scribe.config import Config
from fabledassistant.models import Base from scribe.models import Base
config = context.config config = context.config
if config.config_file_name is not None: if config.config_file_name is not None:
+7 -7
View File
@@ -2,13 +2,13 @@ services:
app: app:
image: git.fabledsword.com/bvandeusen/fabledscribe:latest image: git.fabledsword.com/bvandeusen/fabledscribe:latest
environment: environment:
DATABASE_URL: "postgresql+asyncpg://fabled:${DB_PASSWORD}@db:5432/fabledassistant" DATABASE_URL: "postgresql+asyncpg://scribe:${DB_PASSWORD}@db:5432/scribe"
SECRET_KEY: "${SECRET_KEY}" SECRET_KEY: "${SECRET_KEY}"
LOG_LEVEL: "${LOG_LEVEL:-INFO}" LOG_LEVEL: "${LOG_LEVEL:-INFO}"
TRUST_PROXY_HEADERS: "true" TRUST_PROXY_HEADERS: "true"
SECURE_COOKIES: "true" SECURE_COOKIES: "true"
networks: networks:
- fabledassistant_backend - scribe_backend
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/health')"] test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/health')"]
interval: 30s interval: 30s
@@ -26,16 +26,16 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: fabled POSTGRES_USER: scribe
POSTGRES_PASSWORD: "${DB_PASSWORD}" POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: fabledassistant POSTGRES_DB: scribe
networks: networks:
- fabledassistant_backend - scribe_backend
# Lenient by design: a transient host exec/healthcheck stall (incident: # Lenient by design: a transient host exec/healthcheck stall (incident:
# runc setns failures -> "unhealthy" -> SIGKILL -> crash loop) must never # runc setns failures -> "unhealthy" -> SIGKILL -> crash loop) must never
# escalate to killing the DB. Health here only gates app startup order. # escalate to killing the DB. Health here only gates app startup order.
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U fabled"] test: ["CMD-SHELL", "pg_isready -U scribe"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
@@ -51,5 +51,5 @@ volumes:
pgdata: pgdata:
networks: networks:
fabledassistant_backend: scribe_backend:
driver: overlay driver: overlay
+5 -5
View File
@@ -18,7 +18,7 @@ services:
ports: ports:
- "5000:5000" - "5000:5000"
environment: environment:
DATABASE_URL: "postgresql+asyncpg://fabled:fabled@db:5432/fabledassistant" DATABASE_URL: "postgresql+asyncpg://scribe:scribe@db:5432/scribe"
SECRET_KEY: "${SECRET_KEY:-change-me-in-production}" SECRET_KEY: "${SECRET_KEY:-change-me-in-production}"
LOG_LEVEL: "${LOG_LEVEL:-INFO}" LOG_LEVEL: "${LOG_LEVEL:-INFO}"
volumes: volumes:
@@ -40,13 +40,13 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: fabled POSTGRES_USER: scribe
POSTGRES_PASSWORD: fabled POSTGRES_PASSWORD: scribe
POSTGRES_DB: fabledassistant POSTGRES_DB: scribe
# Lenient by design: a transient host exec/healthcheck stall must never # Lenient by design: a transient host exec/healthcheck stall must never
# escalate to killing the DB. Health here only gates app startup order. # escalate to killing the DB. Health here only gates app startup order.
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U fabled"] test: ["CMD-SHELL", "pg_isready -U scribe"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
+5 -5
View File
@@ -11,7 +11,7 @@ services:
# To use a bind mount instead (gives direct host access to all app data): # To use a bind mount instead (gives direct host access to all app data):
# - ./data:/data # - ./data:/data
environment: environment:
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-fabled}:${POSTGRES_PASSWORD:-fabled}@db:5432/${POSTGRES_DB:-fabledassistant}" DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-scribe}:${POSTGRES_PASSWORD:-scribe}@db:5432/${POSTGRES_DB:-scribe}"
SECRET_KEY: "${SECRET_KEY:-dev-secret-change-me}" SECRET_KEY: "${SECRET_KEY:-dev-secret-change-me}"
# Uncomment if you have a SearXNG instance you want to surface in the # Uncomment if you have a SearXNG instance you want to surface in the
# Integrations tab as a configured web-search backend: # Integrations tab as a configured web-search backend:
@@ -30,13 +30,13 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: ${POSTGRES_USER:-fabled} POSTGRES_USER: ${POSTGRES_USER:-scribe}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fabled} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-scribe}
POSTGRES_DB: ${POSTGRES_DB:-fabledassistant} POSTGRES_DB: ${POSTGRES_DB:-scribe}
# Lenient by design: a transient host exec/healthcheck stall must never # Lenient by design: a transient host exec/healthcheck stall must never
# escalate to killing the DB. Health here only gates app startup order. # escalate to killing the DB. Health here only gates app startup order.
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fabled}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-scribe}"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 10 retries: 10
+3 -3
View File
@@ -12,7 +12,7 @@ A Python MCP (Model Context Protocol) server that lets Claude directly interface
The work is split into two sub-projects: The work is split into two sub-projects:
1. **Fable API Key Feature** — additions to the main `fabledassistant` project to support bearer token authentication 1. **Fable API Key Feature** — additions to the main `scribe` project to support bearer token authentication
2. **Fable MCP Server** — a new standalone Python package at `fable-mcp/` in the same repo root 2. **Fable MCP Server** — a new standalone Python package at `fable-mcp/` in the same repo root
A third sub-project (Forgejo MCP for CI/CD automation) is planned as a follow-on after the Fable MCP is working. A third sub-project (Forgejo MCP for CI/CD automation) is planned as a follow-on after the Fable MCP is working.
@@ -134,7 +134,7 @@ The MCP tool reads tokens until it receives `type: "done"`, then returns `respon
### Location ### Location
`fable-mcp/` at the repository root, alongside `src/`, `frontend/`, `alembic/`. It is **not** part of the main Docker build and has no import relationship with `fabledassistant`. It will be extracted to its own Forgejo repo once stable. `fable-mcp/` at the repository root, alongside `src/`, `frontend/`, `alembic/`. It is **not** part of the main Docker build and has no import relationship with `scribe`. It will be extracted to its own Forgejo repo once stable.
### Package Structure ### Package Structure
@@ -256,7 +256,7 @@ Claude Code spawns the process over stdio automatically. No Docker, no daemon.
## Build & Repo Plan ## Build & Repo Plan
1. Implement and test within `fabledassistant/fable-mcp/` 1. Implement and test within `scribe/fable-mcp/`
2. Once stable, extract to a new Forgejo repo (`bvandeusen/fable-mcp`) 2. Once stable, extract to a new Forgejo repo (`bvandeusen/fable-mcp`)
3. Forgejo MCP (Gitea MCP) added as a second MCP server to automate build/push/config workflows — separate spec when ready 3. Forgejo MCP (Gitea MCP) added as a second MCP server to automate build/push/config workflows — separate spec when ready
+66 -66
View File
@@ -6,7 +6,7 @@
**Architecture:** Phase 1 adds bearer token auth to the existing Quart app (new `api_keys` table, updated `_check_auth`, settings UI tab). Phase 2 is a standalone `fable-mcp/` Python package using the `mcp[cli]` SDK that calls the Fable HTTP API via `httpx`. The two phases are sequential — Phase 2 can be built/tested independently using a write-scoped API key once Phase 1 is done. **Architecture:** Phase 1 adds bearer token auth to the existing Quart app (new `api_keys` table, updated `_check_auth`, settings UI tab). Phase 2 is a standalone `fable-mcp/` Python package using the `mcp[cli]` SDK that calls the Fable HTTP API via `httpx`. The two phases are sequential — Phase 2 can be built/tested independently using a write-scoped API key once Phase 1 is done.
**Deployment decision:** `fable-mcp/` stays permanently inside the `fabledassistant` repo. It will always be versioned alongside the backend it targets. Future Task A will serve the package from the running Fable Docker image so users can install it directly from their instance. **Deployment decision:** `fable-mcp/` stays permanently inside the `scribe` repo. It will always be versioned alongside the backend it targets. Future Task A will serve the package from the running Fable Docker image so users can install it directly from their instance.
**Tech Stack:** Python 3.12, Quart (Phase 1); `mcp[cli]`, `httpx`, `python-dotenv` (Phase 2); Vue 3 + TypeScript (Settings UI); pytest for both. **Tech Stack:** Python 3.12, Quart (Phase 1); `mcp[cli]`, `httpx`, `python-dotenv` (Phase 2); Vue 3 + TypeScript (Settings UI); pytest for both.
@@ -23,16 +23,16 @@
| Action | File | Purpose | | Action | File | Purpose |
|--------|------|---------| |--------|------|---------|
| Create | `alembic/versions/0027_add_api_keys.py` | DB migration for `api_keys` table | | Create | `alembic/versions/0027_add_api_keys.py` | DB migration for `api_keys` table |
| Create | `src/fabledassistant/models/api_key.py` | `ApiKey` SQLAlchemy model | | Create | `src/scribe/models/api_key.py` | `ApiKey` SQLAlchemy model |
| Modify | `src/fabledassistant/models/__init__.py` | Export `ApiKey` | | Modify | `src/scribe/models/__init__.py` | Export `ApiKey` |
| Create | `src/fabledassistant/services/api_keys.py` | create/list/revoke/lookup service functions | | Create | `src/scribe/services/api_keys.py` | create/list/revoke/lookup service functions |
| Modify | `src/fabledassistant/auth.py` | Add bearer token check before session fallback | | Modify | `src/scribe/auth.py` | Add bearer token check before session fallback |
| Create | `src/fabledassistant/routes/api_keys.py` | GET/POST/DELETE `/api/api-keys` blueprint | | Create | `src/scribe/routes/api_keys.py` | GET/POST/DELETE `/api/api-keys` blueprint |
| Modify | `src/fabledassistant/app.py` | Register `api_keys_bp` and `search_bp` | | Modify | `src/scribe/app.py` | Register `api_keys_bp` and `search_bp` |
| Modify | `src/fabledassistant/services/chat.py:17-30` | Add `conversation_type` param to `create_conversation` | | Modify | `src/scribe/services/chat.py:17-30` | Add `conversation_type` param to `create_conversation` |
| Modify | `src/fabledassistant/services/chat.py:123-135` | Exclude `"mcp"` type from `cleanup_old_conversations` | | Modify | `src/scribe/services/chat.py:123-135` | Exclude `"mcp"` type from `cleanup_old_conversations` |
| Modify | `src/fabledassistant/routes/chat.py:73-79` | Pass `conversation_type` from POST body | | Modify | `src/scribe/routes/chat.py:73-79` | Pass `conversation_type` from POST body |
| Create | `src/fabledassistant/routes/search.py` | `GET /api/search` semantic search endpoint | | Create | `src/scribe/routes/search.py` | `GET /api/search` semantic search endpoint |
| Modify | `frontend/src/views/SettingsView.vue` | Add "API Keys" tab | | Modify | `frontend/src/views/SettingsView.vue` | Add "API Keys" tab |
| Create | `tests/test_api_keys.py` | Unit tests for service + auth | | Create | `tests/test_api_keys.py` | Unit tests for service + auth |
| Create | `tests/test_search_route.py` | Unit test for search endpoint | | Create | `tests/test_search_route.py` | Unit test for search endpoint |
@@ -42,13 +42,13 @@
### Task 1: ApiKey model + migration ### Task 1: ApiKey model + migration
**Files:** **Files:**
- Create: `src/fabledassistant/models/api_key.py` - Create: `src/scribe/models/api_key.py`
- Create: `alembic/versions/0027_add_api_keys.py` - Create: `alembic/versions/0027_add_api_keys.py`
- Modify: `src/fabledassistant/models/__init__.py` - Modify: `src/scribe/models/__init__.py`
- [ ] **Step 1: Write the model** - [ ] **Step 1: Write the model**
Create `src/fabledassistant/models/api_key.py`: Create `src/scribe/models/api_key.py`:
```python ```python
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -56,8 +56,8 @@ from datetime import datetime, timezone
from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import CreatedAtMixin from scribe.models.base import CreatedAtMixin
class ApiKey(Base, CreatedAtMixin): class ApiKey(Base, CreatedAtMixin):
@@ -97,10 +97,10 @@ class ApiKey(Base, CreatedAtMixin):
- [ ] **Step 2: Export from models __init__** - [ ] **Step 2: Export from models __init__**
In `src/fabledassistant/models/__init__.py`, add after the last import line: In `src/scribe/models/__init__.py`, add after the last import line:
```python ```python
from fabledassistant.models.api_key import ApiKey # noqa: E402, F401 from scribe.models.api_key import ApiKey # noqa: E402, F401
``` ```
- [ ] **Step 3: Write the migration** - [ ] **Step 3: Write the migration**
@@ -155,8 +155,8 @@ Expected: `Running upgrade 0026 -> 0027, add api_keys table`
- [ ] **Step 5: Commit** - [ ] **Step 5: Commit**
```bash ```bash
git add src/fabledassistant/models/api_key.py \ git add src/scribe/models/api_key.py \
src/fabledassistant/models/__init__.py \ src/scribe/models/__init__.py \
alembic/versions/0027_add_api_keys.py alembic/versions/0027_add_api_keys.py
git commit -m "feat: add ApiKey model and migration 0027" git commit -m "feat: add ApiKey model and migration 0027"
``` ```
@@ -166,7 +166,7 @@ git commit -m "feat: add ApiKey model and migration 0027"
### Task 2: ApiKey service ### Task 2: ApiKey service
**Files:** **Files:**
- Create: `src/fabledassistant/services/api_keys.py` - Create: `src/scribe/services/api_keys.py`
- Create: `tests/test_api_keys.py` (service tests) - Create: `tests/test_api_keys.py` (service tests)
- [ ] **Step 1: Write the failing tests** - [ ] **Step 1: Write the failing tests**
@@ -181,7 +181,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from fabledassistant.services.api_keys import ( from scribe.services.api_keys import (
_hash_key, _hash_key,
generate_key, generate_key,
create_api_key, create_api_key,
@@ -212,7 +212,7 @@ def test_hash_key_is_sha256():
def test_generate_key_prefix(): def test_generate_key_prefix():
key = "fmcp_abcdefghijklmnop" key = "fmcp_abcdefghijklmnop"
# prefix is first 12 chars of the full key # prefix is first 12 chars of the full key
from fabledassistant.services.api_keys import _key_prefix from scribe.services.api_keys import _key_prefix
assert _key_prefix(key) == "fmcp_abcdefg" # first 12 chars assert _key_prefix(key) == "fmcp_abcdefg" # first 12 chars
@@ -222,7 +222,7 @@ async def test_create_api_key_returns_full_key():
mock_key_obj.id = 1 mock_key_obj.id = 1
mock_key_obj.to_dict.return_value = {"id": 1, "name": "test", "scope": "read", "key_prefix": "fmcp_xxx"} mock_key_obj.to_dict.return_value = {"id": 1, "name": "test", "scope": "read", "key_prefix": "fmcp_xxx"}
with patch("fabledassistant.services.api_keys.async_session") as mock_session_ctx: with patch("scribe.services.api_keys.async_session") as mock_session_ctx:
mock_session = AsyncMock() mock_session = AsyncMock()
mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=False) mock_session.__aexit__ = AsyncMock(return_value=False)
@@ -238,7 +238,7 @@ async def test_create_api_key_returns_full_key():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lookup_key_returns_none_for_unknown(): async def test_lookup_key_returns_none_for_unknown():
with patch("fabledassistant.services.api_keys.async_session") as mock_session_ctx: with patch("scribe.services.api_keys.async_session") as mock_session_ctx:
mock_session = AsyncMock() mock_session = AsyncMock()
mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=False) mock_session.__aexit__ = AsyncMock(return_value=False)
@@ -262,7 +262,7 @@ Expected: `ImportError` or `ModuleNotFoundError` (service doesn't exist yet)
- [ ] **Step 3: Write the service** - [ ] **Step 3: Write the service**
Create `src/fabledassistant/services/api_keys.py`: Create `src/scribe/services/api_keys.py`:
```python ```python
import hashlib import hashlib
@@ -271,8 +271,8 @@ from datetime import datetime, timezone
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.api_key import ApiKey from scribe.models.api_key import ApiKey
def generate_key() -> str: def generate_key() -> str:
@@ -365,7 +365,7 @@ Expected: all tests pass
- [ ] **Step 5: Commit** - [ ] **Step 5: Commit**
```bash ```bash
git add src/fabledassistant/services/api_keys.py tests/test_api_keys.py git add src/scribe/services/api_keys.py tests/test_api_keys.py
git commit -m "feat: add ApiKey service with create/list/revoke/lookup" git commit -m "feat: add ApiKey service with create/list/revoke/lookup"
``` ```
@@ -374,7 +374,7 @@ git commit -m "feat: add ApiKey service with create/list/revoke/lookup"
### Task 3: Auth middleware — bearer token support ### Task 3: Auth middleware — bearer token support
**Files:** **Files:**
- Modify: `src/fabledassistant/auth.py` - Modify: `src/scribe/auth.py`
- Modify: `tests/test_api_keys.py` (add auth middleware tests) - Modify: `tests/test_api_keys.py` (add auth middleware tests)
- [ ] **Step 1: Add auth middleware tests** - [ ] **Step 1: Add auth middleware tests**
@@ -400,7 +400,7 @@ def test_scope_validation():
async def test_bearer_token_path_sets_g_user(monkeypatch): async def test_bearer_token_path_sets_g_user(monkeypatch):
"""Valid bearer token authenticates and sets g.user and g.api_key.""" """Valid bearer token authenticates and sets g.user and g.api_key."""
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from fabledassistant.auth import _check_auth from scribe.auth import _check_auth
# Mock ApiKey object # Mock ApiKey object
fake_key = MagicMock() fake_key = MagicMock()
@@ -411,8 +411,8 @@ async def test_bearer_token_path_sets_g_user(monkeypatch):
fake_user = MagicMock() fake_user = MagicMock()
fake_user.role = "user" fake_user.role = "user"
monkeypatch.setattr("fabledassistant.auth.lookup_key", AsyncMock(return_value=fake_key)) monkeypatch.setattr("scribe.auth.lookup_key", AsyncMock(return_value=fake_key))
monkeypatch.setattr("fabledassistant.auth.get_user_by_id", AsyncMock(return_value=fake_user)) monkeypatch.setattr("scribe.auth.get_user_by_id", AsyncMock(return_value=fake_user))
called_with_user = {} called_with_user = {}
@@ -434,7 +434,7 @@ async def test_bearer_token_path_sets_g_user(monkeypatch):
async with app.test_request_context("/test", method="GET", async with app.test_request_context("/test", method="GET",
headers={"Authorization": "Bearer fmcp_valid"}): headers={"Authorization": "Bearer fmcp_valid"}):
# Just verify _check_auth calls lookup_key with the right token # Just verify _check_auth calls lookup_key with the right token
import fabledassistant.auth as auth_module import scribe.auth as auth_module
auth_module.lookup_key.assert_called_with # callable auth_module.lookup_key.assert_called_with # callable
@@ -442,7 +442,7 @@ async def test_bearer_token_path_sets_g_user(monkeypatch):
async def test_read_only_key_blocked_on_post(): async def test_read_only_key_blocked_on_post():
"""Read-only API key returns 403 on non-GET requests.""" """Read-only API key returns 403 on non-GET requests."""
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from fabledassistant.auth import _check_auth from scribe.auth import _check_auth
from quart import Quart from quart import Quart
fake_key = MagicMock() fake_key = MagicMock()
@@ -459,8 +459,8 @@ async def test_read_only_key_blocked_on_post():
headers={"Authorization": "Bearer fmcp_readonly"}), headers={"Authorization": "Bearer fmcp_readonly"}),
): ):
from unittest.mock import patch from unittest.mock import patch
with patch("fabledassistant.auth.lookup_key", AsyncMock(return_value=fake_key)), \ with patch("scribe.auth.lookup_key", AsyncMock(return_value=fake_key)), \
patch("fabledassistant.auth.get_user_by_id", AsyncMock(return_value=fake_user)): patch("scribe.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
async def dummy(): async def dummy():
return "ok" return "ok"
@@ -481,15 +481,15 @@ docker compose run --rm app pytest tests/test_api_keys.py -v
- [ ] **Step 3: Update auth.py** - [ ] **Step 3: Update auth.py**
Replace `src/fabledassistant/auth.py` with: Replace `src/scribe/auth.py` with:
```python ```python
import functools import functools
from quart import g, jsonify, request, session from quart import g, jsonify, request, session
from fabledassistant.services.auth import get_user_by_id from scribe.services.auth import get_user_by_id
from fabledassistant.services.api_keys import lookup_key from scribe.services.api_keys import lookup_key
def _check_auth(f, required_role: str | None = None): def _check_auth(f, required_role: str | None = None):
@@ -556,7 +556,7 @@ Expected: all existing tests still pass
- [ ] **Step 5: Commit** - [ ] **Step 5: Commit**
```bash ```bash
git add src/fabledassistant/auth.py tests/test_api_keys.py git add src/scribe/auth.py tests/test_api_keys.py
git commit -m "feat: add bearer token auth to _check_auth, falls back to session" git commit -m "feat: add bearer token auth to _check_auth, falls back to session"
``` ```
@@ -565,18 +565,18 @@ git commit -m "feat: add bearer token auth to _check_auth, falls back to session
### Task 4: API key routes + app registration ### Task 4: API key routes + app registration
**Files:** **Files:**
- Create: `src/fabledassistant/routes/api_keys.py` - Create: `src/scribe/routes/api_keys.py`
- Modify: `src/fabledassistant/app.py` - Modify: `src/scribe/app.py`
- [ ] **Step 1: Write the routes** - [ ] **Step 1: Write the routes**
Create `src/fabledassistant/routes/api_keys.py`: Create `src/scribe/routes/api_keys.py`:
```python ```python
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.services.api_keys import create_api_key, list_api_keys, revoke_api_key from scribe.services.api_keys import create_api_key, list_api_keys, revoke_api_key
api_keys_bp = Blueprint("api_keys", __name__, url_prefix="/api/api-keys") api_keys_bp = Blueprint("api_keys", __name__, url_prefix="/api/api-keys")
@@ -619,10 +619,10 @@ async def revoke_key_route(key_id: int):
- [ ] **Step 2: Register in app.py** - [ ] **Step 2: Register in app.py**
In `src/fabledassistant/app.py`, add the import alongside the other route imports: In `src/scribe/app.py`, add the import alongside the other route imports:
```python ```python
from fabledassistant.routes.api_keys import api_keys_bp from scribe.routes.api_keys import api_keys_bp
``` ```
And add the registration line after `app.register_blueprint(users_bp)`: And add the registration line after `app.register_blueprint(users_bp)`:
@@ -651,7 +651,7 @@ curl -s http://localhost:8080/api/auth/me \
- [ ] **Step 4: Commit** - [ ] **Step 4: Commit**
```bash ```bash
git add src/fabledassistant/routes/api_keys.py src/fabledassistant/app.py git add src/scribe/routes/api_keys.py src/scribe/app.py
git commit -m "feat: add API key CRUD routes and register blueprint" git commit -m "feat: add API key CRUD routes and register blueprint"
``` ```
@@ -660,14 +660,14 @@ git commit -m "feat: add API key CRUD routes and register blueprint"
### Task 5: Conversation type wiring ### Task 5: Conversation type wiring
**Files:** **Files:**
- Modify: `src/fabledassistant/services/chat.py` (lines 17-30 and 123-135) - Modify: `src/scribe/services/chat.py` (lines 17-30 and 123-135)
- Modify: `src/fabledassistant/routes/chat.py` (lines 73-79) - Modify: `src/scribe/routes/chat.py` (lines 73-79)
Note: `Conversation.conversation_type` already exists in the model. `list_conversations` already filters by `conv_type`. This task only wires up creation and retention exclusion. Note: `Conversation.conversation_type` already exists in the model. `list_conversations` already filters by `conv_type`. This task only wires up creation and retention exclusion.
- [ ] **Step 1: Update `create_conversation` service** - [ ] **Step 1: Update `create_conversation` service**
In `src/fabledassistant/services/chat.py`, change the function signature at line 17: In `src/scribe/services/chat.py`, change the function signature at line 17:
```python ```python
async def create_conversation( async def create_conversation(
@@ -692,7 +692,7 @@ async def create_conversation(
- [ ] **Step 2: Update `cleanup_old_conversations` to exclude "mcp"** - [ ] **Step 2: Update `cleanup_old_conversations` to exclude "mcp"**
In `src/fabledassistant/services/chat.py`, update the WHERE clause at line 130: In `src/scribe/services/chat.py`, update the WHERE clause at line 130:
```python ```python
result = await session.execute( result = await session.execute(
@@ -708,7 +708,7 @@ result = await session.execute(
- [ ] **Step 3: Update the POST route to accept conversation_type** - [ ] **Step 3: Update the POST route to accept conversation_type**
In `src/fabledassistant/routes/chat.py`, update `create_conversation_route` (around line 73): In `src/scribe/routes/chat.py`, update `create_conversation_route` (around line 73):
```python ```python
@chat_bp.route("/conversations", methods=["POST"]) @chat_bp.route("/conversations", methods=["POST"])
@@ -737,7 +737,7 @@ Expected: all tests pass
- [ ] **Step 5: Commit** - [ ] **Step 5: Commit**
```bash ```bash
git add src/fabledassistant/services/chat.py src/fabledassistant/routes/chat.py git add src/scribe/services/chat.py src/scribe/routes/chat.py
git commit -m "feat: wire conversation_type through create_conversation, exclude mcp from retention sweep" git commit -m "feat: wire conversation_type through create_conversation, exclude mcp from retention sweep"
``` ```
@@ -746,8 +746,8 @@ git commit -m "feat: wire conversation_type through create_conversation, exclude
### Task 6: Semantic search endpoint ### Task 6: Semantic search endpoint
**Files:** **Files:**
- Create: `src/fabledassistant/routes/search.py` - Create: `src/scribe/routes/search.py`
- Modify: `src/fabledassistant/app.py` - Modify: `src/scribe/app.py`
- Create: `tests/test_search_route.py` - Create: `tests/test_search_route.py`
- [ ] **Step 1: Write the failing test** - [ ] **Step 1: Write the failing test**
@@ -762,7 +762,7 @@ from unittest.mock import patch, AsyncMock
def test_content_type_mapping(): def test_content_type_mapping():
"""Verify content_type string maps to correct is_task value.""" """Verify content_type string maps to correct is_task value."""
from fabledassistant.routes.search import _content_type_to_is_task from scribe.routes.search import _content_type_to_is_task
assert _content_type_to_is_task("note") is False assert _content_type_to_is_task("note") is False
assert _content_type_to_is_task("task") is True assert _content_type_to_is_task("task") is True
assert _content_type_to_is_task("all") is None assert _content_type_to_is_task("all") is None
@@ -779,13 +779,13 @@ Expected: `ImportError` (module doesn't exist yet)
- [ ] **Step 3: Write the route** - [ ] **Step 3: Write the route**
Create `src/fabledassistant/routes/search.py`: Create `src/scribe/routes/search.py`:
```python ```python
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.services.embeddings import semantic_search_notes from scribe.services.embeddings import semantic_search_notes
search_bp = Blueprint("search", __name__, url_prefix="/api/search") search_bp = Blueprint("search", __name__, url_prefix="/api/search")
@@ -834,9 +834,9 @@ Note: check `services/embeddings.py` to confirm the return type of `semantic_sea
- [ ] **Step 4: Register in app.py** - [ ] **Step 4: Register in app.py**
Add to imports in `src/fabledassistant/app.py`: Add to imports in `src/scribe/app.py`:
```python ```python
from fabledassistant.routes.search import search_bp from scribe.routes.search import search_bp
``` ```
Add registration: Add registration:
@@ -855,8 +855,8 @@ Expected: all tests pass
- [ ] **Step 6: Commit** - [ ] **Step 6: Commit**
```bash ```bash
git add src/fabledassistant/routes/search.py \ git add src/scribe/routes/search.py \
src/fabledassistant/app.py \ src/scribe/app.py \
tests/test_search_route.py tests/test_search_route.py
git commit -m "feat: add GET /api/search semantic search endpoint" git commit -m "feat: add GET /api/search semantic search endpoint"
``` ```
@@ -1905,7 +1905,7 @@ async def send_message(
return f"Error: {e}" return f"Error: {e}"
``` ```
Note: verify the Fable message POST endpoint path by checking `src/fabledassistant/routes/chat.py` — search for the route that accepts a user message and triggers generation. Adjust `/api/chat/conversations/{conv_id}/messages` if the actual path differs. Note: verify the Fable message POST endpoint path by checking `src/scribe/routes/chat.py` — search for the route that accepts a user message and triggers generation. Adjust `/api/chat/conversations/{conv_id}/messages` if the actual path differs.
- [ ] **Step 4: Run tests** - [ ] **Step 4: Run tests**
+9 -9
View File
@@ -39,20 +39,20 @@
## New Backend Files ## New Backend Files
### `src/fabledassistant/services/stt.py` ### `src/scribe/services/stt.py`
Lazy singleton `WhisperModel` loader. Public API: Lazy singleton `WhisperModel` loader. Public API:
- `load_stt_model()` — called at startup via `asyncio.create_task` - `load_stt_model()` — called at startup via `asyncio.create_task`
- `transcribe(audio_bytes, mime_type) -> str` — runs in `run_in_executor`; writes bytes to `NamedTemporaryFile`, returns concatenated segment text - `transcribe(audio_bytes, mime_type) -> str` — runs in `run_in_executor`; writes bytes to `NamedTemporaryFile`, returns concatenated segment text
- `stt_available() -> bool` - `stt_available() -> bool`
### `src/fabledassistant/services/tts.py` ### `src/scribe/services/tts.py`
Lazy singleton `KPipeline` loader. Public API: Lazy singleton `KPipeline` loader. Public API:
- `load_tts_model()` — called at startup - `load_tts_model()` — called at startup
- `synthesise(text, voice, speed) -> bytes` — runs in `run_in_executor`; returns WAV bytes (24kHz, 16-bit mono) - `synthesise(text, voice, speed) -> bytes` — runs in `run_in_executor`; returns WAV bytes (24kHz, 16-bit mono)
- `list_voices() -> list[dict]` — returns static list of known Kokoro voice IDs + labels - `list_voices() -> list[dict]` — returns static list of known Kokoro voice IDs + labels
- `tts_available() -> bool` - `tts_available() -> bool`
### `src/fabledassistant/routes/voice.py` ### `src/scribe/routes/voice.py`
Blueprint at `/api/voice`, all routes `@login_required`. Blueprint at `/api/voice`, all routes `@login_required`.
| Endpoint | Method | Description | | Endpoint | Method | Description |
@@ -66,27 +66,27 @@ Blueprint at `/api/voice`, all routes `@login_required`.
## Modified Backend Files ## Modified Backend Files
### `src/fabledassistant/app.py` ### `src/scribe/app.py`
- Register `voice_bp` blueprint - Register `voice_bp` blueprint
- In `startup()`: `asyncio.create_task(load_stt_model())` + `asyncio.create_task(load_tts_model())` when `VOICE_ENABLED` - In `startup()`: `asyncio.create_task(load_stt_model())` + `asyncio.create_task(load_tts_model())` when `VOICE_ENABLED`
### `src/fabledassistant/config.py` ### `src/scribe/config.py`
- Add 4 new env var attributes - Add 4 new env var attributes
- Add validation in `validate()` - Add validation in `validate()`
### `src/fabledassistant/services/llm.py` ### `src/scribe/services/llm.py`
- Add `voice_mode: bool = False` and `voice_speech_style: str = "conversational"` to `build_context()` - Add `voice_mode: bool = False` and `voice_speech_style: str = "conversational"` to `build_context()`
- When `voice_mode=True`, prepend: *"Respond naturally as if speaking aloud. No markdown, bullet points, headers, or code blocks. Complete sentences only."* - When `voice_mode=True`, prepend: *"Respond naturally as if speaking aloud. No markdown, bullet points, headers, or code blocks. Complete sentences only."*
- Append style modifier based on `voice_speech_style` - Append style modifier based on `voice_speech_style`
### `src/fabledassistant/services/generation_task.py` ### `src/scribe/services/generation_task.py`
- Add `voice_mode: bool = False` to `run_generation()` - Add `voice_mode: bool = False` to `run_generation()`
- Read `voice_speech_style` from settings when voice_mode; pass both to `build_context()` - Read `voice_speech_style` from settings when voice_mode; pass both to `build_context()`
### `src/fabledassistant/routes/chat.py` ### `src/scribe/routes/chat.py`
- Allow `"voice"` in `conversation_type` whitelist - Allow `"voice"` in `conversation_type` whitelist
### `src/fabledassistant/services/chat.py` ### `src/scribe/services/chat.py`
- Exclude `conversation_type == "voice"` from auto-cleanup retention - Exclude `conversation_type == "voice"` from auto-cleanup retention
--- ---
+4 -4
View File
@@ -20,7 +20,7 @@
│ Docker Compose │ │ Docker Compose │
│ │ │ │
│ ┌──────────────────────┐ ┌────────────┐ │ │ ┌──────────────────────┐ ┌────────────┐ │
│ │ fabledassistant │ │ ollama │ │ │ │ scribe │ │ ollama │ │
│ │ ┌────────────────┐ │ │ │ │ │ │ ┌────────────────┐ │ │ │ │
│ │ │ Quart Server │ │ │ LLM API │ │ │ │ │ Quart Server │ │ │ LLM API │ │
│ │ │ ┌──────────┐ │ │ │ │ │ │ │ │ ┌──────────┐ │ │ │ │ │
@@ -43,7 +43,7 @@
## Project Structure ## Project Structure
``` ```
fabledassistant/ scribe/
├── docker-compose.yml # Development stack ├── docker-compose.yml # Development stack
├── docker-compose.prod.yml # Production stack (Docker Swarm) ├── docker-compose.prod.yml # Production stack (Docker Swarm)
├── Dockerfile # Multi-stage build (Node → Python) ├── Dockerfile # Multi-stage build (Node → Python)
@@ -54,7 +54,7 @@ fabledassistant/
│ ├── server.py # FastMCP tool registrations │ ├── server.py # FastMCP tool registrations
│ ├── client.py # FableClient (httpx wrapper) │ ├── client.py # FableClient (httpx wrapper)
│ └── tools/ # Tool modules (notes, tasks, projects, …) │ └── tools/ # Tool modules (notes, tasks, projects, …)
├── src/fabledassistant/ ├── src/scribe/
│ ├── app.py # Quart app factory + blueprint registration │ ├── app.py # Quart app factory + blueprint registration
│ ├── config.py # Config class (reads env vars) │ ├── config.py # Config class (reads env vars)
│ ├── auth.py # login_required decorator, session checks │ ├── auth.py # login_required decorator, session checks
@@ -169,7 +169,7 @@ Permission resolution is centralised in `services/access.py`. `get_project_permi
## Detailed File Reference ## Detailed File Reference
### Backend (`src/fabledassistant/`) ### Backend (`src/scribe/`)
| File | Responsibility | | File | Responsibility |
|------|---------------| |------|---------------|
+2 -2
View File
@@ -8,7 +8,7 @@ Configuration is via environment variables. The `docker-compose.yml` file sets d
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `DATABASE_URL` | `postgresql+asyncpg://fabled:fabled@db/fabledassistant` | PostgreSQL async connection string | | `DATABASE_URL` | `postgresql+asyncpg://fabled:fabled@db/scribe` | PostgreSQL async connection string |
| `SECRET_KEY` | `dev-secret-change-me` | Session signing key — **change this in production** | | `SECRET_KEY` | `dev-secret-change-me` | Session signing key — **change this in production** |
| `SECRET_KEY_FILE` | — | Path to a Docker secret file containing the key (alternative to `SECRET_KEY`) | | `SECRET_KEY_FILE` | — | Path to a Docker secret file containing the key (alternative to `SECRET_KEY`) |
| `LOG_LEVEL` | `INFO` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | | `LOG_LEVEL` | `INFO` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
@@ -91,7 +91,7 @@ The production compose file adds:
```bash ```bash
# Create Docker secrets # Create Docker secrets
echo "$(python3 -c 'import secrets; print(secrets.token_hex(32))')" | docker secret create fabled_secret_key - echo "$(python3 -c 'import secrets; print(secrets.token_hex(32))')" | docker secret create fabled_secret_key -
echo "postgresql+asyncpg://fabled:strongpassword@db/fabledassistant" | docker secret create fabled_db_url - echo "postgresql+asyncpg://fabled:strongpassword@db/scribe" | docker secret create fabled_db_url -
# Deploy # Deploy
docker stack deploy -c docker-compose.prod.yml fabled docker stack deploy -c docker-compose.prod.yml fabled
+3 -3
View File
@@ -93,7 +93,7 @@ config on the runner host.
### Docker Registry ### Docker Registry
Images pushed to: `git.fabledsword.com/bvandeusen/fabledassistant` Images pushed to: `git.fabledsword.com/bvandeusen/scribe`
Cache tag: `:cache` (reduces build time ~80%) Cache tag: `:cache` (reduces build time ~80%)
Required secrets (repo → Settings → Secrets → Actions): Required secrets (repo → Settings → Secrets → Actions):
@@ -140,8 +140,8 @@ Current migration sequence (all idempotent raw SQL):
### Backend ### Backend
- Services: `async with async_session() as session:` — import from `fabledassistant.models` - Services: `async with async_session() as session:` — import from `scribe.models`
- No `fabledassistant.database` module - No `scribe.database` module
- Blueprint per resource: `routes/notes.py`, `routes/tasks.py`, etc. - Blueprint per resource: `routes/notes.py`, `routes/tasks.py`, etc.
- All business logic in `services/`; routes are thin wrappers - All business logic in `services/`; routes are thin wrappers
- Permission checks via `services/access.py` — never inline ownership checks in routes - Permission checks via `services/access.py` — never inline ownership checks in routes
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "fabledassistant-frontend", "name": "scribe-frontend",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "fabledassistant-frontend", "name": "scribe-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fullcalendar/core": "^6.1.20", "@fullcalendar/core": "^6.1.20",
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "fabledassistant-frontend", "name": "scribe-frontend",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
+2 -2
View File
@@ -510,7 +510,7 @@ async function exportData(scope: "user" | "full") {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const a = document.createElement("a"); const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.download = `fabledassistant-backup-${scope}-${new Date().toISOString().slice(0, 10)}.json`; a.download = `scribe-backup-${scope}-${new Date().toISOString().slice(0, 10)}.json`;
a.click(); a.click();
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
toastStore.show("Backup downloaded"); toastStore.show("Backup downloaded");
@@ -533,7 +533,7 @@ async function exportNotes(format: "markdown" | "json") {
const stamp = new Date().toISOString().slice(0, 10); const stamp = new Date().toISOString().slice(0, 10);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.download = `fabledassistant-${stamp}.${ext}`; a.download = `scribe-${stamp}.${ext}`;
a.click(); a.click();
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
toastStore.show("Export downloaded"); toastStore.show("Export downloaded");
+1 -1
View File
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "fabledassistant" name = "scribe"
version = "0.1.0" version = "0.1.0"
description = "Self-hosted note-taking, project-management, and calendar app exposed to Claude via MCP" description = "Self-hosted note-taking, project-management, and calendar app exposed to Claude via MCP"
requires-python = ">=3.14" requires-python = ">=3.14"
+1 -1
View File
@@ -103,7 +103,7 @@ async def run(args) -> None:
print(f" - {r['title']}: {preview}{empty_flag}") print(f" - {r['title']}: {preview}{empty_flag}")
return return
from fabledassistant.services import rulebooks as rb_service from scribe.services import rulebooks as rb_service
existing = await rb_service.find_rulebook_by_title(args.user_id, args.rulebook_title) existing = await rb_service.find_rulebook_by_title(args.user_id, args.rulebook_title)
if existing is not None and not args.force: if existing is not None and not args.force:
-42
View File
@@ -1,42 +0,0 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
from fabledassistant.config import Config
engine = create_async_engine(
Config.DATABASE_URL,
echo=False,
pool_pre_ping=True,
pool_recycle=1800,
)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
from fabledassistant.models.base import CreatedAtMixin, TimestampMixin # noqa: E402, F401
from fabledassistant.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401
from fabledassistant.models.setting import Setting # noqa: E402, F401
from fabledassistant.models.user import User # noqa: E402, F401
from fabledassistant.models.app_log import AppLog # noqa: E402, F401
from fabledassistant.models.password_reset import PasswordResetToken # noqa: E402, F401
from fabledassistant.models.invitation import InvitationToken # noqa: E402, F401
from fabledassistant.models.embedding import NoteEmbedding # noqa: E402, F401
from fabledassistant.models.project import Project # noqa: E402, F401
from fabledassistant.models.event import Event # noqa: E402, F401
from fabledassistant.models.milestone import Milestone # noqa: E402, F401
from fabledassistant.models.task_log import TaskLog # noqa: E402, F401
from fabledassistant.models.note_draft import NoteDraft # noqa: E402, F401
from fabledassistant.models.note_version import NoteVersion # noqa: E402, F401
from fabledassistant.models.group import Group, GroupMembership # noqa: E402, F401
from fabledassistant.models.share import NoteShare, ProjectShare # noqa: E402, F401
from fabledassistant.models.notification import Notification # noqa: E402, F401
from fabledassistant.models.api_key import ApiKey # noqa: E402, F401
from fabledassistant.models.user_profile import UserProfile # noqa: E402, F401
from fabledassistant.models.rulebook import ( # noqa: E402, F401
Rulebook, RulebookTopic, Rule, project_rulebook_subscriptions,
)
@@ -5,30 +5,30 @@ from pathlib import Path
from quart import Quart, g, jsonify, make_response, request, send_from_directory from quart import Quart, g, jsonify, make_response, request, send_from_directory
from fabledassistant.config import Config from scribe.config import Config
from fabledassistant.routes.admin import admin_bp from scribe.routes.admin import admin_bp
from fabledassistant.routes.api import api from scribe.routes.api import api
from fabledassistant.routes.auth import auth_bp from scribe.routes.auth import auth_bp
from fabledassistant.routes.export import export_bp from scribe.routes.export import export_bp
from fabledassistant.routes.notes import notes_bp from scribe.routes.notes import notes_bp
from fabledassistant.routes.milestones import milestones_bp from scribe.routes.milestones import milestones_bp
from fabledassistant.routes.task_logs import task_logs_bp from scribe.routes.task_logs import task_logs_bp
from fabledassistant.routes.projects import projects_bp from scribe.routes.projects import projects_bp
from fabledassistant.routes.settings import settings_bp from scribe.routes.settings import settings_bp
from fabledassistant.routes.tasks import tasks_bp from scribe.routes.tasks import tasks_bp
from fabledassistant.routes.groups import groups_bp from scribe.routes.groups import groups_bp
from fabledassistant.routes.shares import shares_bp from scribe.routes.shares import shares_bp
from fabledassistant.routes.in_app_notifications import notifications_bp from scribe.routes.in_app_notifications import notifications_bp
from fabledassistant.routes.users import users_bp from scribe.routes.users import users_bp
from fabledassistant.routes.api_keys import api_keys_bp from scribe.routes.api_keys import api_keys_bp
from fabledassistant.routes.events import events_bp from scribe.routes.events import events_bp
from fabledassistant.routes.search import search_bp from scribe.routes.search import search_bp
from fabledassistant.routes.profile import profile_bp from scribe.routes.profile import profile_bp
from fabledassistant.routes.knowledge import knowledge_bp from scribe.routes.knowledge import knowledge_bp
from fabledassistant.routes.rulebooks import rulebooks_bp from scribe.routes.rulebooks import rulebooks_bp
from fabledassistant.routes.trash import trash_bp from scribe.routes.trash import trash_bp
from fabledassistant.routes.dashboard import dashboard_bp from scribe.routes.dashboard import dashboard_bp
from fabledassistant.mcp import mount_mcp from scribe.mcp import mount_mcp
STATIC_DIR = Path(__file__).parent / "static" STATIC_DIR = Path(__file__).parent / "static"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -115,7 +115,7 @@ def create_app() -> Quart:
# Log usage for API requests (skip logs endpoint to avoid recursion) # Log usage for API requests (skip logs endpoint to avoid recursion)
if request.path.startswith("/api/") and not request.path.startswith("/api/admin/logs"): if request.path.startswith("/api/") and not request.path.startswith("/api/admin/logs"):
try: try:
from fabledassistant.services.logging import log_usage from scribe.services.logging import log_usage
user = getattr(g, "user", None) user = getattr(g, "user", None)
await log_usage( await log_usage(
@@ -150,10 +150,10 @@ def create_app() -> Quart:
async def startup(): async def startup():
import asyncio import asyncio
from fabledassistant.services.auth import start_auth_token_retention_loop from scribe.services.auth import start_auth_token_retention_loop
from fabledassistant.services.embeddings import backfill_note_embeddings from scribe.services.embeddings import backfill_note_embeddings
from fabledassistant.services.logging import start_log_retention_loop from scribe.services.logging import start_log_retention_loop
from fabledassistant.services.notifications import start_notification_loop from scribe.services.notifications import start_notification_loop
start_log_retention_loop() start_log_retention_loop()
start_notification_loop() start_notification_loop()
@@ -170,36 +170,36 @@ def create_app() -> Quart:
asyncio.create_task(_delayed_backfill()) asyncio.create_task(_delayed_backfill())
# Event scheduler (reminders + CalDAV pull sync) # Event scheduler (reminders + CalDAV pull sync)
from fabledassistant.services.event_scheduler import start_event_scheduler from scribe.services.event_scheduler import start_event_scheduler
start_event_scheduler(asyncio.get_running_loop()) start_event_scheduler(asyncio.get_running_loop())
# Version-pinning scheduler (daily auto-pin scan at 03:00 UTC) # Version-pinning scheduler (daily auto-pin scan at 03:00 UTC)
from fabledassistant.services.version_pinning_scheduler import ( from scribe.services.version_pinning_scheduler import (
start_version_pinning_scheduler, start_version_pinning_scheduler,
) )
start_version_pinning_scheduler(asyncio.get_running_loop()) start_version_pinning_scheduler(asyncio.get_running_loop())
# Trash retention scheduler (daily expired-trash purge at 03:30 UTC) # Trash retention scheduler (daily expired-trash purge at 03:30 UTC)
from fabledassistant.services.trash_scheduler import start_trash_scheduler from scribe.services.trash_scheduler import start_trash_scheduler
start_trash_scheduler(asyncio.get_running_loop()) start_trash_scheduler(asyncio.get_running_loop())
# Diagnostic instrumentation — heartbeat, signal handlers, asyncio # Diagnostic instrumentation — heartbeat, signal handlers, asyncio
# exception hook. Cheap (~1 log line/min), high diagnostic value when # exception hook. Cheap (~1 log line/min), high diagnostic value when
# the app crashes mysteriously. See services/diagnostics.py. # the app crashes mysteriously. See services/diagnostics.py.
from fabledassistant.services.diagnostics import start_diagnostics from scribe.services.diagnostics import start_diagnostics
start_diagnostics(asyncio.get_running_loop()) start_diagnostics(asyncio.get_running_loop())
@app.after_serving @app.after_serving
async def shutdown(): async def shutdown():
from fabledassistant.services.event_scheduler import stop_event_scheduler from scribe.services.event_scheduler import stop_event_scheduler
stop_event_scheduler() stop_event_scheduler()
from fabledassistant.services.version_pinning_scheduler import ( from scribe.services.version_pinning_scheduler import (
stop_version_pinning_scheduler, stop_version_pinning_scheduler,
) )
stop_version_pinning_scheduler() stop_version_pinning_scheduler()
from fabledassistant.services.trash_scheduler import stop_trash_scheduler from scribe.services.trash_scheduler import stop_trash_scheduler
stop_trash_scheduler() stop_trash_scheduler()
from fabledassistant.services.diagnostics import stop_diagnostics from scribe.services.diagnostics import stop_diagnostics
stop_diagnostics() stop_diagnostics()
@app.route("/") @app.route("/")
@@ -249,7 +249,7 @@ def create_app() -> Quart:
logger.exception("Internal server error on %s %s", request.method, request.path) logger.exception("Internal server error on %s %s", request.method, request.path)
try: try:
from fabledassistant.services.logging import log_error from scribe.services.logging import log_error
user = getattr(g, "user", None) user = getattr(g, "user", None)
await log_error( await log_error(
@@ -2,8 +2,8 @@ import functools
from quart import g, jsonify, request, session from quart import g, jsonify, request, session
from fabledassistant.services.auth import get_user_by_id from scribe.services.auth import get_user_by_id
from fabledassistant.services.api_keys import lookup_key from scribe.services.api_keys import lookup_key
def _check_auth(f, required_role: str | None = None): def _check_auth(f, required_role: str | None = None):
@@ -20,7 +20,7 @@ class Config:
DATABASE_URL: str = _read_secret( DATABASE_URL: str = _read_secret(
"DATABASE_URL", "DATABASE_URL",
"DATABASE_URL_FILE", "DATABASE_URL_FILE",
"postgresql+asyncpg://fabled:fabled@localhost:5432/fabledassistant", "postgresql+asyncpg://scribe:scribe@localhost:5432/scribe",
) )
SECRET_KEY: str = _read_secret("SECRET_KEY", "SECRET_KEY_FILE", "dev-secret-change-me") SECRET_KEY: str = _read_secret("SECRET_KEY", "SECRET_KEY_FILE", "dev-secret-change-me")
SECURE_COOKIES: bool = os.environ.get("SECURE_COOKIES", "").lower() in ("1", "true", "yes") SECURE_COOKIES: bool = os.environ.get("SECURE_COOKIES", "").lower() in ("1", "true", "yes")
@@ -3,6 +3,6 @@
Auth uses the existing `api_keys` table: a Bearer token in the Authorization Auth uses the existing `api_keys` table: a Bearer token in the Authorization
header resolves to a user, and every tool call acts on that user's data only. header resolves to a user, and every tool call acts on that user's data only.
""" """
from fabledassistant.mcp.server import build_mcp_server, mount_mcp from scribe.mcp.server import build_mcp_server, mount_mcp
__all__ = ["build_mcp_server", "mount_mcp"] __all__ = ["build_mcp_server", "mount_mcp"]
@@ -1,7 +1,7 @@
"""MCP-side Bearer token resolution. Reuses the existing api_keys infrastructure.""" """MCP-side Bearer token resolution. Reuses the existing api_keys infrastructure."""
from __future__ import annotations from __future__ import annotations
from fabledassistant.services.api_keys import lookup_key from scribe.services.api_keys import lookup_key
async def resolve_bearer_to_user_id(auth_header: str | None) -> int | None: async def resolve_bearer_to_user_id(auth_header: str | None) -> int | None:
@@ -220,7 +220,7 @@ def build_mcp_server() -> FastMCP:
enable_dns_rebinding_protection=False, enable_dns_rebinding_protection=False,
), ),
) )
from fabledassistant.mcp.tools import register_all from scribe.mcp.tools import register_all
register_all(mcp) register_all(mcp)
return mcp return mcp
@@ -239,7 +239,7 @@ def mount_mcp(app: Quart) -> None:
inside Quart, we hook the session manager's `run()` async context manager inside Quart, we hook the session manager's `run()` async context manager
into Quart's serving lifecycle (before_serving / after_serving). into Quart's serving lifecycle (before_serving / after_serving).
""" """
from fabledassistant.mcp.auth import resolve_bearer from scribe.mcp.auth import resolve_bearer
mcp = build_mcp_server() mcp = build_mcp_server()
mcp_asgi = mcp.streamable_http_app() mcp_asgi = mcp.streamable_http_app()
@@ -299,7 +299,7 @@ def mount_mcp(app: Quart) -> None:
return return
scope["scribe_user_id"] = user_id scope["scribe_user_id"] = user_id
from fabledassistant.mcp._context import _user_id_ctx from scribe.mcp._context import _user_id_ctx
token = _user_id_ctx.set(user_id) token = _user_id_ctx.set(user_id)
try: try:
await mcp_asgi(scope, receive, send) await mcp_asgi(scope, receive, send)
@@ -4,7 +4,7 @@ Each tool module exposes a `register(mcp)` function that attaches its tools
to a FastMCP instance. `register_all(mcp)` is the single entry point called to a FastMCP instance. `register_all(mcp)` is the single entry point called
from `mcp.server.build_mcp_server`. from `mcp.server.build_mcp_server`.
""" """
from fabledassistant.mcp.tools import ( from scribe.mcp.tools import (
entities, events, milestones, notes, processes, projects, recent, rulebooks, search, tags, tasks, trash, entities, events, milestones, notes, processes, projects, recent, rulebooks, search, tags, tasks, trash,
) )
@@ -14,9 +14,9 @@ of plain strings for ergonomics; checked-state is reset to False on each call.
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import knowledge as knowledge_svc from scribe.services import knowledge as knowledge_svc
from fabledassistant.services import notes as notes_svc from scribe.services import notes as notes_svc
async def _list_by_type(note_type: str, q: str, tag: str, limit: int) -> dict: async def _list_by_type(note_type: str, q: str, tag: str, limit: int) -> dict:
@@ -19,9 +19,9 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import events as events_svc from scribe.services import events as events_svc
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
def _combine(start_date: str, start_time: str) -> datetime: def _combine(start_date: str, start_time: str) -> datetime:
@@ -11,9 +11,9 @@ Sentinels:
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import milestones as milestones_svc from scribe.services import milestones as milestones_svc
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
async def list_milestones(project_id: int) -> dict: async def list_milestones(project_id: int) -> dict:
@@ -13,9 +13,9 @@ Sentinel conventions (inherited from existing fable-mcp tools):
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import notes as notes_svc from scribe.services import notes as notes_svc
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
async def list_notes( async def list_notes(
@@ -6,9 +6,9 @@ get_process is the fire mechanism: it returns the full prompt for Claude to run.
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import knowledge as knowledge_svc from scribe.services import knowledge as knowledge_svc
from fabledassistant.services import notes as notes_svc from scribe.services import notes as notes_svc
async def list_processes(q: str = "", tag: str = "", limit: int = 50) -> dict: async def list_processes(q: str = "", tag: str = "", limit: int = 50) -> dict:
@@ -16,12 +16,12 @@ keeps working.
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import milestones as milestones_svc from scribe.services import milestones as milestones_svc
from fabledassistant.services import notes as notes_svc from scribe.services import notes as notes_svc
from fabledassistant.services import projects as projects_svc from scribe.services import projects as projects_svc
from fabledassistant.services import rulebooks as rulebooks_svc from scribe.services import rulebooks as rulebooks_svc
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
async def list_projects() -> dict: async def list_projects() -> dict:
@@ -13,11 +13,11 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.event import Event from scribe.models.event import Event
from fabledassistant.models.note import Note from scribe.models.note import Note
from fabledassistant.models.project import Project from scribe.models.project import Project
async def get_recent(days: int = 7, limit: int = 25) -> dict: async def get_recent(days: int = 7, limit: int = 25) -> dict:
@@ -9,9 +9,9 @@ spec.
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import rulebooks as rulebooks_svc from scribe.services import rulebooks as rulebooks_svc
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
# ── Rulebook CRUD ─────────────────────────────────────────────────────── # ── Rulebook CRUD ───────────────────────────────────────────────────────
@@ -7,8 +7,8 @@ working. Differences from fable-mcp:
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services.embeddings import semantic_search_notes from scribe.services.embeddings import semantic_search_notes
async def search(q: str, content_type: str = "all", limit: int = 10) -> dict: async def search(q: str, content_type: str = "all", limit: int = 10) -> dict:
@@ -8,9 +8,9 @@ from __future__ import annotations
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.note import Note from scribe.models.note import Note
def _aggregate_tag_counts(tag_lists) -> dict[str, int]: def _aggregate_tag_counts(tag_lists) -> dict[str, int]:
@@ -18,12 +18,12 @@ Sentinels (preserved from existing fable-mcp):
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import notes as notes_svc from scribe.services import notes as notes_svc
from fabledassistant.services import planning as planning_svc from scribe.services import planning as planning_svc
from fabledassistant.services import rulebooks as rulebooks_svc from scribe.services import rulebooks as rulebooks_svc
from fabledassistant.services import task_logs as task_logs_svc from scribe.services import task_logs as task_logs_svc
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
async def list_tasks( async def list_tasks(
@@ -6,8 +6,8 @@ the deleted_batch_id that a delete operation returns.
""" """
from __future__ import annotations from __future__ import annotations
from fabledassistant.mcp._context import current_user_id from scribe.mcp._context import current_user_id
from fabledassistant.services import trash as trash_svc from scribe.services import trash as trash_svc
async def list_trash() -> dict: async def list_trash() -> dict:
+42
View File
@@ -0,0 +1,42 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
from scribe.config import Config
engine = create_async_engine(
Config.DATABASE_URL,
echo=False,
pool_pre_ping=True,
pool_recycle=1800,
)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
from scribe.models.base import CreatedAtMixin, TimestampMixin # noqa: E402, F401
from scribe.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401
from scribe.models.setting import Setting # noqa: E402, F401
from scribe.models.user import User # noqa: E402, F401
from scribe.models.app_log import AppLog # noqa: E402, F401
from scribe.models.password_reset import PasswordResetToken # noqa: E402, F401
from scribe.models.invitation import InvitationToken # noqa: E402, F401
from scribe.models.embedding import NoteEmbedding # noqa: E402, F401
from scribe.models.project import Project # noqa: E402, F401
from scribe.models.event import Event # noqa: E402, F401
from scribe.models.milestone import Milestone # noqa: E402, F401
from scribe.models.task_log import TaskLog # noqa: E402, F401
from scribe.models.note_draft import NoteDraft # noqa: E402, F401
from scribe.models.note_version import NoteVersion # noqa: E402, F401
from scribe.models.group import Group, GroupMembership # noqa: E402, F401
from scribe.models.share import NoteShare, ProjectShare # noqa: E402, F401
from scribe.models.notification import Notification # noqa: E402, F401
from scribe.models.api_key import ApiKey # noqa: E402, F401
from scribe.models.user_profile import UserProfile # noqa: E402, F401
from scribe.models.rulebook import ( # noqa: E402, F401
Rulebook, RulebookTopic, Rule, project_rulebook_subscriptions,
)
@@ -3,8 +3,8 @@ from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import CreatedAtMixin from scribe.models.base import CreatedAtMixin
class ApiKey(Base, CreatedAtMixin): class ApiKey(Base, CreatedAtMixin):
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy import DateTime, Float, Index, Integer, Text from sqlalchemy import DateTime, Float, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
class AppLog(Base): class AppLog(Base):
@@ -4,7 +4,7 @@ from sqlalchemy import DateTime, ForeignKey, Integer
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
class NoteEmbedding(Base): class NoteEmbedding(Base):
@@ -3,8 +3,8 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Text from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import SoftDeleteMixin from scribe.models.base import SoftDeleteMixin
class Event(Base, SoftDeleteMixin): class Event(Base, SoftDeleteMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import CreatedAtMixin, TimestampMixin from scribe.models.base import CreatedAtMixin, TimestampMixin
class Group(Base, TimestampMixin): class Group(Base, TimestampMixin):
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
class InvitationToken(Base): class InvitationToken(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin, SoftDeleteMixin from scribe.models.base import TimestampMixin, SoftDeleteMixin
class Milestone(Base, TimestampMixin, SoftDeleteMixin): class Milestone(Base, TimestampMixin, SoftDeleteMixin):
@@ -5,8 +5,8 @@ from sqlalchemy import Date, DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin, SoftDeleteMixin from scribe.models.base import TimestampMixin, SoftDeleteMixin
class TaskStatus(str, enum.Enum): class TaskStatus(str, enum.Enum):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin from scribe.models.base import TimestampMixin
class NoteDraft(Base, TimestampMixin): class NoteDraft(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import ARRAY, ForeignKey, Integer, Text from sqlalchemy import ARRAY, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import CreatedAtMixin from scribe.models.base import CreatedAtMixin
class NoteVersion(Base, CreatedAtMixin): class NoteVersion(Base, CreatedAtMixin):
@@ -4,8 +4,8 @@ from sqlalchemy import DateTime, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import CreatedAtMixin from scribe.models.base import CreatedAtMixin
class Notification(Base, CreatedAtMixin): class Notification(Base, CreatedAtMixin):
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
class PasswordResetToken(Base): class PasswordResetToken(Base):
@@ -2,8 +2,8 @@ import enum
from datetime import datetime from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, Text from sqlalchemy import DateTime, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin, SoftDeleteMixin from scribe.models.base import TimestampMixin, SoftDeleteMixin
class ProjectStatus(str, enum.Enum): class ProjectStatus(str, enum.Enum):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import BigInteger, Boolean, Column, DateTime, ForeignKey, Index, Integer, Table, Text, text from sqlalchemy import BigInteger, Boolean, Column, DateTime, ForeignKey, Index, Integer, Table, Text, text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import SoftDeleteMixin from scribe.models.base import SoftDeleteMixin
class Rulebook(Base, SoftDeleteMixin): class Rulebook(Base, SoftDeleteMixin):
@@ -1,7 +1,7 @@
from sqlalchemy import ForeignKey, Integer, Text from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
class Setting(Base): class Setting(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin from scribe.models.base import TimestampMixin
class ProjectShare(Base, TimestampMixin): class ProjectShare(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin from scribe.models.base import TimestampMixin
class TaskLog(Base, TimestampMixin): class TaskLog(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import CreatedAtMixin from scribe.models.base import CreatedAtMixin
class User(Base, CreatedAtMixin): class User(Base, CreatedAtMixin):
@@ -2,8 +2,8 @@ from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base from scribe.models import Base
from fabledassistant.models.base import TimestampMixin from scribe.models.base import TimestampMixin
class UserProfile(Base, TimestampMixin): class UserProfile(Base, TimestampMixin):
@@ -3,8 +3,8 @@ import json
from quart import Blueprint, Response, g, jsonify, request from quart import Blueprint, Response, g, jsonify, request
from fabledassistant.auth import admin_required, login_required, get_current_user_id from scribe.auth import admin_required, login_required, get_current_user_id
from fabledassistant.services.auth import ( from scribe.services.auth import (
create_invitation, create_invitation,
delete_user, delete_user,
is_registration_open, is_registration_open,
@@ -13,15 +13,15 @@ from fabledassistant.services.auth import (
revoke_invitation, revoke_invitation,
set_registration_open, set_registration_open,
) )
from fabledassistant.services.backup import ( from scribe.services.backup import (
export_full_backup, export_full_backup,
export_user_backup, export_user_backup,
restore_full_backup, restore_full_backup,
) )
from fabledassistant.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email from scribe.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email
from fabledassistant.services.logging import get_logs, get_log_stats, log_audit from scribe.services.logging import get_logs, get_log_stats, log_audit
from fabledassistant.services.notifications import send_invitation_email from scribe.services.notifications import send_invitation_email
from fabledassistant.services.settings import set_setting, set_settings_batch from scribe.services.settings import set_setting, set_settings_batch
admin_bp = Blueprint("admin", __name__, url_prefix="/api/admin") admin_bp = Blueprint("admin", __name__, url_prefix="/api/admin")
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.services.api_keys import create_api_key, list_api_keys, revoke_api_key from scribe.services.api_keys import create_api_key, list_api_keys, revoke_api_key
api_keys_bp = Blueprint("api_keys", __name__, url_prefix="/api/api-keys") api_keys_bp = Blueprint("api_keys", __name__, url_prefix="/api/api-keys")
@@ -6,10 +6,10 @@ import secrets
from quart import Blueprint, g, jsonify, redirect, request, session from quart import Blueprint, g, jsonify, redirect, request, session
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.config import Config from scribe.config import Config
from fabledassistant.rate_limit import is_rate_limited from scribe.rate_limit import is_rate_limited
from fabledassistant.services.auth import ( from scribe.services.auth import (
authenticate, authenticate,
change_password, change_password,
create_password_reset_token, create_password_reset_token,
@@ -26,14 +26,14 @@ from fabledassistant.services.auth import (
validate_invitation_token, validate_invitation_token,
verify_password, verify_password,
) )
from fabledassistant.services.logging import log_audit from scribe.services.logging import log_audit
from fabledassistant.services.notifications import ( from scribe.services.notifications import (
notify_security_event, notify_security_event,
send_password_reset_email, send_password_reset_email,
send_password_reset_success_email, send_password_reset_success_email,
) )
from fabledassistant.services.email import get_base_url, is_smtp_configured from scribe.services.email import get_base_url, is_smtp_configured
from fabledassistant.services.oauth import ( from scribe.services.oauth import (
build_auth_url, build_auth_url,
exchange_code, exchange_code,
get_userinfo, get_userinfo,
@@ -1,8 +1,8 @@
"""Dashboard REST endpoint — the aggregated landing payload.""" """Dashboard REST endpoint — the aggregated landing payload."""
from quart import Blueprint, g, jsonify from quart import Blueprint, g, jsonify
from fabledassistant.auth import login_required from scribe.auth import login_required
from fabledassistant.services.dashboard import build_dashboard from scribe.services.dashboard import build_dashboard
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/api/dashboard") dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/api/dashboard")
@@ -5,8 +5,8 @@ from datetime import datetime, timezone
from quart import Blueprint, g, jsonify, request from quart import Blueprint, g, jsonify, request
from fabledassistant.auth import login_required from scribe.auth import login_required
import fabledassistant.services.events as events_svc import scribe.services.events as events_svc
events_bp = Blueprint("events", __name__, url_prefix="/api/events") events_bp = Blueprint("events", __name__, url_prefix="/api/events")
@@ -126,7 +126,7 @@ async def update_event(event_id: int):
@events_bp.delete("/<int:event_id>") @events_bp.delete("/<int:event_id>")
@login_required @login_required
async def delete_event(event_id: int): async def delete_event(event_id: int):
from fabledassistant.services.trash import delete as trash_delete from scribe.services.trash import delete as trash_delete
batch = await trash_delete(_get_current_user_id(), "event", event_id) batch = await trash_delete(_get_current_user_id(), "event", event_id)
if batch is None: if batch is None:
return jsonify({"error": "Event not found"}), 404 return jsonify({"error": "Event not found"}), 404
@@ -137,6 +137,6 @@ async def delete_event(event_id: int):
@login_required @login_required
async def sync_caldav(): async def sync_caldav():
"""Trigger a CalDAV pull sync for the current user.""" """Trigger a CalDAV pull sync for the current user."""
from fabledassistant.services.caldav_sync import sync_user_events from scribe.services.caldav_sync import sync_user_events
result = await sync_user_events(user_id=_get_current_user_id()) result = await sync_user_events(user_id=_get_current_user_id())
return jsonify(result) return jsonify(result)
@@ -6,9 +6,9 @@ from datetime import datetime, timezone
from quart import Blueprint, Response, request from quart import Blueprint, Response, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.note import Note from scribe.models.note import Note
from sqlalchemy import select from sqlalchemy import select
export_bp = Blueprint("export", __name__) export_bp = Blueprint("export", __name__)
@@ -83,7 +83,7 @@ async def export_data():
status=200, status=200,
headers={ headers={
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
"Content-Disposition": f'attachment; filename="fabledassistant-{stamp}.json"', "Content-Disposition": f'attachment; filename="scribe-{stamp}.json"',
}, },
) )
@@ -112,6 +112,6 @@ async def export_data():
status=200, status=200,
headers={ headers={
"Content-Type": "application/zip", "Content-Type": "application/zip",
"Content-Disposition": f'attachment; filename="fabledassistant-{stamp}.zip"', "Content-Disposition": f'attachment; filename="scribe-{stamp}.zip"',
}, },
) )
@@ -2,9 +2,9 @@ import asyncio
from quart import Blueprint, g, jsonify, request from quart import Blueprint, g, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.services import groups as group_svc from scribe.services import groups as group_svc
from fabledassistant.services.notifications import notify_group_added from scribe.services.notifications import notify_group_added
groups_bp = Blueprint("groups", __name__, url_prefix="/api/groups") groups_bp = Blueprint("groups", __name__, url_prefix="/api/groups")
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.services.notifications import ( from scribe.services.notifications import (
list_in_app_notifications, list_in_app_notifications,
mark_all_notifications_read, mark_all_notifications_read,
mark_notification_read, mark_notification_read,
@@ -3,8 +3,8 @@ import logging
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.routes.utils import parse_pagination from scribe.routes.utils import parse_pagination
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ async def list_knowledge():
limit, offset = parse_pagination(default_limit=24, max_limit=100) limit, offset = parse_pagination(default_limit=24, max_limit=100)
page = max(1, int(request.args.get("page", 1))) page = max(1, int(request.args.get("page", 1)))
from fabledassistant.services.knowledge import query_knowledge from scribe.services.knowledge import query_knowledge
items, total = await query_knowledge( items, total = await query_knowledge(
user_id=uid, user_id=uid,
note_type=note_type, note_type=note_type,
@@ -88,7 +88,7 @@ async def list_knowledge_ids():
if note_type and note_type not in _VALID_TYPES: if note_type and note_type not in _VALID_TYPES:
return jsonify({"error": "Invalid type"}), 400 return jsonify({"error": "Invalid type"}), 400
from fabledassistant.services.knowledge import query_knowledge_ids from scribe.services.knowledge import query_knowledge_ids
ids, total = await query_knowledge_ids( ids, total = await query_knowledge_ids(
user_id=uid, note_type=note_type, tags=tags, user_id=uid, note_type=note_type, tags=tags,
sort=sort, q=q, limit=limit, offset=offset, sort=sort, q=q, limit=limit, offset=offset,
@@ -114,7 +114,7 @@ async def get_knowledge_batch():
if len(ids) > 100: if len(ids) > 100:
return jsonify({"error": "Too many IDs (max 100)"}), 400 return jsonify({"error": "Too many IDs (max 100)"}), 400
from fabledassistant.services.knowledge import get_knowledge_by_ids from scribe.services.knowledge import get_knowledge_by_ids
items = await get_knowledge_by_ids(uid, ids) items = await get_knowledge_by_ids(uid, ids)
return jsonify({"items": items}) return jsonify({"items": items})
@@ -126,7 +126,7 @@ async def list_knowledge_tags():
uid = get_current_user_id() uid = get_current_user_id()
note_type = request.args.get("type", "").strip().lower() or None note_type = request.args.get("type", "").strip().lower() or None
from fabledassistant.services.knowledge import get_knowledge_tags from scribe.services.knowledge import get_knowledge_tags
tags = await get_knowledge_tags(uid, note_type=note_type) tags = await get_knowledge_tags(uid, note_type=note_type)
return jsonify({"tags": tags}) return jsonify({"tags": tags})
@@ -139,6 +139,6 @@ async def get_knowledge_counts():
tags_raw = request.args.get("tags", "").strip() tags_raw = request.args.get("tags", "").strip()
tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else None tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else None
from fabledassistant.services.knowledge import get_knowledge_counts as _counts from scribe.services.knowledge import get_knowledge_counts as _counts
counts = await _counts(uid, tags=tags) counts = await _counts(uid, tags=tags)
return jsonify(counts) return jsonify(counts)
@@ -3,10 +3,10 @@ import logging
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.routes.utils import not_found, parse_pagination from scribe.routes.utils import not_found, parse_pagination
from fabledassistant.services.access import can_write_project from scribe.services.access import can_write_project
from fabledassistant.services.milestones import ( from scribe.services.milestones import (
create_milestone, create_milestone,
delete_milestone, delete_milestone,
get_milestone_in_project, get_milestone_in_project,
@@ -14,8 +14,8 @@ from fabledassistant.services.milestones import (
list_milestones, list_milestones,
update_milestone, update_milestone,
) )
from fabledassistant.services.notes import list_notes from scribe.services.notes import list_notes
from fabledassistant.services.projects import get_project_for_user from scribe.services.projects import get_project_for_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -113,7 +113,7 @@ async def delete_milestone_route(project_id: int, milestone_id: int):
milestone = await get_milestone_in_project(project_id, milestone_id) milestone = await get_milestone_in_project(project_id, milestone_id)
if milestone is None: if milestone is None:
return not_found("Milestone") return not_found("Milestone")
from fabledassistant.services.trash import delete as trash_delete from scribe.services.trash import delete as trash_delete
batch = await trash_delete(milestone.user_id, "milestone", milestone_id) batch = await trash_delete(milestone.user_id, "milestone", milestone_id)
if batch is None: if batch is None:
return not_found("Milestone") return not_found("Milestone")
@@ -2,14 +2,14 @@ import asyncio
import logging import logging
import re import re
from fabledassistant.services.embeddings import upsert_note_embedding from scribe.services.embeddings import upsert_note_embedding
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.routes.utils import not_found, parse_iso_date, parse_pagination from scribe.routes.utils import not_found, parse_iso_date, parse_pagination
from fabledassistant.services.access import can_write_note from scribe.services.access import can_write_note
from fabledassistant.services.notes import ( from scribe.services.notes import (
build_note_graph, build_note_graph,
convert_note_to_task, convert_note_to_task,
convert_task_to_note, convert_task_to_note,
@@ -23,8 +23,8 @@ from fabledassistant.services.notes import (
list_notes, list_notes,
update_note, update_note,
) )
from fabledassistant.services.note_drafts import upsert_draft, get_draft, delete_draft from scribe.services.note_drafts import upsert_draft, get_draft, delete_draft
from fabledassistant.services.note_versions import list_versions, get_version from scribe.services.note_versions import list_versions, get_version
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -89,7 +89,7 @@ async def create_note_route():
project_id = data.get("project_id") project_id = data.get("project_id")
if project_id is None and data.get("project"): if project_id is None and data.get("project"):
from fabledassistant.services.projects import get_project_by_title as _gpbt from scribe.services.projects import get_project_by_title as _gpbt
proj = await _gpbt(uid, data["project"]) proj = await _gpbt(uid, data["project"])
if proj: if proj:
project_id = proj.id project_id = proj.id
@@ -282,7 +282,7 @@ async def delete_note_route(note_id: int):
note_obj, _ = result note_obj, _ = result
if not await can_write_note(uid, note_id): if not await can_write_note(uid, note_id):
return jsonify({"error": "Permission denied"}), 403 return jsonify({"error": "Permission denied"}), 403
from fabledassistant.services.trash import delete as trash_delete from scribe.services.trash import delete as trash_delete
batch = await trash_delete(note_obj.user_id, "note", note_id) batch = await trash_delete(note_obj.user_id, "note", note_id)
if batch is None: if batch is None:
return not_found("Note") return not_found("Note")
@@ -445,7 +445,7 @@ async def pin_version_route(note_id: int, version_id: int):
label = data.get("label") label = data.get("label")
if label is not None and not isinstance(label, str): if label is not None and not isinstance(label, str):
return jsonify({"error": "label must be a string or null"}), 400 return jsonify({"error": "label must be a string or null"}), 400
from fabledassistant.services.version_pinning import pin_version from scribe.services.version_pinning import pin_version
try: try:
version = await pin_version(uid, note_id, version_id, label=label) version = await pin_version(uid, note_id, version_id, label=label)
except ValueError as e: except ValueError as e:
@@ -462,7 +462,7 @@ async def pin_version_route(note_id: int, version_id: int):
async def unpin_version_route(note_id: int, version_id: int): async def unpin_version_route(note_id: int, version_id: int):
"""Downgrade a manually-pinned version back to rolling.""" """Downgrade a manually-pinned version back to rolling."""
uid = get_current_user_id() uid = get_current_user_id()
from fabledassistant.services.version_pinning import unpin_version from scribe.services.version_pinning import unpin_version
version = await unpin_version(uid, note_id, version_id) version = await unpin_version(uid, note_id, version_id)
if version is None: if version is None:
return not_found("Version") return not_found("Version")
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.services.user_profile import ( from scribe.services.user_profile import (
VALID_EXPERTISE, VALID_EXPERTISE,
VALID_STYLES, VALID_STYLES,
VALID_TONES, VALID_TONES,
@@ -4,11 +4,11 @@ import logging
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.routes.utils import not_found, parse_pagination from scribe.routes.utils import not_found, parse_pagination
from fabledassistant.services.milestones import list_milestones from scribe.services.milestones import list_milestones
from fabledassistant.services.notes import list_notes from scribe.services.notes import list_notes
from fabledassistant.services.projects import ( from scribe.services.projects import (
create_project, create_project,
delete_project, delete_project,
get_project, get_project,
@@ -101,7 +101,7 @@ async def update_project_route(project_id: int):
@login_required @login_required
async def delete_project_route(project_id: int): async def delete_project_route(project_id: int):
uid = get_current_user_id() uid = get_current_user_id()
from fabledassistant.services.trash import delete as trash_delete from scribe.services.trash import delete as trash_delete
batch = await trash_delete(uid, "project", project_id) batch = await trash_delete(uid, "project", project_id)
if batch is None: if batch is None:
return not_found("Project") return not_found("Project")
@@ -7,9 +7,9 @@ from __future__ import annotations
from quart import Blueprint, g, jsonify, request from quart import Blueprint, g, jsonify, request
from fabledassistant.auth import login_required from scribe.auth import login_required
import fabledassistant.services.rulebooks as rulebooks_svc import scribe.services.rulebooks as rulebooks_svc
from fabledassistant.services.trash import delete as trash_delete from scribe.services.trash import delete as trash_delete
rulebooks_bp = Blueprint("rulebooks", __name__, url_prefix="/api") rulebooks_bp = Blueprint("rulebooks", __name__, url_prefix="/api")
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.services.embeddings import semantic_search_notes from scribe.services.embeddings import semantic_search_notes
search_bp = Blueprint("search", __name__, url_prefix="/api/search") search_bp = Blueprint("search", __name__, url_prefix="/api/search")
@@ -10,10 +10,10 @@ from urllib.parse import urlparse
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.config import Config from scribe.config import Config
from fabledassistant.services.caldav import CALDAV_SETTING_KEYS, get_caldav_config, test_connection from scribe.services.caldav import CALDAV_SETTING_KEYS, get_caldav_config, test_connection
from fabledassistant.services.settings import delete_setting, get_all_settings, get_setting, set_settings_batch from scribe.services.settings import delete_setting, get_all_settings, get_setting, set_settings_batch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -2,11 +2,11 @@ import asyncio
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.services import sharing as share_svc from scribe.services import sharing as share_svc
from fabledassistant.services.access import can_admin_project, can_write_note from scribe.services.access import can_admin_project, can_write_note
from fabledassistant.services.notifications import notify_note_shared, notify_project_shared from scribe.services.notifications import notify_note_shared, notify_project_shared
from fabledassistant.services.sharing import list_shared_with_me from scribe.services.sharing import list_shared_with_me
shares_bp = Blueprint("shares", __name__) shares_bp = Blueprint("shares", __name__)
@@ -3,8 +3,8 @@ import logging
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.services.task_logs import create_log, delete_log, list_logs, update_log from scribe.services.task_logs import create_log, delete_log, list_logs, update_log
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -49,7 +49,7 @@ async def update_log_route(task_id: int, log_id: int):
if content is not None: if content is not None:
content = content.strip() or None content = content.strip() or None
from fabledassistant.services.task_logs import _UNSET from scribe.services.task_logs import _UNSET
duration_minutes = data.get("duration_minutes", _UNSET) duration_minutes = data.get("duration_minutes", _UNSET)
log = await update_log(uid, log_id, content=content, duration_minutes=duration_minutes) log = await update_log(uid, log_id, content=content, duration_minutes=duration_minutes)
@@ -3,20 +3,20 @@ from datetime import date
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id from scribe.auth import login_required, get_current_user_id
from fabledassistant.models.note import TaskPriority, TaskStatus from scribe.models.note import TaskPriority, TaskStatus
from fabledassistant.routes.utils import not_found, parse_iso_date, parse_pagination from scribe.routes.utils import not_found, parse_iso_date, parse_pagination
from fabledassistant.services.access import can_write_note from scribe.services.access import can_write_note
from fabledassistant.services.embeddings import upsert_note_embedding from scribe.services.embeddings import upsert_note_embedding
from fabledassistant.services.notes import ( from scribe.services.notes import (
create_note, create_note,
get_note, get_note,
get_note_for_user, get_note_for_user,
list_notes, list_notes,
update_note, update_note,
) )
from fabledassistant.services.planning import start_planning as svc_start_planning from scribe.services.planning import start_planning as svc_start_planning
from fabledassistant.services.recurrence import calculate_next_due, validate_recurrence_rule from scribe.services.recurrence import calculate_next_due, validate_recurrence_rule
tasks_bp = Blueprint("tasks", __name__, url_prefix="/api/tasks") tasks_bp = Blueprint("tasks", __name__, url_prefix="/api/tasks")
@@ -117,7 +117,7 @@ async def create_task_route():
project_id = data.get("project_id") project_id = data.get("project_id")
if project_id is None and data.get("project"): if project_id is None and data.get("project"):
from fabledassistant.services.projects import get_project_by_title as _gpbt from scribe.services.projects import get_project_by_title as _gpbt
proj = await _gpbt(uid, data["project"]) proj = await _gpbt(uid, data["project"])
if proj: if proj:
project_id = proj.id project_id = proj.id
@@ -299,7 +299,7 @@ async def delete_task_route(task_id: int):
task_note, _ = result task_note, _ = result
if not await can_write_note(uid, task_id): if not await can_write_note(uid, task_id):
return jsonify({"error": "Permission denied"}), 403 return jsonify({"error": "Permission denied"}), 403
from fabledassistant.services.trash import delete as trash_delete from scribe.services.trash import delete as trash_delete
batch = await trash_delete(task_note.user_id, "task", task_id) batch = await trash_delete(task_note.user_id, "task", task_id)
if batch is None: if batch is None:
return not_found("Task") return not_found("Task")
@@ -3,8 +3,8 @@ from __future__ import annotations
from quart import Blueprint, g, jsonify from quart import Blueprint, g, jsonify
from fabledassistant.auth import login_required from scribe.auth import login_required
import fabledassistant.services.trash as trash_svc import scribe.services.trash as trash_svc
trash_bp = Blueprint("trash", __name__, url_prefix="/api/trash") trash_bp = Blueprint("trash", __name__, url_prefix="/api/trash")
@@ -1,9 +1,9 @@
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from sqlalchemy import or_, select from sqlalchemy import or_, select
from fabledassistant.auth import get_current_user_id, login_required from scribe.auth import get_current_user_id, login_required
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.user import User from scribe.models.user import User
users_bp = Blueprint("users", __name__, url_prefix="/api/users") users_bp = Blueprint("users", __name__, url_prefix="/api/users")
@@ -11,11 +11,11 @@ import logging
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.group import GroupMembership from scribe.models.group import GroupMembership
from fabledassistant.models.note import Note from scribe.models.note import Note
from fabledassistant.models.project import Project from scribe.models.project import Project
from fabledassistant.models.share import NoteShare, ProjectShare from scribe.models.share import NoteShare, ProjectShare
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -4,8 +4,8 @@ from datetime import datetime, timezone
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.api_key import ApiKey from scribe.models.api_key import ApiKey
def generate_key() -> str: def generate_key() -> str:
@@ -6,12 +6,12 @@ from datetime import datetime, timedelta, timezone
import bcrypt import bcrypt
from sqlalchemy import func, select, update from sqlalchemy import func, select, update
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.note import Note from scribe.models.note import Note
from fabledassistant.models.invitation import InvitationToken from scribe.models.invitation import InvitationToken
from fabledassistant.models.password_reset import PasswordResetToken from scribe.models.password_reset import PasswordResetToken
from fabledassistant.models.setting import Setting from scribe.models.setting import Setting
from fabledassistant.models.user import User from scribe.models.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -172,7 +172,7 @@ async def delete_user(user_id: int) -> bool:
async def set_registration_open(admin_user_id: int, open: bool) -> None: async def set_registration_open(admin_user_id: int, open: bool) -> None:
from fabledassistant.services.settings import set_setting from scribe.services.settings import set_setting
await set_setting(admin_user_id, "registration_open", "true" if open else "false") await set_setting(admin_user_id, "registration_open", "true" if open else "false")
@@ -3,15 +3,15 @@ from datetime import date, datetime, timezone
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.milestone import Milestone from scribe.models.milestone import Milestone
from fabledassistant.models.note import Note from scribe.models.note import Note
from fabledassistant.models.note_draft import NoteDraft from scribe.models.note_draft import NoteDraft
from fabledassistant.models.note_version import NoteVersion from scribe.models.note_version import NoteVersion
from fabledassistant.models.project import Project from scribe.models.project import Project
from fabledassistant.models.setting import Setting from scribe.models.setting import Setting
from fabledassistant.models.task_log import TaskLog from scribe.models.task_log import TaskLog
from fabledassistant.models.user import User from scribe.models.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
import caldav import caldav
import icalendar import icalendar
from fabledassistant.services.settings import get_all_settings from scribe.services.settings import get_all_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -194,7 +194,7 @@ async def create_event(
tz = timezone or config.get("caldav_timezone") or None tz = timezone or config.get("caldav_timezone") or None
cal = icalendar.Calendar() cal = icalendar.Calendar()
cal.add("prodid", "-//FabledAssistant//EN") cal.add("prodid", "-//Scribe//EN")
cal.add("version", "2.0") cal.add("version", "2.0")
event = icalendar.Event() event = icalendar.Event()
@@ -415,7 +415,7 @@ async def update_event(
# Rebuild ical data and save # Rebuild ical data and save
cal_data = icalendar.Calendar() cal_data = icalendar.Calendar()
cal_data.add("prodid", "-//FabledAssistant//EN") cal_data.add("prodid", "-//Scribe//EN")
cal_data.add("version", "2.0") cal_data.add("version", "2.0")
cal_data.add_component(component) cal_data.add_component(component)
event_obj.data = cal_data.to_ical().decode("utf-8") event_obj.data = cal_data.to_ical().decode("utf-8")
@@ -690,7 +690,7 @@ async def update_todo(
# Rebuild ical data and save # Rebuild ical data and save
cal_data = icalendar.Calendar() cal_data = icalendar.Calendar()
cal_data.add("prodid", "-//FabledAssistant//EN") cal_data.add("prodid", "-//Scribe//EN")
cal_data.add("version", "2.0") cal_data.add("version", "2.0")
cal_data.add_component(component) cal_data.add_component(component)
todo_obj.data = cal_data.to_ical().decode("utf-8") todo_obj.data = cal_data.to_ical().decode("utf-8")
@@ -13,8 +13,8 @@ from typing import Any
from sqlalchemy import select, update from sqlalchemy import select, update
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.event import Event from scribe.models.event import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -112,7 +112,7 @@ async def sync_user_events(user_id: int) -> dict:
Returns a summary dict: {created, updated, unchanged}. Returns a summary dict: {created, updated, unchanged}.
""" """
from fabledassistant.services.caldav import get_caldav_config, is_caldav_configured # noqa: PLC0415 from scribe.services.caldav import get_caldav_config, is_caldav_configured # noqa: PLC0415
if not await is_caldav_configured(user_id): if not await is_caldav_configured(user_id):
return {"skipped": True, "reason": "CalDAV not configured"} return {"skipped": True, "reason": "CalDAV not configured"}
@@ -234,7 +234,7 @@ async def sync_all_users() -> None:
"""Pull CalDAV events for all users with CalDAV configured.""" """Pull CalDAV events for all users with CalDAV configured."""
from sqlalchemy import select as sa_select # noqa: PLC0415 from sqlalchemy import select as sa_select # noqa: PLC0415
from fabledassistant.models.user import User # noqa: PLC0415 from scribe.models.user import User # noqa: PLC0415
async with async_session() as session: async with async_session() as session:
result = await session.execute(sa_select(User.id)) result = await session.execute(sa_select(User.id))
@@ -11,11 +11,11 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import case, func, select from sqlalchemy import case, func, select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.note import Note from scribe.models.note import Note
from fabledassistant.models.project import Project from scribe.models.project import Project
from fabledassistant.models.milestone import Milestone from scribe.models.milestone import Milestone
from fabledassistant.services import milestones as milestones_svc from scribe.services import milestones as milestones_svc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -145,7 +145,7 @@ async def _recently_completed(user_id: int) -> list[dict]:
async def _upcoming_events(user_id: int) -> list[dict]: async def _upcoming_events(user_id: int) -> list[dict]:
from fabledassistant.services import events as events_svc from scribe.services import events as events_svc
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
rows = await events_svc.list_events(user_id, now, now + timedelta(days=WINDOW_DAYS)) rows = await events_svc.list_events(user_id, now, now + timedelta(days=WINDOW_DAYS))
out = [] out = []
@@ -86,7 +86,7 @@ def _process_rss_mb() -> float | None:
def _db_pool_stats() -> dict[str, Any]: def _db_pool_stats() -> dict[str, Any]:
"""Pool checked-in / checked-out / overflow. Direct from SQLAlchemy.""" """Pool checked-in / checked-out / overflow. Direct from SQLAlchemy."""
try: try:
from fabledassistant.models import engine from scribe.models import engine
pool = engine.pool pool = engine.pool
# Async engines wrap a sync pool; .checkedin() / .checkedout() exist # Async engines wrap a sync pool; .checkedin() / .checkedout() exist
# on the underlying QueuePool. Attribute access is documented but # on the underlying QueuePool. Attribute access is documented but
@@ -172,7 +172,7 @@ def _setup_file_logger() -> logging.Logger:
return _file_logger return _file_logger
try: try:
_DIAG_DIR.mkdir(parents=True, exist_ok=True) _DIAG_DIR.mkdir(parents=True, exist_ok=True)
flog = logging.getLogger("fabledassistant.diagnostics.persistent") flog = logging.getLogger("scribe.diagnostics.persistent")
flog.setLevel(logging.INFO) flog.setLevel(logging.INFO)
flog.propagate = False # don't double-log into the root stdout stream flog.propagate = False # don't double-log into the root stdout stream
# Don't re-add handlers across module reloads / re-runs. # Don't re-add handlers across module reloads / re-runs.
@@ -6,10 +6,10 @@ from email.message import EmailMessage
import aiosmtplib import aiosmtplib
from sqlalchemy import select from sqlalchemy import select
from fabledassistant.config import Config from scribe.config import Config
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.setting import Setting from scribe.models.setting import Setting
from fabledassistant.models.user import User from scribe.models.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -16,9 +16,9 @@ import os
from sqlalchemy import delete, select from sqlalchemy import delete, select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.embedding import NoteEmbedding from scribe.models.embedding import NoteEmbedding
from fabledassistant.models.note import Note from scribe.models.note import Note
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,8 +19,8 @@ from apscheduler.triggers.interval import IntervalTrigger
from dateutil.rrule import rrulestr from dateutil.rrule import rrulestr
from sqlalchemy import and_, or_, select from sqlalchemy import and_, or_, select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.event import Event from scribe.models.event import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -84,7 +84,7 @@ async def _fire_reminders() -> None:
return return
# Deliver via the in-app notification feed (push was removed in Phase 8). # Deliver via the in-app notification feed (push was removed in Phase 8).
from fabledassistant.services.notifications import create_in_app_notification from scribe.services.notifications import create_in_app_notification
async with async_session() as session: async with async_session() as session:
for event_id, occurrence_start in to_notify: for event_id, occurrence_start in to_notify:
@@ -116,7 +116,7 @@ def _run_reminders(loop: asyncio.AbstractEventLoop) -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def _run_caldav_sync() -> None: async def _run_caldav_sync() -> None:
from fabledassistant.services.caldav_sync import sync_all_users # noqa: PLC0415 from scribe.services.caldav_sync import sync_all_users # noqa: PLC0415
try: try:
await sync_all_users() await sync_all_users()
except Exception: except Exception:
@@ -132,7 +132,7 @@ def _run_caldav_sync_threadsafe(loop: asyncio.AbstractEventLoop) -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def _run_recurrence_spawn() -> None: async def _run_recurrence_spawn() -> None:
from fabledassistant.services.recurrence import spawn_recurring_tasks # noqa: PLC0415 from scribe.services.recurrence import spawn_recurring_tasks # noqa: PLC0415
try: try:
await spawn_recurring_tasks() await spawn_recurring_tasks()
except Exception: except Exception:
@@ -18,8 +18,8 @@ from datetime import datetime, timedelta, timezone
from dateutil.rrule import rrulestr from dateutil.rrule import rrulestr
from sqlalchemy import or_, select from sqlalchemy import or_, select
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.event import Event from scribe.models.event import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -76,7 +76,7 @@ async def _localize_naive(user_id: int, dt: datetime | None) -> datetime | None:
asyncpg store the correct UTC instant, matching the REST/UI path. asyncpg store the correct UTC instant, matching the REST/UI path.
""" """
if dt is not None and dt.tzinfo is None: if dt is not None and dt.tzinfo is None:
from fabledassistant.services.tz import get_user_tz # noqa: PLC0415 from scribe.services.tz import get_user_tz # noqa: PLC0415
return dt.replace(tzinfo=await get_user_tz(user_id)) return dt.replace(tzinfo=await get_user_tz(user_id))
return dt return dt
@@ -398,7 +398,7 @@ async def find_events_by_query(user_id: int, query: str) -> list[Event]:
async def _push_create(event: Event, user_id: int, extra: dict) -> None: async def _push_create(event: Event, user_id: int, extra: dict) -> None:
try: try:
from fabledassistant.services.caldav import ( from scribe.services.caldav import (
create_event as caldav_create, create_event as caldav_create,
is_caldav_configured, is_caldav_configured,
) )
@@ -438,7 +438,7 @@ async def _push_update(event: Event, user_id: int, old_title: str = "") -> None:
if not event.caldav_uid: if not event.caldav_uid:
return return
try: try:
from fabledassistant.services.caldav import ( from scribe.services.caldav import (
update_event as caldav_update, update_event as caldav_update,
is_caldav_configured, is_caldav_configured,
) )
@@ -466,7 +466,7 @@ async def _push_update(event: Event, user_id: int, old_title: str = "") -> None:
async def _push_delete(caldav_uid: str, event_title: str, user_id: int) -> None: async def _push_delete(caldav_uid: str, event_title: str, user_id: int) -> None:
"""Push a delete to CalDAV. Uses event_title to locate the event by SUMMARY.""" """Push a delete to CalDAV. Uses event_title to locate the event by SUMMARY."""
try: try:
from fabledassistant.services.caldav import ( from scribe.services.caldav import (
delete_event as caldav_delete, delete_event as caldav_delete,
is_caldav_configured, is_caldav_configured,
) )
@@ -5,9 +5,9 @@ import logging
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from fabledassistant.models import async_session from scribe.models import async_session
from fabledassistant.models.group import Group, GroupMembership from scribe.models.group import Group, GroupMembership
from fabledassistant.models.user import User from scribe.models.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

Some files were not shown because too many files have changed in this diff Show More