rename(547): the server is grimoire — package, image, DB defaults, backups
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s

Step 2 of the Scribe -> Grimoire rename.

- src/scribe -> src/grimoire, and every import, patch target, module path,
  hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
  uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
  runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
  each overridable from .env, so an install created as Scribe keeps its data
  by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
  Archives named scribe-* with the scribe-backup-tar manifest still restore
  and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
  carry the new name.

Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.

Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 10:50:45 -04:00
co-authored by Claude Opus 5.5
parent e99f1169f7
commit 0f5ef0be63
447 changed files with 3611 additions and 3558 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
# An install created before the rename from Scribe keeps its database by
# setting these, and PGDATA_VOLUME, to the names it was created with.
POSTGRES_USER=fabled POSTGRES_USER=fabled
POSTGRES_PASSWORD=fabled POSTGRES_PASSWORD=fabled
POSTGRES_DB=scribe POSTGRES_DB=grimoire
# PGDATA_VOLUME=grimoire_pgdata
SECRET_KEY=dev-secret-change-me SECRET_KEY=dev-secret-change-me
+5 -5
View File
@@ -55,7 +55,7 @@ permissions:
env: env:
REGISTRY: git.fabledsword.com REGISTRY: git.fabledsword.com
IMAGE: git.fabledsword.com/bvandeusen/fabledscribe IMAGE: git.fabledsword.com/bvandeusen/fabledgrimoire
jobs: jobs:
typecheck: typecheck:
@@ -193,11 +193,11 @@ jobs:
# Same image as the shipped compose files: Postgres 17 with pgvector. # Same image as the shipped compose files: Postgres 17 with pgvector.
image: pgvector/pgvector:pg17 image: pgvector/pgvector:pg17
env: env:
POSTGRES_USER: scribe POSTGRES_USER: grimoire
POSTGRES_PASSWORD: ci_integration POSTGRES_PASSWORD: ci_integration
POSTGRES_DB: scribe_test POSTGRES_DB: grimoire_test
options: >- options: >-
--health-cmd "pg_isready -U scribe" --health-cmd "pg_isready -U grimoire"
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 10 --health-retries 10
@@ -235,7 +235,7 @@ jobs:
test "$(printf '%s\n' "$PG" | grep -c .)" -eq 1 test "$(printf '%s\n' "$PG" | grep -c .)" -eq 1
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG") PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
test -n "$PG_IP" test -n "$PG_IP"
export DATABASE_URL="postgresql+asyncpg://scribe:ci_integration@${PG_IP}:5432/scribe_test" export DATABASE_URL="postgresql+asyncpg://grimoire:ci_integration@${PG_IP}:5432/grimoire_test"
# Wait for Postgres. The run: shell is dash (no bash /dev/tcp), so use Python. # Wait for Postgres. The run: shell is dash (no bash /dev/tcp), so use Python.
/opt/venv/bin/python - "$PG_IP" <<'PY' /opt/venv/bin/python - "$PG_IP" <<'PY'
import socket, sys, time import socket, sys, time
+2 -2
View File
@@ -31,7 +31,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# alembic and hypercorn in CMD have to be found there. # alembic and hypercorn in CMD have to be found there.
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
COPY --from=build-frontend /build/dist/ src/scribe/static/ COPY --from=build-frontend /build/dist/ src/grimoire/static/
COPY alembic.ini . COPY alembic.ini .
COPY alembic/ alembic/ COPY alembic/ alembic/
@@ -57,4 +57,4 @@ ENV APP_CHANNEL=$BUILD_CHANNEL
ENV APP_COMMIT=$BUILD_COMMIT ENV APP_COMMIT=$BUILD_COMMIT
EXPOSE 5000 EXPOSE 5000
CMD ["sh", "-c", "alembic upgrade head && hypercorn 'scribe.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"] CMD ["sh", "-c", "alembic upgrade head && hypercorn 'grimoire.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
+3 -3
View File
@@ -4,9 +4,9 @@ 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 scribe.config import Config from grimoire.config import Config
from scribe.models import Base from grimoire.models import Base
from scribe.services.db_ready import wait_for_database from grimoire.services.db_ready import wait_for_database
config = context.config config = context.config
if config.config_file_name is not None: if config.config_file_name is not None:
+1 -1
View File
@@ -43,7 +43,7 @@ _LABEL_V1 = b"scribe.secrets.v1\x00"
def _fernet(): def _fernet():
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from scribe.config import Config from grimoire.config import Config
digest = hashlib.sha256(_LABEL_V1 + Config.SECRET_KEY.encode()).digest() digest = hashlib.sha256(_LABEL_V1 + Config.SECRET_KEY.encode()).digest()
return Fernet(base64.urlsafe_b64encode(digest)) return Fernet(base64.urlsafe_b64encode(digest))
+8 -7
View File
@@ -1,14 +1,14 @@
services: services:
app: app:
image: git.fabledsword.com/bvandeusen/fabledscribe:latest image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
environment: environment:
DATABASE_URL: "postgresql+asyncpg://scribe:${DB_PASSWORD}@db:5432/scribe" DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${DB_PASSWORD}@db:5432/${POSTGRES_DB:-grimoire}"
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:
- scribe_backend - backend
db: db:
# Postgres 17 with the vector extension Grimoire needs. # Postgres 17 with the vector extension Grimoire needs.
@@ -17,15 +17,16 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: scribe POSTGRES_USER: ${POSTGRES_USER:-grimoire}
POSTGRES_PASSWORD: "${DB_PASSWORD}" POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: scribe POSTGRES_DB: ${POSTGRES_DB:-grimoire}
networks: networks:
- scribe_backend - backend
volumes: volumes:
pgdata: pgdata:
name: ${PGDATA_VOLUME:-grimoire_pgdata}
networks: networks:
scribe_backend: backend:
driver: overlay driver: overlay
+5 -5
View File
@@ -5,11 +5,11 @@
services: services:
app: app:
image: git.fabledsword.com/bvandeusen/fabledscribe:latest image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
ports: ports:
- "5000:5000" - "5000:5000"
environment: environment:
DATABASE_URL: "postgresql+asyncpg://scribe:scribe@db:5432/scribe" DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${POSTGRES_PASSWORD:-grimoire}@db:5432/${POSTGRES_DB:-grimoire}"
# Set SECRET_KEY in the environment or a .env file beside this one. # Set SECRET_KEY in the environment or a .env file beside this one.
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}"
@@ -26,9 +26,9 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: scribe POSTGRES_USER: ${POSTGRES_USER:-grimoire}
POSTGRES_PASSWORD: scribe POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
POSTGRES_DB: scribe POSTGRES_DB: ${POSTGRES_DB:-grimoire}
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+4 -4
View File
@@ -11,7 +11,7 @@ services:
# Or a bind mount, for direct host access to the app's data: # Or a bind mount, for direct host access to the app's data:
# - ./data:/data # - ./data:/data
environment: environment:
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-scribe}:${POSTGRES_PASSWORD:-scribe}@db:5432/${POSTGRES_DB:-scribe}" DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${POSTGRES_PASSWORD:-grimoire}@db:5432/${POSTGRES_DB:-grimoire}"
SECRET_KEY: "${SECRET_KEY:-dev-secret-change-me}" SECRET_KEY: "${SECRET_KEY:-dev-secret-change-me}"
# Optional: a SearXNG instance to list as the web-search backend in Integrations. # Optional: a SearXNG instance to list as the web-search backend in Integrations.
# SEARXNG_URL: "http://searxng:8080" # SEARXNG_URL: "http://searxng:8080"
@@ -24,9 +24,9 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: ${POSTGRES_USER:-scribe} POSTGRES_USER: ${POSTGRES_USER:-grimoire}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-scribe} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
POSTGRES_DB: ${POSTGRES_DB:-scribe} POSTGRES_DB: ${POSTGRES_DB:-grimoire}
volumes: volumes:
pgdata: pgdata:
+3 -3
View File
@@ -35,7 +35,7 @@ Click **Revoke** next to the key in the keys table and confirm. Revoked keys are
Grimoire exposes itself as a set of MCP tools that Claude (and other MCP clients) Grimoire exposes itself as a set of MCP tools that Claude (and other MCP clients)
can use to read and write your notes, tasks, projects, rulebooks, and more. The can use to read and write your notes, tasks, projects, rulebooks, and more. The
server is **built into the app** — it is mounted as a streamable-HTTP endpoint server is **built into the app** — it is mounted as a streamable-HTTP endpoint
at **`/mcp`** on the running Grimoire instance (`src/scribe/mcp/server.py`). There at **`/mcp`** on the running Grimoire instance (`src/grimoire/mcp/server.py`). There
is nothing to install: no wheel, no separate package, no CLI. You connect a is nothing to install: no wheel, no separate package, no CLI. You connect a
client straight to the URL with a Bearer token. client straight to the URL with a Bearer token.
@@ -90,7 +90,7 @@ specific project at a dev instance or an admin key.
### Available Tools ### Available Tools
There are well over a hundred tools and they change with the app, so the There are well over a hundred tools and they change with the app, so the
registration in **`src/scribe/mcp/tools/`** is the reference. By family: registration in **`src/grimoire/mcp/tools/`** is the reference. By family:
| Family | Examples | Purpose | | Family | Examples | Purpose |
|--------|----------|---------| |--------|----------|---------|
@@ -111,4 +111,4 @@ registration in **`src/scribe/mcp/tools/`** is the reference. By family:
Server-level usage guidance — when to reach for each entity, the Server-level usage guidance — when to reach for each entity, the
recall-before-acting reflex, and the rulebook conventions — is delivered to the recall-before-acting reflex, and the rulebook conventions — is delivered to the
client automatically via the MCP server's `instructions` block (defined in client automatically via the MCP server's `instructions` block (defined in
`src/scribe/mcp/server.py`). `src/grimoire/mcp/server.py`).
+1 -1
View File
@@ -294,7 +294,7 @@ These serve the plugin's hooks.
## Grimoire MCP ## Grimoire MCP
Claude clients connect to the built-in MCP server at `POST /mcp` (streamable HTTP, Claude clients connect to the built-in MCP server at `POST /mcp` (streamable HTTP,
Bearer auth with an `fmcp_` key), served by `src/scribe/mcp/`. It is not a REST Bearer auth with an `fmcp_` key), served by `src/grimoire/mcp/`. It is not a REST
surface — it exposes the same data as typed tools (`create_note`, `create_task`, surface — it exposes the same data as typed tools (`create_note`, `create_task`,
`start_planning`, `search`, `enter_project`, …) with `start_planning`, `search`, `enter_project`, …) with
server-level usage guidance delivered in the MCP `instructions` block. See server-level usage guidance delivered in the MCP `instructions` block. See
+5 -5
View File
@@ -24,10 +24,10 @@ Grimoire is one app container and one PostgreSQL container. There is no language
``` ```
- **The web UI** is a Vue single-page app, built into the image and served by Quart. Every unknown path that isn't under `/api/` falls back to `index.html`; `/api/*` always answers JSON. - **The web UI** is a Vue single-page app, built into the image and served by Quart. Every unknown path that isn't under `/api/` falls back to `index.html`; `/api/*` always answers JSON.
- **The REST API** (`src/scribe/routes/`) backs the UI, one blueprint per resource. - **The REST API** (`src/grimoire/routes/`) backs the UI, one blueprint per resource.
- **The MCP server** (`src/scribe/mcp/`) is how Claude reads and writes records. Each module in `mcp/tools/` registers its tools, and the server's instructions and each tool's docstring are the contract an agent reads. Requests authenticate with an API key as a Bearer token. - **The MCP server** (`src/grimoire/mcp/`) is how Claude reads and writes records. Each module in `mcp/tools/` registers its tools, and the server's instructions and each tool's docstring are the contract an agent reads. Requests authenticate with an API key as a Bearer token.
- **The plugin** (`plugin/`) runs in the user's Claude Code. Its hooks call `/api/plugin/*` to put the right context in front of Claude at the right moment: the project at session start, matching rules and prior art before a prompt or a tool call, the rules mounted on a moment of work, and a check on the reply that ends a turn. Its skills describe the working practices. See [API Keys & MCP](api-keys-and-mcp.md) to install it. - **The plugin** (`plugin/`) runs in the user's Claude Code. Its hooks call `/api/plugin/*` to put the right context in front of Claude at the right moment: the project at session start, matching rules and prior art before a prompt or a tool call, the rules mounted on a moment of work, and a check on the reply that ends a turn. Its skills describe the working practices. See [API Keys & MCP](api-keys-and-mcp.md) to install it.
- **Services** (`src/scribe/services/`) hold the logic both doors share. Routes and MCP tools stay thin: parse the request, check access, call a service, return a plain dict. - **Services** (`src/grimoire/services/`) hold the logic both doors share. Routes and MCP tools stay thin: parse the request, check access, call a service, return a plain dict.
## Data model ## Data model
@@ -80,14 +80,14 @@ Credentials Grimoire stores for outside services (forge tokens, the backup desti
``` ```
├── alembic/versions/ database migrations ├── alembic/versions/ database migrations
├── src/scribe/ ├── src/grimoire/
│ ├── app.py app factory, blueprints, startup and shutdown │ ├── app.py app factory, blueprints, startup and shutdown
│ ├── config.py environment configuration │ ├── config.py environment configuration
│ ├── models/ SQLAlchemy models │ ├── models/ SQLAlchemy models
│ ├── routes/ REST blueprints │ ├── routes/ REST blueprints
│ ├── mcp/ MCP server and its tool modules │ ├── mcp/ MCP server and its tool modules
│ ├── services/ shared logic │ ├── services/ shared logic
│ └── restore.py `python -m scribe.restore` for a fresh instance │ └── restore.py `python -m grimoire.restore` for a fresh instance
├── frontend/src/ views, components, stores, composables, api client ├── frontend/src/ views, components, stores, composables, api client
├── plugin/ Claude Code hooks, skills and commands ├── plugin/ Claude Code hooks, skills and commands
├── scripts/ CI checks and the plugin version minter ├── scripts/ CI checks and the plugin version minter
+11 -8
View File
@@ -11,9 +11,12 @@ Set it up under **Settings → Data → Scheduled backups** (admins only).
One file per run, named for the moment it was taken in UTC: One file per run, named for the moment it was taken in UTC:
``` ```
scribe-20261015T020000Z.tar grimoire-20261015T020000Z.tar
``` ```
Backups written while the app was called Scribe are named `scribe-…tar`.
They are counted with the rest and restore the same way.
It is a plain, **uncompressed** tar: It is a plain, **uncompressed** tar:
- `manifest.json` comes first. It holds the backup format version, the Grimoire - `manifest.json` comes first. It holds the backup format version, the Grimoire
@@ -65,12 +68,12 @@ A path **inside the Grimoire container**, normally a share mounted there:
```yaml ```yaml
services: services:
scribe: grimoire:
volumes: volumes:
- /nfs/backup/scribe:/backups - /nfs/backup/grimoire:/backups
``` ```
Grimoire writes each file under a hidden name (`.scribe-….tar.part`) and renames Grimoire writes each file under a hidden name (`.grimoire-….tar.part`) and renames
it once it is complete. A program watching the folder never sees half a it once it is complete. A program watching the folder never sees half a
backup. backup.
@@ -115,7 +118,7 @@ page asks for the secret again.
[Fabled Archives](https://git.fabledsword.com/bvandeusen/fabledarchives) [Fabled Archives](https://git.fabledsword.com/bvandeusen/fabledarchives)
keeps service backups as "backup targets". Either destination works with it. keeps service backups as "backup targets". Either destination works with it.
1. On Archives' **Backups** page, make a target for Grimoire, named `scribe` 1. On Archives' **Backups** page, make a target for Grimoire, named `grimoire`
for example. for example.
2. Choose how Grimoire's files arrive: 2. Choose how Grimoire's files arrive:
- **S3**: turn on the S3 way in for the target and make an **append-only** - **S3**: turn on the S3 way in for the target and make an **append-only**
@@ -124,8 +127,8 @@ keeps service backups as "backup targets". Either destination works with it.
- bucket: the target's name; - bucket: the target's name;
- access key and secret key: the key you just made. - access key and secret key: the key you just made.
- **A folder**: mount the share Archives watches into Grimoire as - **A folder**: mount the share Archives watches into Grimoire as
`/backups/scribe`, and point the target at that folder. In Grimoire, choose `/backups/grimoire`, and point the target at that folder. In Grimoire, choose
**A folder** with `/backups/scribe`. **A folder** with `/backups/grimoire`.
3. Set the target's **Retention** to **Archives**. Archives then keeps the 3. Set the target's **Retention** to **Archives**. Archives then keeps the
newest backups and thins the rest by day, week and month. Grimoire only adds. newest backups and thins the rest by day, week and month. Grimoire only adds.
4. Give the target an expected cadence of **daily**, so a missed backup turns 4. Give the target an expected cadence of **daily**, so a missed backup turns
@@ -141,7 +144,7 @@ Start Grimoire on an empty database, copy the backup file into the container,
and run: and run:
```sh ```sh
docker exec -it scribe python -m scribe.restore /backups/scribe-20261015T020000Z.tar docker exec -it grimoire python -m grimoire.restore /backups/grimoire-20261015T020000Z.tar
``` ```
- The command refuses a database that already has an account. A restore - The command refuses a database that already has an account. A restore
+5 -3
View File
@@ -2,7 +2,7 @@
Grimoire reads its configuration from environment variables. Most settings people change day to day (registration, SMTP, retrieval thresholds, backups, forge connections) live in **Settings** in the web UI instead. Grimoire reads its configuration from environment variables. Most settings people change day to day (registration, SMTP, retrieval thresholds, backups, forge connections) live in **Settings** in the web UI instead.
Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to the path, for example a Docker secret at `/run/secrets/scribe_key`. Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to the path, for example a Docker secret at `/run/secrets/grimoire_key`.
## Environment variables ## Environment variables
@@ -10,7 +10,7 @@ Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `DATABASE_URL` *(secret)* | `postgresql+asyncpg://scribe:scribe@localhost:5432/scribe` | PostgreSQL connection string, using the `asyncpg` driver | | `DATABASE_URL` *(secret)* | `postgresql+asyncpg://grimoire:grimoire@localhost:5432/grimoire` | PostgreSQL connection string, using the `asyncpg` driver |
| `SECRET_KEY` *(secret)* | `dev-secret-change-me` | Signs sessions and seals stored credentials. Set it before first use and keep it (see below) | | `SECRET_KEY` *(secret)* | `dev-secret-change-me` | Signs sessions and seals stored credentials. Set it before first use and keep it (see below) |
| `BASE_URL` | `http://localhost:5000` | Public URL, used in email links and OIDC redirects | | `BASE_URL` | `http://localhost:5000` | Public URL, used in email links and OIDC redirects |
| `SECURE_COOKIES` | `false` | `true` when Grimoire is served over HTTPS | | `SECURE_COOKIES` | `false` | `true` when Grimoire is served over HTTPS |
@@ -79,9 +79,11 @@ docker compose up --build
[`docker-compose.prod.yml`](../docker-compose.prod.yml) is a stack file: the app and database on a private overlay network, with `SECURE_COOKIES` and `TRUST_PROXY_HEADERS` on. Provide `DB_PASSWORD` and `SECRET_KEY` in the environment: [`docker-compose.prod.yml`](../docker-compose.prod.yml) is a stack file: the app and database on a private overlay network, with `SECURE_COOKIES` and `TRUST_PROXY_HEADERS` on. Provide `DB_PASSWORD` and `SECRET_KEY` in the environment:
```bash ```bash
DB_PASSWORD=... SECRET_KEY=... docker stack deploy -c docker-compose.prod.yml scribe DB_PASSWORD=... SECRET_KEY=... docker stack deploy -c docker-compose.prod.yml grimoire
``` ```
The database user, database and volume default to `grimoire`. An install created under the name Scribe keeps its data by setting `POSTGRES_USER`, `POSTGRES_DB` and `PGDATA_VOLUME` to the names it was created with.
The shipped files leave out healthchecks and restart policies on purpose. The app waits for its database on startup, and the orchestrator's defaults restart it if it ever exits. The shipped files leave out healthchecks and restart policies on purpose. The app waits for its database on startup, and the orchestrator's defaults restart it if it ever exits.
### Volumes ### Volumes
+1 -1
View File
@@ -65,7 +65,7 @@ Work happens on `dev`. A release merges `dev` into `main` with a plain merge com
### Backend ### Backend
- Services open their own session: `async with async_session() as session:` (from `scribe.models`). - Services open their own session: `async with async_session() as session:` (from `grimoire.models`).
- Routes and MCP tools stay thin; the logic lives in `services/`, shared by both. - Routes and MCP tools stay thin; the logic lives in `services/`, shared by both.
- Access checks go through `services/access.py`, never inline ownership checks. - Access checks go through `services/access.py`, never inline ownership checks.
- Every wait on something outside the process has a deadline. - Every wait on something outside the process has a deadline.
+1 -1
View File
@@ -86,7 +86,7 @@ Share a project, note or task with users or groups, as viewer, editor or admin.
- **Personal export**: your notes and tasks as a Markdown ZIP (with YAML frontmatter) or JSON. - **Personal export**: your notes and tasks as a Markdown ZIP (with YAML frontmatter) or JSON.
- **Admin backup and restore**, with ids remapped so a backup restores onto another instance. - **Admin backup and restore**, with ids remapped so a backup restores onto another instance.
- **Scheduled backups**: a daily, dated, uncompressed tar to a folder or an S3-compatible bucket, laid out so a deduplicating tool such as restic stores only what changed. `python -m scribe.restore` brings a fresh instance back from one. See [Backups](backups.md). - **Scheduled backups**: a daily, dated, uncompressed tar to a folder or an S3-compatible bucket, laid out so a deduplicating tool such as restic stores only what changed. `python -m grimoire.restore` brings a fresh instance back from one. See [Backups](backups.md).
## Sign-in ## Sign-in
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "scribe-frontend", "name": "grimoire-frontend",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "scribe-frontend", "name": "grimoire-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fullcalendar/core": "^6.1.20", "@fullcalendar/core": "^6.1.20",
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "scribe-frontend", "name": "grimoire-frontend",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
+1 -1
View File
@@ -2,7 +2,7 @@ import { apiGet } from "./client";
/** /**
* What `/api/version` answers — the client's half of `build_version_payload` * What `/api/version` answers — the client's half of `build_version_payload`
* (`src/scribe/routes/api.py`), which is where the reasoning for the shape is * (`src/grimoire/routes/api.py`), which is where the reasoning for the shape is
* written down. * written down.
* *
* EVERY FIELD BUT `version` IS OPTIONAL, and an absent one means "this build * EVERY FIELD BUT `version` IS OPTIONAL, and an absent one means "this build
@@ -187,7 +187,7 @@ async function backUpNow() {
</ul> </ul>
<p class="intro"> <p class="intro">
Each run adds one dated file, <code>scribe-&lt;date&gt;.tar</code>, holding every table Each run adds one dated file, <code>grimoire-&lt;date&gt;.tar</code>, holding every table
as plain lines. It is not compressed, so a deduplicating tool such as restic stores only what as plain lines. It is not compressed, so a deduplicating tool such as restic stores only what
changed since the day before. Grimoire never deletes old backups — the destination keeps and changed since the day before. Grimoire never deletes old backups — the destination keeps and
thins the history. Sharing, API keys and forge connections are not included. thins the history. Sharing, API keys and forge connections are not included.
@@ -237,7 +237,7 @@ async function backUpNow() {
</label> </label>
<label class="field"> <label class="field">
<span>Bucket</span> <span>Bucket</span>
<input v-model.trim="form.s3_bucket" class="fs-input" placeholder="scribe" /> <input v-model.trim="form.s3_bucket" class="fs-input" placeholder="grimoire" />
</label> </label>
<label class="field"> <label class="field">
<span>Access key</span> <span>Access key</span>
+3 -3
View File
@@ -1068,7 +1068,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 = `scribe-backup-${scope}-${new Date().toISOString().slice(0, 10)}.json`; a.download = `grimoire-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");
@@ -1091,7 +1091,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 = `scribe-${stamp}.${ext}`; a.download = `grimoire-${stamp}.${ext}`;
a.click(); a.click();
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
toastStore.show("Export downloaded"); toastStore.show("Export downloaded");
@@ -1319,7 +1319,7 @@ async function handleRestoreFile(event: Event) {
if (res.status === 413) { if (res.status === 413) {
throw new Error( throw new Error(
"the file is bigger than the web upload allows. Restore it with " + "the file is bigger than the web upload allows. Restore it with " +
"`python -m scribe.restore <file>` inside the Grimoire container instead.", "`python -m grimoire.restore <file>` inside the Grimoire container instead.",
); );
} }
if (!res.ok) { if (!res.ok) {
+1 -1
View File
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "scribe" name = "grimoire"
version = "0.1.0" version = "0.1.0"
description = "Self-hosted note-taking, project-management, and calendar app exposed to Claude via MCP" description = "Self-hosted note-taking, project-management, and calendar app exposed to Claude via MCP"
requires-python = ">=3.14" requires-python = ">=3.14"
+1 -1
View File
@@ -103,7 +103,7 @@ async def run(args) -> None:
print(f" - {r['title']}: {preview}{empty_flag}") print(f" - {r['title']}: {preview}{empty_flag}")
return return
from scribe.services import rulebooks as rb_service from grimoire.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:
+52 -52
View File
@@ -5,38 +5,38 @@ 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 scribe.config import Config from grimoire.config import Config
from scribe.routes.admin import admin_bp from grimoire.routes.admin import admin_bp
from scribe.routes.api import api from grimoire.routes.api import api
from scribe.routes.auth import auth_bp from grimoire.routes.auth import auth_bp
from scribe.routes.export import export_bp from grimoire.routes.export import export_bp
from scribe.routes.notes import notes_bp from grimoire.routes.notes import notes_bp
from scribe.routes.milestones import milestones_bp from grimoire.routes.milestones import milestones_bp
from scribe.routes.task_logs import task_logs_bp from grimoire.routes.task_logs import task_logs_bp
from scribe.routes.projects import projects_bp from grimoire.routes.projects import projects_bp
from scribe.routes.retrieval import retrieval_bp from grimoire.routes.retrieval import retrieval_bp
from scribe.routes.settings import settings_bp from grimoire.routes.settings import settings_bp
from scribe.routes.tasks import tasks_bp from grimoire.routes.tasks import tasks_bp
from scribe.routes.groups import groups_bp from grimoire.routes.groups import groups_bp
from scribe.routes.shares import shares_bp from grimoire.routes.shares import shares_bp
from scribe.routes.in_app_notifications import notifications_bp from grimoire.routes.in_app_notifications import notifications_bp
from scribe.routes.users import users_bp from grimoire.routes.users import users_bp
from scribe.routes.api_keys import api_keys_bp from grimoire.routes.api_keys import api_keys_bp
from scribe.routes.search import search_bp from grimoire.routes.search import search_bp
from scribe.routes.knowledge import knowledge_bp from grimoire.routes.knowledge import knowledge_bp
from scribe.routes.rulebooks import rulebooks_bp from grimoire.routes.rulebooks import rulebooks_bp
from scribe.routes.plugin import plugin_bp from grimoire.routes.plugin import plugin_bp
from scribe.routes.design_systems import design_systems_bp from grimoire.routes.design_systems import design_systems_bp
from scribe.routes.trash import trash_bp from grimoire.routes.trash import trash_bp
from scribe.routes.dashboard import dashboard_bp from grimoire.routes.dashboard import dashboard_bp
from scribe.routes.systems import systems_bp from grimoire.routes.systems import systems_bp
from scribe.routes.canonical_systems import canonical_systems_bp from grimoire.routes.canonical_systems import canonical_systems_bp
from scribe.routes.platforms import platforms_bp from grimoire.routes.platforms import platforms_bp
from scribe.routes.family import family_bp from grimoire.routes.family import family_bp
from scribe.routes.lessons import lessons_bp from grimoire.routes.lessons import lessons_bp
from scribe.routes.snippets import snippets_bp from grimoire.routes.snippets import snippets_bp
from scribe.routes.webhooks import webhooks_bp from grimoire.routes.webhooks import webhooks_bp
from scribe.mcp import mount_mcp from grimoire.mcp import mount_mcp
STATIC_DIR = Path(__file__).parent / "static" STATIC_DIR = Path(__file__).parent / "static"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -131,7 +131,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 scribe.services.logging import log_usage from grimoire.services.logging import log_usage
user = getattr(g, "user", None) user = getattr(g, "user", None)
await log_usage( await log_usage(
@@ -173,13 +173,13 @@ def create_app() -> Quart:
# `create_app`, which runs before either is true. # `create_app`, which runs before either is true.
_startup_finished = asyncio.Event() _startup_finished = asyncio.Event()
from scribe.services.auth import start_auth_token_retention_loop from grimoire.services.auth import start_auth_token_retention_loop
from scribe.services.embeddings import ( from grimoire.services.embeddings import (
backfill_milestone_embeddings, backfill_note_embeddings, backfill_milestone_embeddings, backfill_note_embeddings,
backfill_rule_embeddings, backfill_system_embeddings, backfill_rule_embeddings, backfill_system_embeddings,
) )
from scribe.services.logging import start_log_retention_loop from grimoire.services.logging import start_log_retention_loop
from scribe.services.notifications import start_notification_loop from grimoire.services.notifications import start_notification_loop
start_log_retention_loop() start_log_retention_loop()
start_notification_loop() start_notification_loop()
@@ -200,7 +200,7 @@ def create_app() -> Quart:
# answer this exists to stop giving. Failure is logged and swallowed — # answer this exists to stop giving. Failure is logged and swallowed —
# provenance is worth a lot and never worth refusing to boot. # provenance is worth a lot and never worth refusing to boot.
try: try:
from scribe.services.retrieval_tuning import record_release_defaults from grimoire.services.retrieval_tuning import record_release_defaults
moved = await record_release_defaults() moved = await record_release_defaults()
for row in moved: for row in moved:
if not row["baseline"]: if not row["baseline"]:
@@ -255,7 +255,7 @@ def create_app() -> Quart:
# row would read as "no snippet here" rather than as a gap. Separate # row would read as "no snippet here" rather than as a gap. Separate
# try block so neither backfill can skip the other. # try block so neither backfill can skip the other.
try: try:
from scribe.services.snippets import backfill_snippet_data from grimoire.services.snippets import backfill_snippet_data
await backfill_snippet_data() await backfill_snippet_data()
except Exception: except Exception:
logger.warning("Snippet data backfill failed", exc_info=True) logger.warning("Snippet data backfill failed", exc_info=True)
@@ -272,17 +272,17 @@ def create_app() -> Quart:
# living on as a coroutine nobody will ever wake. # living on as a coroutine nobody will ever wake.
try: try:
# Recurrence scheduler (recurring-task spawn every 15m) # Recurrence scheduler (recurring-task spawn every 15m)
from scribe.services.recurrence_scheduler import start_recurrence_scheduler from grimoire.services.recurrence_scheduler import start_recurrence_scheduler
start_recurrence_scheduler(asyncio.get_running_loop()) start_recurrence_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 scribe.services.version_pinning_scheduler import ( from grimoire.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 scribe.services.trash_scheduler import start_trash_scheduler from grimoire.services.trash_scheduler import start_trash_scheduler
start_trash_scheduler(asyncio.get_running_loop()) start_trash_scheduler(asyncio.get_running_loop())
# DB maintenance scheduler (daily targeted VACUUM ANALYZE, default # DB maintenance scheduler (daily targeted VACUUM ANALYZE, default
@@ -291,7 +291,7 @@ def create_app() -> Quart:
# comment above `_STARTUP_READ_TIMEOUT`. Anything added here that # comment above `_STARTUP_READ_TIMEOUT`. Anything added here that
# touches the database needs the same treatment: a lifespan hook # touches the database needs the same treatment: a lifespan hook
# that does not return is a worker that never serves. # that does not return is a worker that never serves.
from scribe.services.db_maintenance_scheduler import ( from grimoire.services.db_maintenance_scheduler import (
get_maintenance_hour, get_maintenance_hour,
start_db_maintenance_scheduler, start_db_maintenance_scheduler,
) )
@@ -301,13 +301,13 @@ def create_app() -> Quart:
# Scheduled backups. An hourly tick that reads its config when it # Scheduled backups. An hourly tick that reads its config when it
# fires, so starting it reads nothing here. # fires, so starting it reads nothing here.
from scribe.services.backup_job import start_backup_scheduler from grimoire.services.backup_job import start_backup_scheduler
start_backup_scheduler(asyncio.get_running_loop()) start_backup_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 scribe.services.diagnostics import start_diagnostics from grimoire.services.diagnostics import start_diagnostics
start_diagnostics(asyncio.get_running_loop()) start_diagnostics(asyncio.get_running_loop())
finally: finally:
# STARTUP IS OVER — release the backfill. Everything above # STARTUP IS OVER — release the backfill. Everything above
@@ -318,21 +318,21 @@ def create_app() -> Quart:
@app.after_serving @app.after_serving
async def shutdown(): async def shutdown():
from scribe.services.recurrence_scheduler import stop_recurrence_scheduler from grimoire.services.recurrence_scheduler import stop_recurrence_scheduler
stop_recurrence_scheduler() stop_recurrence_scheduler()
from scribe.services.version_pinning_scheduler import ( from grimoire.services.version_pinning_scheduler import (
stop_version_pinning_scheduler, stop_version_pinning_scheduler,
) )
stop_version_pinning_scheduler() stop_version_pinning_scheduler()
from scribe.services.trash_scheduler import stop_trash_scheduler from grimoire.services.trash_scheduler import stop_trash_scheduler
stop_trash_scheduler() stop_trash_scheduler()
from scribe.services.db_maintenance_scheduler import ( from grimoire.services.db_maintenance_scheduler import (
stop_db_maintenance_scheduler, stop_db_maintenance_scheduler,
) )
stop_db_maintenance_scheduler() stop_db_maintenance_scheduler()
from scribe.services.backup_job import stop_backup_scheduler from grimoire.services.backup_job import stop_backup_scheduler
stop_backup_scheduler() stop_backup_scheduler()
from scribe.services.diagnostics import stop_diagnostics from grimoire.services.diagnostics import stop_diagnostics
stop_diagnostics() stop_diagnostics()
@app.route("/") @app.route("/")
@@ -382,7 +382,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 scribe.services.logging import log_error from grimoire.services.logging import log_error
user = getattr(g, "user", None) user = getattr(g, "user", None)
await log_error( await log_error(
+2 -2
View File
@@ -2,8 +2,8 @@ import functools
from quart import g, jsonify, request, session from quart import g, jsonify, request, session
from scribe.services.auth import get_user_by_id from grimoire.services.auth import get_user_by_id
from scribe.services.api_keys import lookup_key from grimoire.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://scribe:scribe@localhost:5432/scribe", "postgresql+asyncpg://grimoire:grimoire@localhost:5432/grimoire",
) )
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 scribe.mcp.server import build_mcp_server, mount_mcp from grimoire.mcp.server import build_mcp_server, mount_mcp
__all__ = ["build_mcp_server", "mount_mcp"] __all__ = ["build_mcp_server", "mount_mcp"]
@@ -1,7 +1,7 @@
"""Per-request MCP context. """Per-request MCP context.
The ASGI middleware (mcp/server.py) populates `_user_id_ctx` from The ASGI middleware (mcp/server.py) populates `_user_id_ctx` from
scope['scribe_user_id'] before dispatching to MCPServer tool handlers. scope['grimoire_user_id'] before dispatching to MCPServer tool handlers.
Tool functions then call `current_user_id()` to retrieve it. Tool functions then call `current_user_id()` to retrieve it.
Tools must never fall back to a default user — `current_user_id()` raises Tools must never fall back to a default user — `current_user_id()` raises
@@ -9,7 +9,7 @@ if called outside a properly-authed MCP request.
""" """
from contextvars import ContextVar from contextvars import ContextVar
_user_id_ctx: ContextVar[int | None] = ContextVar("scribe_mcp_user_id", default=None) _user_id_ctx: ContextVar[int | None] = ContextVar("grimoire_mcp_user_id", default=None)
def current_user_id() -> int: def current_user_id() -> int:
@@ -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 scribe.services.api_keys import lookup_key from grimoire.services.api_keys import lookup_key
@@ -354,7 +354,7 @@ def build_mcp_server() -> MCPServer:
here: since SDK v2 they belong to the ASGI app, built in `mount_mcp`. here: since SDK v2 they belong to the ASGI app, built in `mount_mcp`.
""" """
mcp = StrictArgsMCPServer("scribe", instructions=_INSTRUCTIONS.strip()) mcp = StrictArgsMCPServer("scribe", instructions=_INSTRUCTIONS.strip())
from scribe.mcp.tools import register_all from grimoire.mcp.tools import register_all
register_all(mcp) register_all(mcp)
return mcp return mcp
@@ -364,7 +364,7 @@ def mount_mcp(app: Quart) -> None:
A small ASGI middleware between Quart and the MCPServer sub-app validates the A small ASGI middleware between Quart and the MCPServer sub-app validates the
Bearer token against the api_keys table. Authenticated requests have their Bearer token against the api_keys table. Authenticated requests have their
user_id attached to the ASGI scope under "scribe_user_id" for tool handlers user_id attached to the ASGI scope under "grimoire_user_id" for tool handlers
to read. to read.
The SDK's streamable_http session manager owns a task group that must be The SDK's streamable_http session manager owns a task group that must be
@@ -373,7 +373,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 scribe.mcp.auth import resolve_bearer from grimoire.mcp.auth import resolve_bearer
mcp = build_mcp_server() mcp = build_mcp_server()
# stateless_http=True: don't hand the client a persistent Mcp-Session-Id. # stateless_http=True: don't hand the client a persistent Mcp-Session-Id.
@@ -427,7 +427,7 @@ def mount_mcp(app: Quart) -> None:
"status": 401, "status": 401,
"headers": [ "headers": [
(b"content-type", b"application/json"), (b"content-type", b"application/json"),
(b"www-authenticate", b'Bearer realm="scribe-mcp"'), (b"www-authenticate", b'Bearer realm="grimoire-mcp"'),
], ],
}) })
await send({ await send({
@@ -456,8 +456,8 @@ def mount_mcp(app: Quart) -> None:
}) })
return return
scope["scribe_user_id"] = user_id scope["grimoire_user_id"] = user_id
from scribe.mcp._context import _user_id_ctx from grimoire.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 an MCPServer instance. `register_all(mcp)` is the single entry point called to an MCPServer instance. `register_all(mcp)` is the single entry point called
from `mcp.server.build_mcp_server`. from `mcp.server.build_mcp_server`.
""" """
from scribe.mcp.tools import ( from grimoire.mcp.tools import (
design_systems, family, lessons, milestones, notes, processes, projects, recent, repos, design_systems, family, lessons, milestones, notes, processes, projects, recent, repos,
moments, platforms, retrieval_review, retrieval_tuning, moments, platforms, retrieval_review, retrieval_tuning,
wide_net, wide_net,
@@ -19,10 +19,10 @@ Sentinels, matching the milestone/task tool conventions:
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import design_systems as ds_svc from grimoire.services import design_systems as ds_svc
from scribe.services.design_systems import DesignSystemCycle from grimoire.services.design_systems import DesignSystemCycle
from scribe.services.design_starter_roles import ( from grimoire.services.design_starter_roles import (
ALL_GROUPS, ALL_GROUPS,
DEFAULT_TOKEN_PREFIX, DEFAULT_TOKEN_PREFIX,
describe_groups, describe_groups,
@@ -6,9 +6,9 @@ and enforced by the service, and every decision is logged with its reason.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import family as family_svc from grimoire.services import family as family_svc
from scribe.services import family_adoption as adoption_svc from grimoire.services import family_adoption as adoption_svc
async def list_family_ideas( async def list_family_ideas(
@@ -9,17 +9,17 @@ would be something the writer had to know to include.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import access as access_svc from grimoire.services import access as access_svc
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc from grimoire.services import knowledge as knowledge_svc
from scribe.services import lesson_rules as lesson_rules_svc from grimoire.services import lesson_rules as lesson_rules_svc
from scribe.services import lessons as lessons_svc from grimoire.services import lessons as lessons_svc
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.mcp.tools import systems as systems_tools from grimoire.mcp.tools import systems as systems_tools
from scribe.services.note_usage import attach_usage, record_pulled from grimoire.services.note_usage import attach_usage, record_pulled
from scribe.services import moment_delivery from grimoire.services import moment_delivery
# The payload shape lives in the service (`lesson_to_dict`), shared with the # The payload shape lives in the service (`lesson_to_dict`), shared with the
@@ -11,16 +11,16 @@ Sentinels:
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import family as family_svc from grimoire.services import family as family_svc
from scribe.services import milestones as milestones_svc from grimoire.services import milestones as milestones_svc
from scribe.services import notes as notes_svc from grimoire.services import notes as notes_svc
from scribe.services import task_logs as task_logs_svc from grimoire.services import task_logs as task_logs_svc
from scribe.services import rulebooks as rulebooks_svc from grimoire.services import rulebooks as rulebooks_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.services.record_refs import refuse_guessed_ids from grimoire.services.record_refs import refuse_guessed_ids
from scribe.services import moment_delivery from grimoire.services import moment_delivery
async def list_milestones(project_id: int) -> dict: async def list_milestones(project_id: int) -> dict:
@@ -12,11 +12,11 @@ operator's yes.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import moment_actions as actions_svc from grimoire.services import moment_actions as actions_svc
from scribe.services import moments as moments_svc from grimoire.services import moments as moments_svc
from scribe.services import reply_shapes as shapes_svc from grimoire.services import reply_shapes as shapes_svc
from scribe.services import rule_moment_judgments as judgments_svc from grimoire.services import rule_moment_judgments as judgments_svc
async def list_moments() -> dict: async def list_moments() -> dict:
@@ -13,18 +13,18 @@ Sentinel conventions (inherited from existing fable-mcp tools):
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools from grimoire.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc from grimoire.services import access as access_svc
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import family as family_svc from grimoire.services import family as family_svc
from scribe.services import notes as notes_svc from grimoire.services import notes as notes_svc
from scribe.services import supersession as supersession_svc from grimoire.services import supersession as supersession_svc
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.services.note_usage import record_pulled from grimoire.services.note_usage import record_pulled
from scribe.services.record_refs import refuse_guessed_ids from grimoire.services.record_refs import refuse_guessed_ids
from scribe.services import moment_delivery from grimoire.services import moment_delivery
async def list_notes( async def list_notes(
@@ -5,8 +5,8 @@ platforms decide which family ideas reach it.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import platforms as platforms_svc from grimoire.services import platforms as platforms_svc
async def list_platforms(project_id: int = 0) -> dict: async def list_platforms(project_id: int = 0) -> dict:
@@ -6,17 +6,17 @@ get_process is the fire mechanism: it returns the full prompt for Claude to run.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import access as access_svc from grimoire.services import access as access_svc
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc from grimoire.services import knowledge as knowledge_svc
from scribe.services import moment_actions from grimoire.services import moment_actions
from scribe.services import moments as moments_svc from grimoire.services import moments as moments_svc
from scribe.services import notes as notes_svc from grimoire.services import notes as notes_svc
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.services.note_usage import record_pulled from grimoire.services.note_usage import record_pulled
from scribe.services import moment_delivery from grimoire.services import moment_delivery
async def list_processes( async def list_processes(
@@ -9,22 +9,22 @@ from __future__ import annotations
import logging import logging
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools from grimoire.mcp.tools import systems as systems_tools
from scribe.services import coverage as coverage_svc from grimoire.services import coverage as coverage_svc
from scribe.services import design_systems as design_systems_svc from grimoire.services import design_systems as design_systems_svc
from scribe.services import family_adoption as family_adoption_svc from grimoire.services import family_adoption as family_adoption_svc
from scribe.services import inception as inception_svc from grimoire.services import inception as inception_svc
from scribe.services import milestones as milestones_svc from grimoire.services import milestones as milestones_svc
from scribe.services import notes as notes_svc from grimoire.services import notes as notes_svc
from scribe.services import platforms as platforms_svc from grimoire.services import platforms as platforms_svc
from scribe.services import projects as projects_svc from grimoire.services import projects as projects_svc
from scribe.services import reply_shapes as reply_shapes_svc from grimoire.services import reply_shapes as reply_shapes_svc
from scribe.services import rulebooks as rulebooks_svc from grimoire.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.services.background import spawn from grimoire.services.background import spawn
from scribe.services.note_usage import record_surfaced from grimoire.services.note_usage import record_surfaced
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -13,10 +13,10 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select from sqlalchemy import select
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.models import async_session from grimoire.models import async_session
from scribe.models.note import Note from grimoire.models.note import Note
from scribe.models.project import Project from grimoire.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:
@@ -8,9 +8,9 @@ it survives re-clones and ssh/https URL differences.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import projects as projects_svc from grimoire.services import projects as projects_svc
from scribe.services import repo_bindings as repo_bindings_svc from grimoire.services import repo_bindings as repo_bindings_svc
async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict: async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict:
@@ -4,8 +4,8 @@
not. These two say whether the line RELATED, which is a reading only someone not. These two say whether the line RELATED, which is a reading only someone
looking at the query beside the line can do. looking at the query beside the line can do.
""" """
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import retrieval_review as review_svc from grimoire.services import retrieval_review as review_svc
async def menus_to_review( async def menus_to_review(
@@ -4,9 +4,9 @@ The write half of the loop `retrieval_telemetry` opens. That tool says what a
bar did; these say what the bar IS, and let it be changed with the argument bar did; these say what the bar IS, and let it be changed with the argument
attached. attached.
""" """
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import retrieval_migration as migration_svc from grimoire.services import retrieval_migration as migration_svc
from scribe.services import retrieval_tuning as tuning_svc from grimoire.services import retrieval_tuning as tuning_svc
async def retrieval_surfaces() -> dict: async def retrieval_surfaces() -> dict:
@@ -10,15 +10,15 @@ preview-style warning.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import moments as moments_svc from grimoire.services import moments as moments_svc
from scribe.services import rulebooks as rulebooks_svc from grimoire.services import rulebooks as rulebooks_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.services.rule_usage import ( from grimoire.services.rule_usage import (
record_rule_outcome, record_rule_pulled, record_rule_outcome, record_rule_pulled,
) )
from scribe.services import moment_delivery from grimoire.services import moment_delivery
# ── Rulebook CRUD ─────────────────────────────────────────────────────── # ── Rulebook CRUD ───────────────────────────────────────────────────────
@@ -9,16 +9,16 @@ from __future__ import annotations
import time import time
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services.access import owner_names_for from grimoire.services.access import owner_names_for
from scribe.services.knowledge import content_type_filters from grimoire.services.knowledge import content_type_filters
from scribe.services.text import MATCHED_PASSAGE, excerpt_fields from grimoire.services.text import MATCHED_PASSAGE, excerpt_fields
from scribe.services.embeddings import ( from grimoire.services.embeddings import (
DEFAULT_SIMILARITY_THRESHOLD, semantic_search_milestones, semantic_search_notes, DEFAULT_SIMILARITY_THRESHOLD, semantic_search_milestones, semantic_search_notes,
semantic_search_rules, semantic_search_systems, semantic_search_rules, semantic_search_systems,
) )
from scribe.services import rulebooks as rulebooks_svc from grimoire.services import rulebooks as rulebooks_svc
from scribe.services.retrieval_telemetry import record_retrieval, retrieval_summary from grimoire.services.retrieval_telemetry import record_retrieval, retrieval_summary
# A matched chunk is at most _CHUNK_CHAR_BUDGET (1400) characters, and it is # A matched chunk is at most _CHUNK_CHAR_BUDGET (1400) characters, and it is
@@ -109,7 +109,7 @@ async def _search_milestones(uid: int, q: str, limit: int, project_id: int) -> d
chunks = report.get("best_chunk") or {} chunks = report.get("best_chunk") or {}
progress: dict[int, dict] = {} progress: dict[int, dict] = {}
if raw: if raw:
from scribe.services import milestones as milestones_svc from grimoire.services import milestones as milestones_svc
for pid in {m.project_id for _s, m in raw}: for pid in {m.project_id for _s, m in raw}:
for row in await milestones_svc.get_project_milestone_summary(uid, pid): for row in await milestones_svc.get_project_milestone_summary(uid, pid):
@@ -9,9 +9,9 @@ turn that the sync has not seen yet.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import coverage as coverage_svc from grimoire.services import coverage as coverage_svc
from scribe.services import shape_ledger as shape_ledger_svc from grimoire.services import shape_ledger as shape_ledger_svc
async def classify_shapes( async def classify_shapes(
@@ -10,15 +10,15 @@ on create, System association passthrough).
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools from grimoire.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc from grimoire.services import access as access_svc
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import family as family_svc from grimoire.services import family as family_svc
from scribe.services import snippets as snippets_svc from grimoire.services import snippets as snippets_svc
from scribe.services.note_usage import attach_usage, record_pulled from grimoire.services.note_usage import attach_usage, record_pulled
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import moment_delivery from grimoire.services import moment_delivery
async def list_snippets( async def list_snippets(
@@ -288,7 +288,7 @@ async def get_snippet(snippet_id: int, project_id: int = 0) -> dict:
# The structured consumer map: ledger rows judged against this # The structured consumer map: ledger rows judged against this
# snippet. Attached only when non-empty — and never for projects # snippet. Attached only when non-empty — and never for projects
# the caller can't read. # the caller can't read.
from scribe.services import shape_ledger as shape_ledger_svc from grimoire.services import shape_ledger as shape_ledger_svc
consumers = await shape_ledger_svc.snippet_consumers(uid, int(note.id)) consumers = await shape_ledger_svc.snippet_consumers(uid, int(note.id))
if consumers["instances"]: if consumers["instances"]:
@@ -13,13 +13,13 @@ Sentinels (match the milestone/task tool conventions):
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import canonical_systems as canonical_systems_svc from grimoire.services import canonical_systems as canonical_systems_svc
from scribe.services import milestones as milestones_svc from grimoire.services import milestones as milestones_svc
from scribe.services import notes as notes_svc from grimoire.services import notes as notes_svc
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import system_usage as system_usage_svc from grimoire.services import system_usage as system_usage_svc
from scribe.services.system_usage import record_system_pulled from grimoire.services.system_usage import record_system_pulled
# Below this, a project is young enough that the mild "which area is this # Below this, a project is young enough that the mild "which area is this
# about?" question stays proportionate; at or above it, a zero-Systems project # about?" question stays proportionate; at or above it, a zero-Systems project
@@ -8,9 +8,9 @@ from __future__ import annotations
from sqlalchemy import select from sqlalchemy import select
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.models import async_session from grimoire.models import async_session
from scribe.models.note import Note from grimoire.models.note import Note
def _aggregate_tag_counts(tag_lists) -> dict[str, int]: def _aggregate_tag_counts(tag_lists) -> dict[str, int]:
@@ -18,30 +18,30 @@ Sentinels (preserved from existing fable-mcp):
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools from grimoire.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc from grimoire.services import access as access_svc
from scribe.services import dedup as dedup_svc from grimoire.services import dedup as dedup_svc
from scribe.services import family as family_svc from grimoire.services import family as family_svc
from scribe.services import family_adoption as family_adoption_svc from grimoire.services import family_adoption as family_adoption_svc
from scribe.services import milestones as milestones_svc from grimoire.services import milestones as milestones_svc
from scribe.services import notes as notes_svc from grimoire.services import notes as notes_svc
# Imported by NAME, not reached through notes_svc: minted_kind is pure # Imported by NAME, not reached through notes_svc: minted_kind is pure
# validation, not a service call, and a test that stubs the service module to # validation, not a service call, and a test that stubs the service module to
# avoid the database would otherwise stub the validation too — turning a # avoid the database would otherwise stub the validation too — turning a
# guard into a MagicMock that approves anything. # guard into a MagicMock that approves anything.
from scribe.services.notes import minted_kind from grimoire.services.notes import minted_kind
from scribe.services import placement as placement_svc from grimoire.services import placement as placement_svc
from scribe.services import planning as planning_svc from grimoire.services import planning as planning_svc
from scribe.services import record_batch as batch_svc from grimoire.services import record_batch as batch_svc
from scribe.services import rulebooks as rulebooks_svc from grimoire.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc from grimoire.services import systems as systems_svc
from scribe.services import task_logs as task_logs_svc from grimoire.services import task_logs as task_logs_svc
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
from scribe.services.note_usage import record_pulled from grimoire.services.note_usage import record_pulled
from scribe.services.record_refs import refuse_guessed_ids from grimoire.services.record_refs import refuse_guessed_ids
from scribe.services.text import elide from grimoire.services.text import elide
from scribe.services import moment_delivery from grimoire.services import moment_delivery
# A work log entry is prose, often long — the discipline asks for what was # A work log entry is prose, often long — the discipline asks for what was
@@ -6,8 +6,8 @@ the deleted_batch_id that a delete operation returns.
""" """
from __future__ import annotations from __future__ import annotations
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services import trash as trash_svc from grimoire.services import trash as trash_svc
async def list_trash() -> dict: async def list_trash() -> dict:
@@ -35,9 +35,9 @@ from __future__ import annotations
import textwrap import textwrap
import time import time
from scribe.mcp._context import current_user_id from grimoire.mcp._context import current_user_id
from scribe.services.embeddings import semantic_search_rules from grimoire.services.embeddings import semantic_search_rules
from scribe.services.retrieval_telemetry import record_retrieval from grimoire.services.retrieval_telemetry import record_retrieval
# The telemetry `source`, and a PULL — never added to AMBIENT_SOURCES (those # The telemetry `source`, and a PULL — never added to AMBIENT_SOURCES (those
# are deliveries nobody chose) and never confused with a push arm. Keeping it # are deliveries nobody chose) and never confused with a push arm. Keeping it
@@ -1,7 +1,7 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from scribe.config import Config from grimoire.config import Config
# Named rather than inlined so the deadline below is READABLE. SQLAlchemy # Named rather than inlined so the deadline below is READABLE. SQLAlchemy
# captures `connect_args` in a closure and merges it at connect time, so an # captures `connect_args` in a closure and merges it at connect time, so an
@@ -44,53 +44,53 @@ class Base(DeclarativeBase):
pass pass
from scribe.models.base import CreatedAtMixin, TimestampMixin # noqa: E402, F401 from grimoire.models.base import CreatedAtMixin, TimestampMixin # noqa: E402, F401
from scribe.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401 from grimoire.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401
from scribe.models.setting import Setting # noqa: E402, F401 from grimoire.models.setting import Setting # noqa: E402, F401
from scribe.models.user import User # noqa: E402, F401 from grimoire.models.user import User # noqa: E402, F401
from scribe.models.app_log import AppLog # noqa: E402, F401 from grimoire.models.app_log import AppLog # noqa: E402, F401
from scribe.models.password_reset import PasswordResetToken # noqa: E402, F401 from grimoire.models.password_reset import PasswordResetToken # noqa: E402, F401
from scribe.models.invitation import InvitationToken # noqa: E402, F401 from grimoire.models.invitation import InvitationToken # noqa: E402, F401
from scribe.models.embedding import ( # noqa: E402, F401 from grimoire.models.embedding import ( # noqa: E402, F401
MilestoneEmbedding, NoteEmbedding, RuleEmbedding, SystemEmbedding, MilestoneEmbedding, NoteEmbedding, RuleEmbedding, SystemEmbedding,
) )
from scribe.models.retrieval_log import RetrievalLog # noqa: E402, F401 from grimoire.models.retrieval_log import RetrievalLog # noqa: E402, F401
from scribe.models.retrieval_tuning import RetrievalTuningEvent # noqa: E402, F401 from grimoire.models.retrieval_tuning import RetrievalTuningEvent # noqa: E402, F401
from scribe.models.note_usage import NoteUsageEvent # noqa: E402, F401 from grimoire.models.note_usage import NoteUsageEvent # noqa: E402, F401
from scribe.models.rule_usage import RuleUsageEvent # noqa: E402, F401 from grimoire.models.rule_usage import RuleUsageEvent # noqa: E402, F401
from scribe.models.system_usage import SystemUsageEvent # noqa: E402, F401 from grimoire.models.system_usage import SystemUsageEvent # noqa: E402, F401
from scribe.models.retrieval_judgment import RetrievalJudgment # noqa: E402, F401 from grimoire.models.retrieval_judgment import RetrievalJudgment # noqa: E402, F401
from scribe.models.moment_mapping import MomentMapping # noqa: E402, F401 from grimoire.models.moment_mapping import MomentMapping # noqa: E402, F401
from scribe.models.project import Project # noqa: E402, F401 from grimoire.models.project import Project # noqa: E402, F401
from scribe.models.milestone import Milestone # noqa: E402, F401 from grimoire.models.milestone import Milestone # noqa: E402, F401
from scribe.models.task_log import TaskLog # noqa: E402, F401 from grimoire.models.task_log import TaskLog # noqa: E402, F401
from scribe.models.note_draft import NoteDraft # noqa: E402, F401 from grimoire.models.note_draft import NoteDraft # noqa: E402, F401
from scribe.models.note_version import NoteVersion # noqa: E402, F401 from grimoire.models.note_version import NoteVersion # noqa: E402, F401
from scribe.models.rule_version import RuleVersion # noqa: E402, F401 from grimoire.models.rule_version import RuleVersion # noqa: E402, F401
from scribe.models.note_supersession import NoteSupersession # noqa: E402, F401 from grimoire.models.note_supersession import NoteSupersession # noqa: E402, F401
from scribe.models.group import Group, GroupMembership # noqa: E402, F401 from grimoire.models.group import Group, GroupMembership # noqa: E402, F401
from scribe.models.share import NoteShare, ProjectShare # noqa: E402, F401 from grimoire.models.share import NoteShare, ProjectShare # noqa: E402, F401
from scribe.models.notification import Notification # noqa: E402, F401 from grimoire.models.notification import Notification # noqa: E402, F401
from scribe.models.api_key import ApiKey # noqa: E402, F401 from grimoire.models.api_key import ApiKey # noqa: E402, F401
# Imported before rulebook: rule_systems foreign-keys canonical_systems. # Imported before rulebook: rule_systems foreign-keys canonical_systems.
from scribe.models.canonical_system import CanonicalSystem # noqa: E402, F401 from grimoire.models.canonical_system import CanonicalSystem # noqa: E402, F401
from scribe.models.rulebook import ( # noqa: E402, F401 from grimoire.models.rulebook import ( # noqa: E402, F401
Rulebook, RulebookTopic, Rule, RuleRelation, rule_moments, rule_systems, Rulebook, RulebookTopic, Rule, RuleRelation, rule_moments, rule_systems,
) )
# After notes and rules: it foreign-keys both. # After notes and rules: it foreign-keys both.
from scribe.models.lesson_rule_link import LessonNoRule, LessonRuleLink # noqa: E402, F401 from grimoire.models.lesson_rule_link import LessonNoRule, LessonRuleLink # noqa: E402, F401
# After rules: it foreign-keys them. # After rules: it foreign-keys them.
from scribe.models.rule_moment_judgment import RuleMomentJudgment # noqa: E402, F401 from grimoire.models.rule_moment_judgment import RuleMomentJudgment # noqa: E402, F401
from scribe.models.repo_binding import RepoBinding # noqa: E402, F401 from grimoire.models.repo_binding import RepoBinding # noqa: E402, F401
from scribe.models.forge_connection import ForgeConnection # noqa: E402, F401 from grimoire.models.forge_connection import ForgeConnection # noqa: E402, F401
from scribe.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401 from grimoire.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401
from scribe.models.system import System, RecordSystem # noqa: E402, F401 from grimoire.models.system import System, RecordSystem # noqa: E402, F401
from scribe.models.design_system import DesignSystem, DesignToken # noqa: E402, F401 from grimoire.models.design_system import DesignSystem, DesignToken # noqa: E402, F401
# After notes, projects and rulebook topics: family canon foreign-keys all # After notes, projects and rulebook topics: family canon foreign-keys all
# three. # three.
from scribe.models.family import ( # noqa: E402, F401 from grimoire.models.family import ( # noqa: E402, F401
FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform, FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform,
FamilyIdeaReference, Platform, ProjectPlatform, FamilyIdeaReference, Platform, ProjectPlatform,
) )
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
class ApiKey(Base, CreatedAtMixin): class ApiKey(Base, CreatedAtMixin):
@@ -3,8 +3,8 @@ 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import iso from grimoire.models.base import iso
class AppLog(Base): class AppLog(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text, text from sqlalchemy import Index, Integer, Text, text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin): class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -12,8 +12,8 @@ from sqlalchemy import (
) )
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import TimestampMixin, iso from grimoire.models.base import TimestampMixin, iso
# The classification vocabulary. `unclassified` is the default and # The classification vocabulary. `unclassified` is the default and
# THE todo state; every other status is a judgment, stamped with who made it — # THE todo state; every other status is a judgment, stamped with who made it —
@@ -19,8 +19,8 @@ from sqlalchemy import BigInteger, ForeignKey, Index, Integer, Text, 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class DesignSystem(Base, TimestampMixin, SoftDeleteMixin): class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -4,7 +4,7 @@ from pgvector.sqlalchemy import Vector
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
# bge-small-en-v1.5 produces 384-dim unit-normalized vectors. The column is a # bge-small-en-v1.5 produces 384-dim unit-normalized vectors. The column is a
# native pgvector `vector(384)` (see migration 0067) so similarity is ranked in # native pgvector `vector(384)` (see migration 0067) so similarity is ranked in
@@ -41,8 +41,8 @@ from sqlalchemy import (
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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
# Each tuple is the whitelist its CHECK constraint enforces (migration 0120). # Each tuple is the whitelist its CHECK constraint enforces (migration 0120).
# A new value means DROP + ADD CONSTRAINT in the same migration. # A new value means DROP + ADD CONSTRAINT in the same migration.
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import TimestampMixin, iso from grimoire.models.base import TimestampMixin, iso
class ForgeConnection(Base, TimestampMixin): class ForgeConnection(Base, TimestampMixin):
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, TimestampMixin, iso from grimoire.models.base import CreatedAtMixin, TimestampMixin, iso
class Group(Base, TimestampMixin): class Group(Base, TimestampMixin):
@@ -3,8 +3,8 @@ 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin from grimoire.models.base import CreatedAtMixin
class InvitationToken(Base, CreatedAtMixin): class InvitationToken(Base, CreatedAtMixin):
@@ -4,8 +4,8 @@ from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text, UniqueCo
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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
SUGGESTED = "suggested" SUGGESTED = "suggested"
CONFIRMED = "confirmed" CONFIRMED = "confirmed"
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class Milestone(Base, TimestampMixin, SoftDeleteMixin): class Milestone(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, ForeignKey, Integer, Text, UniqueConstraint from sqlalchemy import BigInteger, ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
class MomentMapping(Base, CreatedAtMixin): class MomentMapping(Base, CreatedAtMixin):
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class TaskStatus(str, enum.Enum): class TaskStatus(str, enum.Enum):
@@ -212,6 +212,6 @@ class Note(Base, TimestampMixin, SoftDeleteMixin):
def _claim_state(note: "Note") -> dict | None: def _claim_state(note: "Note") -> dict | None:
from scribe.services.task_claims import claim_state from grimoire.services.task_claims import claim_state
return claim_state(note) return claim_state(note)
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import TimestampMixin, iso from grimoire.models.base import TimestampMixin, iso
class NoteDraft(Base, TimestampMixin): class NoteDraft(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
class NoteSupersession(Base, CreatedAtMixin): class NoteSupersession(Base, CreatedAtMixin):
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced" SURFACED = "surfaced"
PULLED = "pulled" PULLED = "pulled"
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
class Notification(Base, CreatedAtMixin): class Notification(Base, CreatedAtMixin):
@@ -3,8 +3,8 @@ 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin from grimoire.models.base import CreatedAtMixin
class PasswordResetToken(Base, CreatedAtMixin): class PasswordResetToken(Base, CreatedAtMixin):
@@ -2,8 +2,8 @@ import enum
from sqlalchemy import BigInteger, ForeignKey, Integer, Text from sqlalchemy import BigInteger, 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class ProjectStatus(str, enum.Enum): class ProjectStatus(str, enum.Enum):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import TimestampMixin, iso from grimoire.models.base import TimestampMixin, iso
class RepoBinding(Base, TimestampMixin): class RepoBinding(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Boolean, Float, Index, Integer, Text, UniqueConstraint from sqlalchemy import BigInteger, Boolean, Float, Index, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
ON_POINT = "on_point" ON_POINT = "on_point"
ADJACENT = "adjacent" ADJACENT = "adjacent"
@@ -4,8 +4,8 @@ from sqlalchemy import Boolean, DateTime, Float, Index, 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import iso from grimoire.models.base import iso
class RetrievalLog(Base): class RetrievalLog(Base):
@@ -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 scribe.models import Base from grimoire.models import Base
class RetrievalTuningEvent(Base): class RetrievalTuningEvent(Base):
@@ -4,9 +4,9 @@ from sqlalchemy import BigInteger, DateTime, ForeignKey, Text, UniqueConstraint
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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
from scribe.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUGGESTED from grimoire.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUGGESTED
# The same three states as a lesson's link to a rule, for the same reasons — # The same three states as a lesson's link to a rule, for the same reasons —
# re-exported so a reader of this table need not know where they were first # re-exported so a reader of this table need not know where they were first
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Index, Text from sqlalchemy import BigInteger, Index, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced" SURFACED = "surfaced"
PULLED = "pulled" PULLED = "pulled"
@@ -3,8 +3,8 @@ from datetime import datetime
from sqlalchemy import BigInteger, ForeignKey, Text from sqlalchemy import BigInteger, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
class RuleVersion(Base, CreatedAtMixin): class RuleVersion(Base, CreatedAtMixin):
@@ -6,8 +6,8 @@ from sqlalchemy import (
) )
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class Rulebook(Base, TimestampMixin, SoftDeleteMixin): class Rulebook(Base, TimestampMixin, 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 scribe.models import Base from grimoire.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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import TimestampMixin, iso from grimoire.models.base import TimestampMixin, iso
class ProjectShare(Base, TimestampMixin): class ProjectShare(Base, TimestampMixin):
@@ -2,8 +2,8 @@ from sqlalchemy import ForeignKey, Index, Integer, Text, UniqueConstraint, 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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class System(Base, TimestampMixin, SoftDeleteMixin): class System(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Index, Text from sqlalchemy import BigInteger, Index, Text
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced" SURFACED = "surfaced"
PULLED = "pulled" PULLED = "pulled"
@@ -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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import TimestampMixin, iso from grimoire.models.base import TimestampMixin, iso
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 scribe.models import Base from grimoire.models import Base
from scribe.models.base import CreatedAtMixin, iso from grimoire.models.base import CreatedAtMixin, iso
class User(Base, CreatedAtMixin): class User(Base, CreatedAtMixin):
@@ -1,6 +1,6 @@
"""Restore a Grimoire backup into an empty instance, from the command line. """Restore a Grimoire backup into an empty instance, from the command line.
python -m scribe.restore /backups/scribe-20261015T020000Z.tar python -m grimoire.restore /backups/scribe-20261015T020000Z.tar
This is how a fresh instance is brought back after losing the old one. Run it This is how a fresh instance is brought back after losing the old one. Run it
inside the Grimoire container, which already has the database settings and has inside the Grimoire container, which already has the database settings and has
@@ -32,7 +32,7 @@ from sqlalchemy import func, select
def load_backup(path: Path) -> dict: def load_backup(path: Path) -> dict:
"""The payload in `path`, whichever of the two backup formats it is.""" """The payload in `path`, whichever of the two backup formats it is."""
from scribe.services.backup_file import is_archive, read_archive from grimoire.services.backup_file import is_archive, read_archive
with open(path, "rb") as f: with open(path, "rb") as f:
if is_archive(f): if is_archive(f):
@@ -41,17 +41,17 @@ def load_backup(path: Path) -> dict:
async def _account_count() -> int: async def _account_count() -> int:
from scribe.models import async_session from grimoire.models import async_session
from scribe.models.user import User from grimoire.models.user import User
async with async_session() as session: async with async_session() as session:
return (await session.execute(select(func.count()).select_from(User))).scalar_one() return (await session.execute(select(func.count()).select_from(User))).scalar_one()
async def run(path: Path) -> int: async def run(path: Path) -> int:
from scribe.models import engine from grimoire.models import engine
from scribe.services.backup import restore_full_backup from grimoire.services.backup import restore_full_backup
from scribe.services.backup_file import ArchiveError from grimoire.services.backup_file import ArchiveError
try: try:
try: try:
@@ -83,7 +83,7 @@ async def run(path: Path) -> int:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="python -m scribe.restore", prog="python -m grimoire.restore",
description="Restore a Grimoire backup (.tar or .json) into an empty instance.", description="Restore a Grimoire backup (.tar or .json) into an empty instance.",
) )
parser.add_argument("file", type=Path, help="the backup file") parser.add_argument("file", type=Path, help="the backup file")
@@ -3,8 +3,8 @@ import json
from quart import Blueprint, Response, g, jsonify, request from quart import Blueprint, Response, g, jsonify, request
from scribe.auth import admin_required, login_required, get_current_user_id from grimoire.auth import admin_required, login_required, get_current_user_id
from scribe.services.auth import ( from grimoire.services.auth import (
create_invitation, create_invitation,
delete_user, delete_user,
is_registration_open, is_registration_open,
@@ -13,15 +13,15 @@ from scribe.services.auth import (
revoke_invitation, revoke_invitation,
set_registration_open, set_registration_open,
) )
from scribe.services.backup import ( from grimoire.services.backup import (
export_full_backup, export_full_backup,
export_user_backup, export_user_backup,
restore_full_backup, restore_full_backup,
) )
from scribe.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email from grimoire.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email
from scribe.services.logging import get_logs, get_log_stats, log_audit from grimoire.services.logging import get_logs, get_log_stats, log_audit
from scribe.services.notifications import send_invitation_email from grimoire.services.notifications import send_invitation_email
from scribe.services.settings import ( from grimoire.services.settings import (
SECRET_MASK, SECRET_MASK,
get_admin_setting, get_admin_setting,
set_admin_setting, set_admin_setting,
@@ -61,14 +61,14 @@ async def backup():
async def restore(): async def restore():
"""Restore from an uploaded backup: the JSON download (application/json) """Restore from an uploaded backup: the JSON download (application/json)
or the scheduled backup's tar (sent as the raw body). The upload is held or the scheduled backup's tar (sent as the raw body). The upload is held
to the app's request-size limit; `python -m scribe.restore` has none, and to the app's request-size limit; `python -m grimoire.restore` has none, and
is the way in for a bigger file or a fresh instance.""" is the way in for a bigger file or a fresh instance."""
if request.mimetype == "application/json": if request.mimetype == "application/json":
data = await request.get_json() data = await request.get_json()
else: else:
import io import io
from scribe.services.backup_file import ArchiveError, read_archive from grimoire.services.backup_file import ArchiveError, read_archive
try: try:
data = read_archive(io.BytesIO(await request.get_data())) data = read_archive(io.BytesIO(await request.get_data()))
@@ -180,8 +180,8 @@ async def test_smtp():
@admin_bp.route("/forge-webhook", methods=["GET"]) @admin_bp.route("/forge-webhook", methods=["GET"])
@admin_required @admin_required
async def get_forge_webhook_settings(): async def get_forge_webhook_settings():
from scribe.config import Config from grimoire.config import Config
from scribe.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY from grimoire.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
webhook_secret = ( webhook_secret = (
await get_admin_setting(FORGE_WEBHOOK_SECRET_KEY, "") await get_admin_setting(FORGE_WEBHOOK_SECRET_KEY, "")
@@ -197,7 +197,7 @@ async def get_forge_webhook_settings():
@admin_bp.route("/forge-webhook", methods=["PUT"]) @admin_bp.route("/forge-webhook", methods=["PUT"])
@admin_required @admin_required
async def update_forge_webhook_settings(): async def update_forge_webhook_settings():
from scribe.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY from grimoire.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
data = await request.get_json() or {} data = await request.get_json() or {}
uid = get_current_user_id() uid = get_current_user_id()
@@ -270,8 +270,8 @@ async def update_base_url():
@admin_required @admin_required
async def get_db_maintenance(): async def get_db_maintenance():
"""Current DB-maintenance config + the last run's summary.""" """Current DB-maintenance config + the last run's summary."""
from scribe.services.db_maintenance import get_last_run from grimoire.services.db_maintenance import get_last_run
from scribe.services.db_maintenance_scheduler import ( from grimoire.services.db_maintenance_scheduler import (
get_maintenance_hour, get_maintenance_hour,
is_maintenance_enabled, is_maintenance_enabled,
) )
@@ -286,7 +286,7 @@ async def get_db_maintenance():
@admin_required @admin_required
async def get_db_maintenance_health(): async def get_db_maintenance_health():
"""Read-only per-table bloat/health stats from Postgres + total DB size.""" """Read-only per-table bloat/health stats from Postgres + total DB size."""
from scribe.services.db_maintenance import get_table_health from grimoire.services.db_maintenance import get_table_health
return jsonify(await get_table_health()) return jsonify(await get_table_health())
@@ -294,7 +294,7 @@ async def get_db_maintenance_health():
@admin_required @admin_required
async def update_db_maintenance(): async def update_db_maintenance():
"""Set whether scheduled maintenance runs and at what UTC hour.""" """Set whether scheduled maintenance runs and at what UTC hour."""
from scribe.services.db_maintenance_scheduler import reschedule_db_maintenance from grimoire.services.db_maintenance_scheduler import reschedule_db_maintenance
data = await request.get_json() or {} data = await request.get_json() or {}
enabled = bool(data.get("enabled", True)) enabled = bool(data.get("enabled", True))
try: try:
@@ -319,7 +319,7 @@ async def update_db_maintenance():
@admin_required @admin_required
async def run_db_maintenance_now(): async def run_db_maintenance_now():
"""Run a VACUUM (ANALYZE) sweep immediately and return its summary.""" """Run a VACUUM (ANALYZE) sweep immediately and return its summary."""
from scribe.services.db_maintenance import run_maintenance from grimoire.services.db_maintenance import run_maintenance
uid = get_current_user_id() uid = get_current_user_id()
await log_audit( await log_audit(
"db_maintenance_run", user_id=uid, username=g.user.username, "db_maintenance_run", user_id=uid, username=g.user.username,
@@ -342,8 +342,8 @@ _BACKUP_CHECK_TIMEOUT = 45.0
async def _backups_payload() -> dict: async def _backups_payload() -> dict:
from datetime import datetime, timezone from datetime import datetime, timezone
from scribe.services import backup_destination as dest from grimoire.services import backup_destination as dest
from scribe.services import backup_job as job from grimoire.services import backup_job as job
config = await dest.load_config() config = await dest.load_config()
secret, stored = await dest.load_secret() secret, stored = await dest.load_secret()
@@ -373,7 +373,7 @@ async def get_backups():
async def update_backups(): async def update_backups():
"""Change the destination or schedule. `s3_secret` replaces the stored """Change the destination or schedule. `s3_secret` replaces the stored
secret when it is sent and isn't the mask; `clear_s3_secret` removes it.""" secret when it is sent and isn't the mask; `clear_s3_secret` removes it."""
from scribe.services import backup_destination as dest from grimoire.services import backup_destination as dest
data = await request.get_json() or {} data = await request.get_json() or {}
try: try:
@@ -403,7 +403,7 @@ async def check_backups_destination():
"""Ask the destination whether it would take a backup, without sending """Ask the destination whether it would take a backup, without sending
one. The body may carry unsaved form values, so a destination can be one. The body may carry unsaved form values, so a destination can be
tested before it is saved; the stored secret is used unless one is sent.""" tested before it is saved; the stored secret is used unless one is sent."""
from scribe.services import backup_destination as dest from grimoire.services import backup_destination as dest
data = await request.get_json() or {} data = await request.get_json() or {}
try: try:
@@ -431,7 +431,7 @@ async def check_backups_destination():
async def run_backup_now(): async def run_backup_now():
"""Start a backup now. It runs in the background; the page follows it """Start a backup now. It runs in the background; the page follows it
through GET /backups (`running`, then the newest run).""" through GET /backups (`running`, then the newest run)."""
from scribe.services import backup_job as job from grimoire.services import backup_job as job
try: try:
job.start_backup_now() job.start_backup_now()
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id from grimoire.auth import login_required, get_current_user_id
from scribe.services.api_keys import create_api_key, list_api_keys, revoke_api_key from grimoire.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 scribe.auth import login_required, get_current_user_id from grimoire.auth import login_required, get_current_user_id
from scribe.config import Config from grimoire.config import Config
from scribe.rate_limit import is_rate_limited from grimoire.rate_limit import is_rate_limited
from scribe.services.auth import ( from grimoire.services.auth import (
authenticate, authenticate,
change_password, change_password,
create_password_reset_token, create_password_reset_token,
@@ -26,14 +26,14 @@ from scribe.services.auth import (
validate_invitation_token, validate_invitation_token,
verify_password, verify_password,
) )
from scribe.services.logging import log_audit from grimoire.services.logging import log_audit
from scribe.services.notifications import ( from grimoire.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 scribe.services.email import get_base_url, is_smtp_configured from grimoire.services.email import get_base_url, is_smtp_configured
from scribe.services.oauth import ( from grimoire.services.oauth import (
build_auth_url, build_auth_url,
exchange_code, exchange_code,
get_userinfo, get_userinfo,
@@ -13,10 +13,10 @@ import logging
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from scribe.auth import admin_required, get_current_user_id, login_required from grimoire.auth import admin_required, get_current_user_id, login_required
from scribe.routes.utils import not_found from grimoire.routes.utils import not_found
from scribe.services import canonical_systems as canonical_svc from grimoire.services import canonical_systems as canonical_svc
from scribe.services.projects import get_project_for_user from grimoire.services.projects import get_project_for_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -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 scribe.auth import login_required from grimoire.auth import login_required
from scribe.services.dashboard import build_dashboard from grimoire.services.dashboard import build_dashboard
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/api/dashboard") dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/api/dashboard")
@@ -17,14 +17,14 @@ one and returns None for the other:
""" """
from quart import Blueprint, jsonify, request from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required from grimoire.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found from grimoire.routes.utils import not_found
from scribe.services import design_systems as ds_svc from grimoire.services import design_systems as ds_svc
from scribe.services.design_starter_roles import ( from grimoire.services.design_starter_roles import (
DEFAULT_TOKEN_PREFIX, DEFAULT_TOKEN_PREFIX,
describe_groups, describe_groups,
) )
from scribe.services.design_systems import DesignSystemCycle from grimoire.services.design_systems import DesignSystemCycle
design_systems_bp = Blueprint("design_systems", __name__, url_prefix="/api") design_systems_bp = Blueprint("design_systems", __name__, url_prefix="/api")

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