Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
__pycache__/
*.pyc
*.egg-info/
build/
dist/
.venv/
.env
.coverage
Expand Down
31 changes: 25 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
# Unreleased

## Installer
- `scripts/install.sh` now skips starting services on a fresh install when `/etc/sacred-brain/*` still contains `CHANGE_ME` placeholders. Edit configs, then run `sudo ./scripts/install.sh --update` to start.
- Systemd units are discovered by globbing `ops/systemd/` instead of being listed in the script. New units added to the repo are picked up automatically.
- Added `--uninstall` (removes units, keeps state) and `--uninstall --purge` (also removes `/etc/sacred-brain`, `/var/lib/sacred-brain`, and the `sacred` user).
- Python venv is now `chown`'d to `sacred:sacred` so post-install pip operations from `just` recipes or hooks work without sudo.
- Added a `shellcheck` GitHub Actions workflow for `scripts/`.
## Installer — conventional pipx + Makefile

Sacred Brain is now installed as a proper Python package via `pipx`, driven by
a Makefile that honors `DESTDIR` / `PREFIX` / `SYSCONFDIR`.

- `pyproject.toml` ships console-script entry points: `hippocampus` (runs the
FastAPI app via uvicorn) and `memory-governor` (runs `memory_governor.app:run`).
Packages cover `brain.*`, `memory_governor.*`, and `sacred_brain.*`, including
`sacred_brain/prompts/sam_system.txt` as package data.
- New `Makefile` with `install`, `install-update`, `uninstall`, and
`uninstall-purge` targets. `make install` runs `pipx install --force .` into
`/opt/pipx/venvs/sacred-brain-hippocampus/` and symlinks the entry-point
binaries into `$(PREFIX)/bin/`.
- Systemd units now point at `/usr/local/bin/hippocampus` and
`/usr/local/bin/memory-governor` instead of an in-tree `.venv`. Timer-target
scripts use `/opt/pipx/venvs/sacred-brain-hippocampus/bin/python`.
- Migration: `make install` auto-detects and removes any pre-existing
`/opt/sacred-brain/.venv/` from the old in-tree-venv layout. Idempotent.
- `scripts/install.sh` is now a thin shim that exec's the Makefile, so existing
automation that calls `./scripts/install.sh [--update|--uninstall]` keeps
working.
- `sacred-search` is now installed by the Makefile to `$(PREFIX)/bin/`.
- Earlier in this cycle: skipped service start when configs contain `CHANGE_ME`,
switched to globbed unit discovery in `ops/systemd/`, added `--uninstall` /
`--uninstall --purge`, and added a `shellcheck` GitHub Actions workflow.

# v0.3.0 (2026-02-15)

Expand Down
149 changes: 149 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Sacred Brain installer
#
# Common targets:
# sudo make install Full install (user, dirs, package, units, configs)
# sudo make install-update Reinstall package + units, restart services
# sudo make uninstall Stop + disable + remove units (keeps state)
# sudo make uninstall-purge Also remove configs, state, and 'sacred' user
#
# Honors DESTDIR, PREFIX, SYSCONFDIR, PIPX_HOME for packagers and non-default
# installs:
# make install PREFIX=/usr SYSCONFDIR=/etc DESTDIR=/tmp/stage

PREFIX ?= /usr/local
SYSCONFDIR ?= /etc
DESTDIR ?=

ETC_DIR := $(DESTDIR)$(SYSCONFDIR)/sacred-brain
STATE_DIR := $(DESTDIR)/var/lib/sacred-brain
SYSTEMD_DIR := $(DESTDIR)/etc/systemd/system
BIN_DIR := $(DESTDIR)$(PREFIX)/bin

# pipx layout: a system-wide venv under /opt/pipx, with bins symlinked into
# $(PREFIX)/bin. This works on every pipx version (no --global needed) and
# keeps service binaries on the standard PATH.
PIPX_HOME ?= /opt/pipx
PIPX_BIN_DIR ?= $(PREFIX)/bin

REPO_DIR := $(shell pwd)

.PHONY: install install-update install-deps migrate-legacy install-package \
install-bin install-systemd install-config uninstall uninstall-purge help

help:
@sed -n '1,12p' Makefile

# ── Composite targets ───────────────────────────────────────────────

install: install-deps migrate-legacy install-package install-bin install-systemd install-config
@./scripts/post-install-message.sh "$(ETC_DIR)" || true

# Auto-cleanup of pre-pipx layout. Older installs put the venv at
# /opt/sacred-brain/.venv and the systemd ExecStarts pointed there. After
# this install, those paths are dead weight (and could confuse a future
# `pip install -e` if anyone runs it). Idempotent — silent if already clean.
migrate-legacy:
@if [ -d /opt/sacred-brain/.venv ]; then \
echo " [+] Removing legacy in-tree venv: /opt/sacred-brain/.venv"; \
rm -rf /opt/sacred-brain/.venv; \
fi
@if [ -e /opt/sacred-brain/.venv ]; then \
echo " [!] /opt/sacred-brain/.venv still present after cleanup — investigate"; \
fi

