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_PASSWORD=fabled
POSTGRES_DB=scribe
POSTGRES_DB=grimoire
# PGDATA_VOLUME=grimoire_pgdata
SECRET_KEY=dev-secret-change-me
+5 -5
View File
@@ -55,7 +55,7 @@ permissions:
env:
REGISTRY: git.fabledsword.com
IMAGE: git.fabledsword.com/bvandeusen/fabledscribe
IMAGE: git.fabledsword.com/bvandeusen/fabledgrimoire
jobs:
typecheck:
@@ -193,11 +193,11 @@ jobs:
# Same image as the shipped compose files: Postgres 17 with pgvector.
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: scribe
POSTGRES_USER: grimoire
POSTGRES_PASSWORD: ci_integration
POSTGRES_DB: scribe_test
POSTGRES_DB: grimoire_test
options: >-
--health-cmd "pg_isready -U scribe"
--health-cmd "pg_isready -U grimoire"
--health-interval 10s
--health-timeout 5s
--health-retries 10
@@ -235,7 +235,7 @@ jobs:
test "$(printf '%s\n' "$PG" | grep -c .)" -eq 1
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
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.
/opt/venv/bin/python - "$PG_IP" <<'PY'
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.
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/ alembic/
@@ -57,4 +57,4 @@ ENV APP_CHANNEL=$BUILD_CHANNEL
ENV APP_COMMIT=$BUILD_COMMIT
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 sqlalchemy.ext.asyncio import create_async_engine
from scribe.config import Config
from scribe.models import Base
from scribe.services.db_ready import wait_for_database
from grimoire.config import Config
from grimoire.models import Base
from grimoire.services.db_ready import wait_for_database
config = context.config
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():
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()
return Fernet(base64.urlsafe_b64encode(digest))
+8 -7
View File
@@ -1,14 +1,14 @@
services:
app:
image: git.fabledsword.com/bvandeusen/fabledscribe:latest
image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
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}"
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
TRUST_PROXY_HEADERS: "true"
SECURE_COOKIES: "true"
networks:
- scribe_backend
- backend
db:
# Postgres 17 with the vector extension Grimoire needs.
@@ -17,15 +17,16 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: scribe
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: scribe
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
networks:
- scribe_backend
- backend
volumes:
pgdata:
name: ${PGDATA_VOLUME:-grimoire_pgdata}
networks:
scribe_backend:
backend:
driver: overlay
+5 -5
View File
@@ -5,11 +5,11 @@
services:
app:
image: git.fabledsword.com/bvandeusen/fabledscribe:latest
image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
ports:
- "5000:5000"
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.
SECRET_KEY: "${SECRET_KEY:-change-me-in-production}"
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
@@ -26,9 +26,9 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: scribe
POSTGRES_PASSWORD: scribe
POSTGRES_DB: scribe
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
restart: unless-stopped
volumes:
+4 -4
View File
@@ -11,7 +11,7 @@ services:
# Or a bind mount, for direct host access to the app's data:
# - ./data:/data
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}"
# Optional: a SearXNG instance to list as the web-search backend in Integrations.
# SEARXNG_URL: "http://searxng:8080"
@@ -24,9 +24,9 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${POSTGRES_USER:-scribe}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-scribe}
POSTGRES_DB: ${POSTGRES_DB:-scribe}
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
volumes:
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)
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
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
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
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 |
|--------|----------|---------|
@@ -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
recall-before-acting reflex, and the rulebook conventions — is delivered to the
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
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`,
`start_planning`, `search`, `enter_project`, …) with
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 REST API** (`src/scribe/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 REST API** (`src/grimoire/routes/`) backs the UI, one blueprint per resource.
- **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.
- **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
@@ -80,14 +80,14 @@ Credentials Grimoire stores for outside services (forge tokens, the backup desti
```
├── alembic/versions/ database migrations
├── src/scribe/
├── src/grimoire/
│ ├── app.py app factory, blueprints, startup and shutdown
│ ├── config.py environment configuration
│ ├── models/ SQLAlchemy models
│ ├── routes/ REST blueprints
│ ├── mcp/ MCP server and its tool modules
│ ├── 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
├── plugin/ Claude Code hooks, skills and commands
├── 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:
```
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:
- `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
services:
scribe:
grimoire:
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
backup.
@@ -115,7 +118,7 @@ page asks for the secret again.
[Fabled Archives](https://git.fabledsword.com/bvandeusen/fabledarchives)
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.
2. Choose how Grimoire's files arrive:
- **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;
- access key and secret key: the key you just made.
- **A folder**: mount the share Archives watches into Grimoire as
`/backups/scribe`, and point the target at that folder. In Grimoire, choose
**A folder** with `/backups/scribe`.
`/backups/grimoire`, and point the target at that folder. In Grimoire, choose
**A folder** with `/backups/grimoire`.
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.
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:
```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
+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.
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
@@ -10,7 +10,7 @@ Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to
| 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) |
| `BASE_URL` | `http://localhost:5000` | Public URL, used in email links and OIDC redirects |
| `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:
```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.
### 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
- 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.
- Access checks go through `services/access.py`, never inline ownership checks.
- 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.
- **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
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "scribe-frontend",
"name": "grimoire-frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scribe-frontend",
"name": "grimoire-frontend",
"version": "0.1.0",
"dependencies": {
"@fullcalendar/core": "^6.1.20",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "scribe-frontend",
"name": "grimoire-frontend",
"private": true,
"version": "0.1.0",
"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`
* (`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.
*
* EVERY FIELD BUT `version` IS OPTIONAL, and an absent one means "this build
@@ -187,7 +187,7 @@ async function backUpNow() {
</ul>
<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
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.
@@ -237,7 +237,7 @@ async function backUpNow() {
</label>
<label class="field">
<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 class="field">
<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 a = document.createElement("a");
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();
URL.revokeObjectURL(a.href);
toastStore.show("Backup downloaded");
@@ -1091,7 +1091,7 @@ async function exportNotes(format: "markdown" | "json") {
const stamp = new Date().toISOString().slice(0, 10);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `scribe-${stamp}.${ext}`;
a.download = `grimoire-${stamp}.${ext}`;
a.click();
URL.revokeObjectURL(a.href);
toastStore.show("Export downloaded");
@@ -1319,7 +1319,7 @@ async function handleRestoreFile(event: Event) {
if (res.status === 413) {
throw new Error(
"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) {
+1 -1
View File
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "scribe"
name = "grimoire"
version = "0.1.0"
description = "Self-hosted note-taking, project-management, and calendar app exposed to Claude via MCP"
requires-python = ">=3.14"
+1 -1
View File
@@ -103,7 +103,7 @@ async def run(args) -> None:
print(f" - {r['title']}: {preview}{empty_flag}")
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)
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 scribe.config import Config
from scribe.routes.admin import admin_bp
from scribe.routes.api import api
from scribe.routes.auth import auth_bp
from scribe.routes.export import export_bp
from scribe.routes.notes import notes_bp
from scribe.routes.milestones import milestones_bp
from scribe.routes.task_logs import task_logs_bp
from scribe.routes.projects import projects_bp
from scribe.routes.retrieval import retrieval_bp
from scribe.routes.settings import settings_bp
from scribe.routes.tasks import tasks_bp
from scribe.routes.groups import groups_bp
from scribe.routes.shares import shares_bp
from scribe.routes.in_app_notifications import notifications_bp
from scribe.routes.users import users_bp
from scribe.routes.api_keys import api_keys_bp
from scribe.routes.search import search_bp
from scribe.routes.knowledge import knowledge_bp
from scribe.routes.rulebooks import rulebooks_bp
from scribe.routes.plugin import plugin_bp
from scribe.routes.design_systems import design_systems_bp
from scribe.routes.trash import trash_bp
from scribe.routes.dashboard import dashboard_bp
from scribe.routes.systems import systems_bp
from scribe.routes.canonical_systems import canonical_systems_bp
from scribe.routes.platforms import platforms_bp
from scribe.routes.family import family_bp
from scribe.routes.lessons import lessons_bp
from scribe.routes.snippets import snippets_bp
from scribe.routes.webhooks import webhooks_bp
from scribe.mcp import mount_mcp
from grimoire.config import Config
from grimoire.routes.admin import admin_bp
from grimoire.routes.api import api
from grimoire.routes.auth import auth_bp
from grimoire.routes.export import export_bp
from grimoire.routes.notes import notes_bp
from grimoire.routes.milestones import milestones_bp
from grimoire.routes.task_logs import task_logs_bp
from grimoire.routes.projects import projects_bp
from grimoire.routes.retrieval import retrieval_bp
from grimoire.routes.settings import settings_bp
from grimoire.routes.tasks import tasks_bp
from grimoire.routes.groups import groups_bp
from grimoire.routes.shares import shares_bp
from grimoire.routes.in_app_notifications import notifications_bp
from grimoire.routes.users import users_bp
from grimoire.routes.api_keys import api_keys_bp
from grimoire.routes.search import search_bp
from grimoire.routes.knowledge import knowledge_bp
from grimoire.routes.rulebooks import rulebooks_bp
from grimoire.routes.plugin import plugin_bp
from grimoire.routes.design_systems import design_systems_bp
from grimoire.routes.trash import trash_bp
from grimoire.routes.dashboard import dashboard_bp
from grimoire.routes.systems import systems_bp
from grimoire.routes.canonical_systems import canonical_systems_bp
from grimoire.routes.platforms import platforms_bp
from grimoire.routes.family import family_bp
from grimoire.routes.lessons import lessons_bp
from grimoire.routes.snippets import snippets_bp
from grimoire.routes.webhooks import webhooks_bp
from grimoire.mcp import mount_mcp
STATIC_DIR = Path(__file__).parent / "static"
logger = logging.getLogger(__name__)
@@ -131,7 +131,7 @@ def create_app() -> Quart:
# Log usage for API requests (skip logs endpoint to avoid recursion)
if request.path.startswith("/api/") and not request.path.startswith("/api/admin/logs"):
try:
from scribe.services.logging import log_usage
from grimoire.services.logging import log_usage
user = getattr(g, "user", None)
await log_usage(
@@ -173,13 +173,13 @@ def create_app() -> Quart:
# `create_app`, which runs before either is true.
_startup_finished = asyncio.Event()
from scribe.services.auth import start_auth_token_retention_loop
from scribe.services.embeddings import (
from grimoire.services.auth import start_auth_token_retention_loop
from grimoire.services.embeddings import (
backfill_milestone_embeddings, backfill_note_embeddings,
backfill_rule_embeddings, backfill_system_embeddings,
)
from scribe.services.logging import start_log_retention_loop
from scribe.services.notifications import start_notification_loop
from grimoire.services.logging import start_log_retention_loop
from grimoire.services.notifications import start_notification_loop
start_log_retention_loop()
start_notification_loop()
@@ -200,7 +200,7 @@ def create_app() -> Quart:
# answer this exists to stop giving. Failure is logged and swallowed —
# provenance is worth a lot and never worth refusing to boot.
try:
from scribe.services.retrieval_tuning import record_release_defaults
from grimoire.services.retrieval_tuning import record_release_defaults
moved = await record_release_defaults()
for row in moved:
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
# try block so neither backfill can skip the other.
try:
from scribe.services.snippets import backfill_snippet_data
from grimoire.services.snippets import backfill_snippet_data
await backfill_snippet_data()
except Exception:
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.
try:
# 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())
# 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(asyncio.get_running_loop())
# 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())
# 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
# touches the database needs the same treatment: a lifespan hook
# 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,
start_db_maintenance_scheduler,
)
@@ -301,13 +301,13 @@ def create_app() -> Quart:
# Scheduled backups. An hourly tick that reads its config when it
# 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())
# Diagnostic instrumentation — heartbeat, signal handlers, asyncio
# exception hook. Cheap (~1 log line/min), high diagnostic value when
# 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())
finally:
# STARTUP IS OVER — release the backfill. Everything above
@@ -318,21 +318,21 @@ def create_app() -> Quart:
@app.after_serving
async def shutdown():
from scribe.services.recurrence_scheduler import stop_recurrence_scheduler
from grimoire.services.recurrence_scheduler import 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()
from scribe.services.trash_scheduler import stop_trash_scheduler
from grimoire.services.trash_scheduler import 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()
from scribe.services.backup_job import stop_backup_scheduler
from grimoire.services.backup_job import stop_backup_scheduler
stop_backup_scheduler()
from scribe.services.diagnostics import stop_diagnostics
from grimoire.services.diagnostics import stop_diagnostics
stop_diagnostics()
@app.route("/")
@@ -382,7 +382,7 @@ def create_app() -> Quart:
logger.exception("Internal server error on %s %s", request.method, request.path)
try:
from scribe.services.logging import log_error
from grimoire.services.logging import log_error
user = getattr(g, "user", None)
await log_error(
+2 -2
View File
@@ -2,8 +2,8 @@ import functools
from quart import g, jsonify, request, session
from scribe.services.auth import get_user_by_id
from scribe.services.api_keys import lookup_key
from grimoire.services.auth import get_user_by_id
from grimoire.services.api_keys import lookup_key
def _check_auth(f, required_role: str | None = None):
@@ -20,7 +20,7 @@ class Config:
DATABASE_URL: str = _read_secret(
"DATABASE_URL",
"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")
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
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"]
@@ -1,7 +1,7 @@
"""Per-request MCP context.
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.
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
_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:
@@ -1,7 +1,7 @@
"""MCP-side Bearer token resolution. Reuses the existing api_keys infrastructure."""
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`.
"""
mcp = StrictArgsMCPServer("scribe", instructions=_INSTRUCTIONS.strip())
from scribe.mcp.tools import register_all
from grimoire.mcp.tools import register_all
register_all(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
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.
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
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()
# 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,
"headers": [
(b"content-type", b"application/json"),
(b"www-authenticate", b'Bearer realm="scribe-mcp"'),
(b"www-authenticate", b'Bearer realm="grimoire-mcp"'),
],
})
await send({
@@ -456,8 +456,8 @@ def mount_mcp(app: Quart) -> None:
})
return
scope["scribe_user_id"] = user_id
from scribe.mcp._context import _user_id_ctx
scope["grimoire_user_id"] = user_id
from grimoire.mcp._context import _user_id_ctx
token = _user_id_ctx.set(user_id)
try:
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
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,
moments, platforms, retrieval_review, retrieval_tuning,
wide_net,
@@ -19,10 +19,10 @@ Sentinels, matching the milestone/task tool conventions:
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import design_systems as ds_svc
from scribe.services.design_systems import DesignSystemCycle
from scribe.services.design_starter_roles import (
from grimoire.mcp._context import current_user_id
from grimoire.services import design_systems as ds_svc
from grimoire.services.design_systems import DesignSystemCycle
from grimoire.services.design_starter_roles import (
ALL_GROUPS,
DEFAULT_TOKEN_PREFIX,
describe_groups,
@@ -6,9 +6,9 @@ and enforced by the service, and every decision is logged with its reason.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import family as family_svc
from scribe.services import family_adoption as adoption_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import family as family_svc
from grimoire.services import family_adoption as adoption_svc
async def list_family_ideas(
@@ -9,17 +9,17 @@ would be something the writer had to know to include.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc
from scribe.services import lesson_rules as lesson_rules_svc
from scribe.services import lessons as lessons_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.mcp.tools import systems as systems_tools
from scribe.services.note_usage import attach_usage, record_pulled
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import knowledge as knowledge_svc
from grimoire.services import lesson_rules as lesson_rules_svc
from grimoire.services import lessons as lessons_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services.note_usage import attach_usage, record_pulled
from grimoire.services import moment_delivery
# The payload shape lives in the service (`lesson_to_dict`), shared with the
@@ -11,16 +11,16 @@ Sentinels:
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import task_logs as task_logs_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import trash as trash_svc
from scribe.services.record_refs import refuse_guessed_ids
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from grimoire.services import task_logs as task_logs_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import trash as trash_svc
from grimoire.services.record_refs import refuse_guessed_ids
from grimoire.services import moment_delivery
async def list_milestones(project_id: int) -> dict:
@@ -12,11 +12,11 @@ operator's yes.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import moment_actions as actions_svc
from scribe.services import moments as moments_svc
from scribe.services import reply_shapes as shapes_svc
from scribe.services import rule_moment_judgments as judgments_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import moment_actions as actions_svc
from grimoire.services import moments as moments_svc
from grimoire.services import reply_shapes as shapes_svc
from grimoire.services import rule_moment_judgments as judgments_svc
async def list_moments() -> dict:
@@ -13,18 +13,18 @@ Sentinel conventions (inherited from existing fable-mcp tools):
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import notes as notes_svc
from scribe.services import supersession as supersession_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.note_usage import record_pulled
from scribe.services.record_refs import refuse_guessed_ids
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import notes as notes_svc
from grimoire.services import supersession as supersession_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.note_usage import record_pulled
from grimoire.services.record_refs import refuse_guessed_ids
from grimoire.services import moment_delivery
async def list_notes(
@@ -5,8 +5,8 @@ platforms decide which family ideas reach it.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import platforms as platforms_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import platforms as platforms_svc
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 scribe.mcp._context import current_user_id
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc
from scribe.services import moment_actions
from scribe.services import moments as moments_svc
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.note_usage import record_pulled
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import knowledge as knowledge_svc
from grimoire.services import moment_actions
from grimoire.services import moments as moments_svc
from grimoire.services import notes as notes_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.note_usage import record_pulled
from grimoire.services import moment_delivery
async def list_processes(
@@ -9,22 +9,22 @@ from __future__ import annotations
import logging
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import coverage as coverage_svc
from scribe.services import design_systems as design_systems_svc
from scribe.services import family_adoption as family_adoption_svc
from scribe.services import inception as inception_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import platforms as platforms_svc
from scribe.services import projects as projects_svc
from scribe.services import reply_shapes as reply_shapes_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.background import spawn
from scribe.services.note_usage import record_surfaced
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import coverage as coverage_svc
from grimoire.services import design_systems as design_systems_svc
from grimoire.services import family_adoption as family_adoption_svc
from grimoire.services import inception as inception_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from grimoire.services import platforms as platforms_svc
from grimoire.services import projects as projects_svc
from grimoire.services import reply_shapes as reply_shapes_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.background import spawn
from grimoire.services.note_usage import record_surfaced
logger = logging.getLogger(__name__)
@@ -13,10 +13,10 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from scribe.mcp._context import current_user_id
from scribe.models import async_session
from scribe.models.note import Note
from scribe.models.project import Project
from grimoire.mcp._context import current_user_id
from grimoire.models import async_session
from grimoire.models.note import Note
from grimoire.models.project import Project
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 scribe.mcp._context import current_user_id
from scribe.services import projects as projects_svc
from scribe.services import repo_bindings as repo_bindings_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import projects as projects_svc
from grimoire.services import repo_bindings as repo_bindings_svc
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
looking at the query beside the line can do.
"""
from scribe.mcp._context import current_user_id
from scribe.services import retrieval_review as review_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import retrieval_review as review_svc
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
attached.
"""
from scribe.mcp._context import current_user_id
from scribe.services import retrieval_migration as migration_svc
from scribe.services import retrieval_tuning as tuning_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import retrieval_migration as migration_svc
from grimoire.services import retrieval_tuning as tuning_svc
async def retrieval_surfaces() -> dict:
@@ -10,15 +10,15 @@ preview-style warning.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import dedup as dedup_svc
from scribe.services import moments as moments_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import trash as trash_svc
from scribe.services.rule_usage import (
from grimoire.mcp._context import current_user_id
from grimoire.services import dedup as dedup_svc
from grimoire.services import moments as moments_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import trash as trash_svc
from grimoire.services.rule_usage import (
record_rule_outcome, record_rule_pulled,
)
from scribe.services import moment_delivery
from grimoire.services import moment_delivery
# ── Rulebook CRUD ───────────────────────────────────────────────────────
@@ -9,16 +9,16 @@ from __future__ import annotations
import time
from scribe.mcp._context import current_user_id
from scribe.services.access import owner_names_for
from scribe.services.knowledge import content_type_filters
from scribe.services.text import MATCHED_PASSAGE, excerpt_fields
from scribe.services.embeddings import (
from grimoire.mcp._context import current_user_id
from grimoire.services.access import owner_names_for
from grimoire.services.knowledge import content_type_filters
from grimoire.services.text import MATCHED_PASSAGE, excerpt_fields
from grimoire.services.embeddings import (
DEFAULT_SIMILARITY_THRESHOLD, semantic_search_milestones, semantic_search_notes,
semantic_search_rules, semantic_search_systems,
)
from scribe.services import rulebooks as rulebooks_svc
from scribe.services.retrieval_telemetry import record_retrieval, retrieval_summary
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services.retrieval_telemetry import record_retrieval, retrieval_summary
# 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 {}
progress: dict[int, dict] = {}
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 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 scribe.mcp._context import current_user_id
from scribe.services import coverage as coverage_svc
from scribe.services import shape_ledger as shape_ledger_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import coverage as coverage_svc
from grimoire.services import shape_ledger as shape_ledger_svc
async def classify_shapes(
@@ -10,15 +10,15 @@ on create, System association passthrough).
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import snippets as snippets_svc
from scribe.services.note_usage import attach_usage, record_pulled
from scribe.services import systems as systems_svc
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import snippets as snippets_svc
from grimoire.services.note_usage import attach_usage, record_pulled
from grimoire.services import systems as systems_svc
from grimoire.services import moment_delivery
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
# snippet. Attached only when non-empty — and never for projects
# 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))
if consumers["instances"]:
@@ -13,13 +13,13 @@ Sentinels (match the milestone/task tool conventions):
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import canonical_systems as canonical_systems_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
from scribe.services import system_usage as system_usage_svc
from scribe.services.system_usage import record_system_pulled
from grimoire.mcp._context import current_user_id
from grimoire.services import canonical_systems as canonical_systems_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from grimoire.services import systems as systems_svc
from grimoire.services import system_usage as system_usage_svc
from grimoire.services.system_usage import record_system_pulled
# 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
@@ -8,9 +8,9 @@ from __future__ import annotations
from sqlalchemy import select
from scribe.mcp._context import current_user_id
from scribe.models import async_session
from scribe.models.note import Note
from grimoire.mcp._context import current_user_id
from grimoire.models import async_session
from grimoire.models.note import Note
def _aggregate_tag_counts(tag_lists) -> dict[str, int]:
@@ -18,30 +18,30 @@ Sentinels (preserved from existing fable-mcp):
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import family_adoption as family_adoption_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import family_adoption as family_adoption_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
# 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
# avoid the database would otherwise stub the validation too — turning a
# guard into a MagicMock that approves anything.
from scribe.services.notes import minted_kind
from scribe.services import placement as placement_svc
from scribe.services import planning as planning_svc
from scribe.services import record_batch as batch_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc
from scribe.services import task_logs as task_logs_svc
from scribe.services import trash as trash_svc
from scribe.services.note_usage import record_pulled
from scribe.services.record_refs import refuse_guessed_ids
from scribe.services.text import elide
from scribe.services import moment_delivery
from grimoire.services.notes import minted_kind
from grimoire.services import placement as placement_svc
from grimoire.services import planning as planning_svc
from grimoire.services import record_batch as batch_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import systems as systems_svc
from grimoire.services import task_logs as task_logs_svc
from grimoire.services import trash as trash_svc
from grimoire.services.note_usage import record_pulled
from grimoire.services.record_refs import refuse_guessed_ids
from grimoire.services.text import elide
from grimoire.services import moment_delivery
# 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 scribe.mcp._context import current_user_id
from scribe.services import trash as trash_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import trash as trash_svc
async def list_trash() -> dict:
@@ -35,9 +35,9 @@ from __future__ import annotations
import textwrap
import time
from scribe.mcp._context import current_user_id
from scribe.services.embeddings import semantic_search_rules
from scribe.services.retrieval_telemetry import record_retrieval
from grimoire.mcp._context import current_user_id
from grimoire.services.embeddings import semantic_search_rules
from grimoire.services.retrieval_telemetry import record_retrieval
# 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
@@ -1,7 +1,7 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
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
# captures `connect_args` in a closure and merges it at connect time, so an
@@ -44,53 +44,53 @@ class Base(DeclarativeBase):
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 scribe.models.setting import Setting # noqa: E402, F401
from scribe.models.user import User # noqa: E402, F401
from scribe.models.app_log import AppLog # noqa: E402, F401
from scribe.models.password_reset import PasswordResetToken # noqa: E402, F401
from scribe.models.invitation import InvitationToken # noqa: E402, F401
from scribe.models.embedding import ( # noqa: E402, F401
from grimoire.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401
from grimoire.models.setting import Setting # noqa: E402, F401
from grimoire.models.user import User # noqa: E402, F401
from grimoire.models.app_log import AppLog # noqa: E402, F401
from grimoire.models.password_reset import PasswordResetToken # noqa: E402, F401
from grimoire.models.invitation import InvitationToken # noqa: E402, F401
from grimoire.models.embedding import ( # noqa: E402, F401
MilestoneEmbedding, NoteEmbedding, RuleEmbedding, SystemEmbedding,
)
from scribe.models.retrieval_log import RetrievalLog # noqa: E402, F401
from scribe.models.retrieval_tuning import RetrievalTuningEvent # noqa: E402, F401
from scribe.models.note_usage import NoteUsageEvent # noqa: E402, F401
from scribe.models.rule_usage import RuleUsageEvent # noqa: E402, F401
from scribe.models.system_usage import SystemUsageEvent # noqa: E402, F401
from scribe.models.retrieval_judgment import RetrievalJudgment # noqa: E402, F401
from scribe.models.moment_mapping import MomentMapping # noqa: E402, F401
from scribe.models.project import Project # noqa: E402, F401
from scribe.models.milestone import Milestone # noqa: E402, F401
from scribe.models.task_log import TaskLog # noqa: E402, F401
from scribe.models.note_draft import NoteDraft # noqa: E402, F401
from scribe.models.note_version import NoteVersion # noqa: E402, F401
from scribe.models.rule_version import RuleVersion # noqa: E402, F401
from scribe.models.note_supersession import NoteSupersession # noqa: E402, F401
from scribe.models.group import Group, GroupMembership # noqa: E402, F401
from scribe.models.share import NoteShare, ProjectShare # noqa: E402, F401
from scribe.models.notification import Notification # noqa: E402, F401
from scribe.models.api_key import ApiKey # noqa: E402, F401
from grimoire.models.retrieval_log import RetrievalLog # noqa: E402, F401
from grimoire.models.retrieval_tuning import RetrievalTuningEvent # noqa: E402, F401
from grimoire.models.note_usage import NoteUsageEvent # noqa: E402, F401
from grimoire.models.rule_usage import RuleUsageEvent # noqa: E402, F401
from grimoire.models.system_usage import SystemUsageEvent # noqa: E402, F401
from grimoire.models.retrieval_judgment import RetrievalJudgment # noqa: E402, F401
from grimoire.models.moment_mapping import MomentMapping # noqa: E402, F401
from grimoire.models.project import Project # noqa: E402, F401
from grimoire.models.milestone import Milestone # noqa: E402, F401
from grimoire.models.task_log import TaskLog # noqa: E402, F401
from grimoire.models.note_draft import NoteDraft # noqa: E402, F401
from grimoire.models.note_version import NoteVersion # noqa: E402, F401
from grimoire.models.rule_version import RuleVersion # noqa: E402, F401
from grimoire.models.note_supersession import NoteSupersession # noqa: E402, F401
from grimoire.models.group import Group, GroupMembership # noqa: E402, F401
from grimoire.models.share import NoteShare, ProjectShare # noqa: E402, F401
from grimoire.models.notification import Notification # noqa: E402, F401
from grimoire.models.api_key import ApiKey # noqa: E402, F401
# Imported before rulebook: rule_systems foreign-keys canonical_systems.
from scribe.models.canonical_system import CanonicalSystem # noqa: E402, F401
from scribe.models.rulebook import ( # noqa: E402, F401
from grimoire.models.canonical_system import CanonicalSystem # noqa: E402, F401
from grimoire.models.rulebook import ( # noqa: E402, F401
Rulebook, RulebookTopic, Rule, RuleRelation, rule_moments, rule_systems,
)
# 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.
from scribe.models.rule_moment_judgment import RuleMomentJudgment # noqa: E402, F401
from scribe.models.repo_binding import RepoBinding # noqa: E402, F401
from scribe.models.forge_connection import ForgeConnection # noqa: E402, F401
from scribe.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401
from scribe.models.system import System, RecordSystem # noqa: E402, F401
from scribe.models.design_system import DesignSystem, DesignToken # noqa: E402, F401
from grimoire.models.rule_moment_judgment import RuleMomentJudgment # noqa: E402, F401
from grimoire.models.repo_binding import RepoBinding # noqa: E402, F401
from grimoire.models.forge_connection import ForgeConnection # noqa: E402, F401
from grimoire.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401
from grimoire.models.system import System, RecordSystem # 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
# three.
from scribe.models.family import ( # noqa: E402, F401
from grimoire.models.family import ( # noqa: E402, F401
FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform,
FamilyIdeaReference, Platform, ProjectPlatform,
)
@@ -3,8 +3,8 @@ from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class ApiKey(Base, CreatedAtMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import DateTime, Float, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import iso
from grimoire.models import Base
from grimoire.models.base import iso
class AppLog(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -12,8 +12,8 @@ from sqlalchemy import (
)
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
# The classification vocabulary. `unclassified` is the default and
# 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.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -4,7 +4,7 @@ from pgvector.sqlalchemy import Vector
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text
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
# 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.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
# Each tuple is the whitelist its CHECK constraint enforces (migration 0120).
# A new value means DROP + ADD CONSTRAINT in the same migration.
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class ForgeConnection(Base, TimestampMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, TimestampMixin, iso
class Group(Base, TimestampMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from grimoire.models import Base
from grimoire.models.base import 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.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SUGGESTED = "suggested"
CONFIRMED = "confirmed"
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class Milestone(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
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.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class TaskStatus(str, enum.Enum):
@@ -212,6 +212,6 @@ class Note(Base, TimestampMixin, SoftDeleteMixin):
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)
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class NoteDraft(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class NoteSupersession(Base, CreatedAtMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
@@ -1,8 +1,8 @@
from sqlalchemy import ARRAY, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class NoteVersion(Base, CreatedAtMixin):
@@ -4,8 +4,8 @@ from sqlalchemy import DateTime, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class Notification(Base, CreatedAtMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin
class PasswordResetToken(Base, CreatedAtMixin):
@@ -2,8 +2,8 @@ import enum
from sqlalchemy import BigInteger, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class ProjectStatus(str, enum.Enum):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class RepoBinding(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Boolean, Float, Index, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
ON_POINT = "on_point"
ADJACENT = "adjacent"
@@ -4,8 +4,8 @@ from sqlalchemy import Boolean, DateTime, Float, Index, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import iso
from grimoire.models import Base
from grimoire.models.base import iso
class RetrievalLog(Base):
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy import DateTime, Float, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from grimoire.models import Base
class RetrievalTuningEvent(Base):
@@ -4,9 +4,9 @@ from sqlalchemy import BigInteger, DateTime, ForeignKey, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from scribe.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUGGESTED
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
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 —
# 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.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
@@ -3,8 +3,8 @@ from datetime import datetime
from sqlalchemy import BigInteger, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class RuleVersion(Base, CreatedAtMixin):
@@ -6,8 +6,8 @@ from sqlalchemy import (
)
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class Rulebook(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,7 +1,7 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from grimoire.models import Base
class Setting(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
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.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class System(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class TaskLog(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class User(Base, CreatedAtMixin):
@@ -1,6 +1,6 @@
"""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
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:
"""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:
if is_archive(f):
@@ -41,17 +41,17 @@ def load_backup(path: Path) -> dict:
async def _account_count() -> int:
from scribe.models import async_session
from scribe.models.user import User
from grimoire.models import async_session
from grimoire.models.user import User
async with async_session() as session:
return (await session.execute(select(func.count()).select_from(User))).scalar_one()
async def run(path: Path) -> int:
from scribe.models import engine
from scribe.services.backup import restore_full_backup
from scribe.services.backup_file import ArchiveError
from grimoire.models import engine
from grimoire.services.backup import restore_full_backup
from grimoire.services.backup_file import ArchiveError
try:
try:
@@ -83,7 +83,7 @@ async def run(path: Path) -> int:
def main(argv: list[str] | None = None) -> int:
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.",
)
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 scribe.auth import admin_required, login_required, get_current_user_id
from scribe.services.auth import (
from grimoire.auth import admin_required, login_required, get_current_user_id
from grimoire.services.auth import (
create_invitation,
delete_user,
is_registration_open,
@@ -13,15 +13,15 @@ from scribe.services.auth import (
revoke_invitation,
set_registration_open,
)
from scribe.services.backup import (
from grimoire.services.backup import (
export_full_backup,
export_user_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 scribe.services.logging import get_logs, get_log_stats, log_audit
from scribe.services.notifications import send_invitation_email
from scribe.services.settings import (
from grimoire.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email
from grimoire.services.logging import get_logs, get_log_stats, log_audit
from grimoire.services.notifications import send_invitation_email
from grimoire.services.settings import (
SECRET_MASK,
get_admin_setting,
set_admin_setting,
@@ -61,14 +61,14 @@ async def backup():
async def restore():
"""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
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."""
if request.mimetype == "application/json":
data = await request.get_json()
else:
import io
from scribe.services.backup_file import ArchiveError, read_archive
from grimoire.services.backup_file import ArchiveError, read_archive
try:
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_required
async def get_forge_webhook_settings():
from scribe.config import Config
from scribe.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
from grimoire.config import Config
from grimoire.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
webhook_secret = (
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_required
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 {}
uid = get_current_user_id()
@@ -270,8 +270,8 @@ async def update_base_url():
@admin_required
async def get_db_maintenance():
"""Current DB-maintenance config + the last run's summary."""
from scribe.services.db_maintenance import get_last_run
from scribe.services.db_maintenance_scheduler import (
from grimoire.services.db_maintenance import get_last_run
from grimoire.services.db_maintenance_scheduler import (
get_maintenance_hour,
is_maintenance_enabled,
)
@@ -286,7 +286,7 @@ async def get_db_maintenance():
@admin_required
async def get_db_maintenance_health():
"""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())
@@ -294,7 +294,7 @@ async def get_db_maintenance_health():
@admin_required
async def update_db_maintenance():
"""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 {}
enabled = bool(data.get("enabled", True))
try:
@@ -319,7 +319,7 @@ async def update_db_maintenance():
@admin_required
async def run_db_maintenance_now():
"""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()
await log_audit(
"db_maintenance_run", user_id=uid, username=g.user.username,
@@ -342,8 +342,8 @@ _BACKUP_CHECK_TIMEOUT = 45.0
async def _backups_payload() -> dict:
from datetime import datetime, timezone
from scribe.services import backup_destination as dest
from scribe.services import backup_job as job
from grimoire.services import backup_destination as dest
from grimoire.services import backup_job as job
config = await dest.load_config()
secret, stored = await dest.load_secret()
@@ -373,7 +373,7 @@ async def get_backups():
async def update_backups():
"""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."""
from scribe.services import backup_destination as dest
from grimoire.services import backup_destination as dest
data = await request.get_json() or {}
try:
@@ -403,7 +403,7 @@ async def check_backups_destination():
"""Ask the destination whether it would take a backup, without sending
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."""
from scribe.services import backup_destination as dest
from grimoire.services import backup_destination as dest
data = await request.get_json() or {}
try:
@@ -431,7 +431,7 @@ async def check_backups_destination():
async def run_backup_now():
"""Start a backup now. It runs in the background; the page follows it
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:
job.start_backup_now()
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request
from scribe.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.auth import login_required, get_current_user_id
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")
@@ -6,10 +6,10 @@ import secrets
from quart import Blueprint, g, jsonify, redirect, request, session
from scribe.auth import login_required, get_current_user_id
from scribe.config import Config
from scribe.rate_limit import is_rate_limited
from scribe.services.auth import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.config import Config
from grimoire.rate_limit import is_rate_limited
from grimoire.services.auth import (
authenticate,
change_password,
create_password_reset_token,
@@ -26,14 +26,14 @@ from scribe.services.auth import (
validate_invitation_token,
verify_password,
)
from scribe.services.logging import log_audit
from scribe.services.notifications import (
from grimoire.services.logging import log_audit
from grimoire.services.notifications import (
notify_security_event,
send_password_reset_email,
send_password_reset_success_email,
)
from scribe.services.email import get_base_url, is_smtp_configured
from scribe.services.oauth import (
from grimoire.services.email import get_base_url, is_smtp_configured
from grimoire.services.oauth import (
build_auth_url,
exchange_code,
get_userinfo,
@@ -13,10 +13,10 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import admin_required, get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import canonical_systems as canonical_svc
from scribe.services.projects import get_project_for_user
from grimoire.auth import admin_required, get_current_user_id, login_required
from grimoire.routes.utils import not_found
from grimoire.services import canonical_systems as canonical_svc
from grimoire.services.projects import get_project_for_user
logger = logging.getLogger(__name__)
@@ -1,8 +1,8 @@
"""Dashboard REST endpoint — the aggregated landing payload."""
from quart import Blueprint, g, jsonify
from scribe.auth import login_required
from scribe.services.dashboard import build_dashboard
from grimoire.auth import login_required
from grimoire.services.dashboard import build_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 scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import design_systems as ds_svc
from scribe.services.design_starter_roles import (
from grimoire.auth import get_current_user_id, login_required
from grimoire.routes.utils import not_found
from grimoire.services import design_systems as ds_svc
from grimoire.services.design_starter_roles import (
DEFAULT_TOKEN_PREFIX,
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")

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