refactor: rename package fabledassistant -> scribe (code-only)
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:
+1
-1
@@ -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 @@
|
|||||||
632037
|
1425947
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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 |
|
||||||
|------|---------------|
|
|------|---------------|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Generated
+2
-2
@@ -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,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",
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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")
|
||||||
|
|
||||||
+2
-2
@@ -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__)
|
||||||
|
|
||||||
+5
-5
@@ -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
Reference in New Issue
Block a user