install-update: install-package install-systemd
systemctl daemon-reload
@for unit in $$(ls ops/systemd/*.service | xargs -n1 basename); do \
systemctl is-enabled $$unit >/dev/null 2>&1 && systemctl restart $$unit || true; \
done

# ── Phase: service user + state dirs ────────────────────────────────

install-deps:
@if ! id sacred >/dev/null 2>&1; then \
echo " [+] Creating service user 'sacred'"; \
useradd --system --shell /usr/sbin/nologin --home-dir /opt/sacred-brain sacred; \
else echo " [+] User 'sacred' already exists"; fi
@echo " [+] Creating state directories"
install -d -m 0755 -o sacred -g sacred $(STATE_DIR)/hippocampus
install -d -m 0755 -o sacred -g sacred $(STATE_DIR)/governor
install -d -m 0755 -o sacred -g sacred $(STATE_DIR)/cache
install -d -m 0755 -o sacred -g sacred $(ETC_DIR)

# ── Phase: Python package via pipx ──────────────────────────────────

install-package:
@command -v pipx >/dev/null || { echo "ERROR: pipx not installed (apt install pipx)"; exit 1; }
@echo " [+] Installing Python package via pipx into $(PIPX_HOME)"
PIPX_HOME=$(PIPX_HOME) PIPX_BIN_DIR=$(PIPX_BIN_DIR) \
pipx install --force "$(REPO_DIR)"

# ── Phase: shell scripts on PATH ────────────────────────────────────

install-bin:
@echo " [+] Installing shell utilities to $(BIN_DIR)"
install -d -m 0755 $(BIN_DIR)
install -m 0755 scripts/sacred-search $(BIN_DIR)/sacred-search

# ── Phase: systemd units ────────────────────────────────────────────

install-systemd:
@echo " [+] Installing systemd units to $(SYSTEMD_DIR)"
install -d -m 0755 $(SYSTEMD_DIR)
@for f in ops/systemd/*.service ops/systemd/*.timer; do \
[ -e "$$f" ] || continue; \
install -m 0644 "$$f" $(SYSTEMD_DIR)/; \
echo " $$(basename $$f)"; \
done
@if [ -z "$(DESTDIR)" ]; then \
systemctl daemon-reload; \
for f in ops/systemd/*.service ops/systemd/*.timer; do \
[ -e "$$f" ] || continue; \
if grep -q '^\[Install\]' "$$f"; then \
systemctl enable "$$(basename $$f)"; \
fi; \
done; \
fi

# ── Phase: config templates ─────────────────────────────────────────

install-config:
@echo " [+] Installing config templates to $(ETC_DIR)"
@for tmpl in ops/config/*.example; do \
[ -e "$$tmpl" ] || continue; \
target="$(ETC_DIR)/$$(basename $${tmpl%.example})"; \
if [ -f "$$target" ]; then \
echo " exists, skipping: $$target"; \
else \
install -m 0640 -o sacred -g sacred "$$tmpl" "$$target"; \
echo " created: $$target (edit CHANGE_ME values!)"; \
fi; \
done

# ── Uninstall ───────────────────────────────────────────────────────

uninstall:
@echo " [+] Stopping and disabling units"
@for f in ops/systemd/*.service ops/systemd/*.timer; do \
[ -e "$$f" ] || continue; \
unit=$$(basename $$f); \
if [ -f "$(SYSTEMD_DIR)/$$unit" ]; then \
systemctl disable --now "$$unit" 2>/dev/null || true; \
rm -f "$(SYSTEMD_DIR)/$$unit"; \
fi; \
done
@if [ -z "$(DESTDIR)" ]; then systemctl daemon-reload; fi
@echo " [+] Removing $(BIN_DIR)/sacred-search"
@rm -f $(BIN_DIR)/sacred-search
@echo " [+] Uninstalling Python package"
@PIPX_HOME=$(PIPX_HOME) PIPX_BIN_DIR=$(PIPX_BIN_DIR) \
pipx uninstall sacred-brain-hippocampus 2>/dev/null || true
@echo " [+] Kept $(ETC_DIR) and $(STATE_DIR) (use 'make uninstall-purge' to remove)"

uninstall-purge: uninstall
@echo " [+] Removing $(ETC_DIR) and $(STATE_DIR)"
rm -rf $(ETC_DIR) $(STATE_DIR)
@if id sacred >/dev/null 2>&1; then \
echo " [+] Removing service user 'sacred'"; \
userdel sacred 2>/dev/null || echo " [!] could not remove user 'sacred'"; \
fi
25 changes: 25 additions & 0 deletions brain/hippocampus/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Console entry point for the Hippocampus service.

Installed by pyproject's [project.scripts] as `hippocampus`. Reads
HIPPOCAMPUS_HOST / HIPPOCAMPUS_PORT from the environment so the systemd
unit and dev runs share configuration.
"""

from __future__ import annotations

import os

import uvicorn


def main() -> None:
uvicorn.run(
"brain.hippocampus.app:app",
host=os.environ.get("HIPPOCAMPUS_HOST", "0.0.0.0"),
port=int(os.environ.get("HIPPOCAMPUS_PORT", "54321")),
reload=False,
)


if __name__ == "__main__":
main()
11 changes: 8 additions & 3 deletions brain/hippocampus/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,17 @@ async def delete_memory(
@application.get("/memories/{user_id}", response_model=MemoryQueryResponse)
async def query_memories(
user_id: str,
query: str = Query(..., min_length=1),
limit: int | None = Query(None, ge=1, le=100),
query: str | None = Query(None),
limit: int | None = Query(None, ge=1, le=1000),
adapter: Mem0Adapter = Depends(get_adapter),
_: None = Depends(auth_dependency),
) -> MemoryQueryResponse:
records = adapter.query_memories(user_id=user_id, query=query, limit=limit)
# query present -> semantic search; absent -> list all (dreaming /
# governor recall-fallback need list-all, which used to 422).
if query and query.strip():
records = adapter.query_memories(user_id=user_id, query=query, limit=limit)
else:
records = adapter.list_memories(user_id=user_id, limit=limit)
return MemoryQueryResponse(memories=records)

@application.post("/summaries", response_model=SummaryResponse)
Expand Down
Loading
Loading