--- /dev/null
+# Keep host-specific runtimes and models out of the clean-install test context.
+.git
+.idea
+.venv
+.hf-cache
+**/__pycache__
+**/*.py[cod]
+**/*.html
| Runtime deps | =model2vec=, =sqlite-vec= |
| Recall | =prefetch()= injects up to 30 memories per turn; ids already returned in the session are filtered out |
| Embedding model | =minishlab/potion-multilingual-128M= (~540 MB, cached offline; the repo's unused ONNX export is skipped) |
-| Plugin metadata | plugin.yaml (version 1.2.0) |
+| Plugin metadata | plugin.yaml (version 1.3.0) |
| Public docs site | https://www.svjatoslav.eu/projects/ (served from Documentation/) |
* Purpose
│ ├── __init__.py
│ ├── memory.py # MemoryStore: model2vec embed + sqlite-vec CRUD
│ ├── provider.py # RetinueMemoryProvider: MemoryProvider ABC impl
+│ ├── bootstrap.py # Idempotent installer seed memories
│ └── system_prompt_block.txt # Injected into the Hermes system prompt
├── Install Retinue # Deploy into $HERMES_HOME/plugins/retinue/
-├── test/test_standalone.py # End-to-end MemoryStore + provider tests
+├── test/
+│ ├── test_standalone.py # End-to-end MemoryStore + provider tests
+│ └── clean-install/ # Disposable Docker install + activation test
├── Documentation/
│ ├── index.org # Public user documentation (exports to HTML)
│ └── Development/index.org # Architecture & integration documentation
├── Tools/ # Development helpers (filenames contain spaces)
│ ├── lib/common.sh # Shared bash helpers: RETINUE_PY resolution (venv → uv → Hermes venv)
│ ├── Run tests # Standalone smoke test runner (no Hermes needed)
+│ ├── Test clean installation # Official Hermes + Retinue in clean Docker
│ ├── Show Retinue memory contents # Debug CLI: dump/search all memories
│ ├── Import Holographic memories # Migrate facts from Holographic memory_store.db
│ ├── Open with IntelliJ IDEA
The installer is idempotent: it wipes everything under
=$HERMES_HOME/plugins/retinue/= except =.venv= / =.hf-cache=, copies the
-whitelist =(plugin.yaml __init__.py src)=, installs missing Python deps
-into the interpreter Hermes actually runs, pre-downloads the embedding
-model into =.hf-cache/= (no first-use stall), sets =memory.provider:
-retinue=, enables the =memory= toolset for the CLI platform (clean
-"Blank Slate" installs disable it — see pitfalls), and validates the
-plugin imports.
+whitelist =(plugin.yaml __init__.py src)=, sets =memory.provider: retinue=,
+and prepares dependencies through Hermes PM on current Hermes versions
+(legacy pip / ensurepip / uv fallbacks remain for older versions). It
+pre-downloads the embedding model, adds two non-duplicating example memories
+(Retinue's operation and the quoted installation source), enables the =memory=
+toolset for the CLI platform, and validates the plugin imports.
To target a non-default Hermes home:
hermes chat -q "What shell do I use?"
#+end_src
+** Test clean installation in Docker
+:PROPERTIES:
+:CUSTOM_ID: test-clean-installation-in-docker
+:ID: 931163de-ffb6-4c24-a46f-0b0e3a23b724
+:END:
+
+#+begin_src sh
+cd /path/to/retinue
+bash "Tools/Test clean installation"
+#+end_src
+
+Builds a fresh Debian 13 image, installs the official stable Hermes release
+as a non-root user, then installs Retinue twice. The verifier uses Hermes' real
+plugin loader and checks automatic activation, the CLI memory toolset, all
+four Retinue tools, two non-duplicating seed memories, and offline semantic
+search. No host Hermes state or LLM credentials are used. Forward =--no-cache=
+to repeat the official Hermes installation from scratch as well.
+
** Inspect the memory store from the command line
#+begin_src sh
:ID: 6f0702f2-2957-4083-a602-caa709ed046e
:END:
-- *Python on Debian 13 is PEP 668.* System pip is blocked. The installer
- deliberately installs deps into the Python that runs Hermes
- (=PYTHON= env override, =$HERMES_HOME/hermes-agent/venv/bin/python3=,
- or the =hermes= launcher shebang). That interpreter is a uv-created venv
- without pip (see pitfall below), so dependency installation tries pip,
- then =ensurepip=-bootstrapped pip, then =uv= from =PATH= or
- =$HERMES_HOME/bin/uv=. Do not pip-install into the system Python from
- this project.
+- *Python and dependencies on current Hermes.* Set =memory.provider= first,
+ then run =hermes pm install= so PM builds Retinue's declared requirements
+ into the managed generation. Activate the result with
+ =hermes --run-module pm.environments --format sh= and use that Python and
+ =PYTHONPATH= for model preload, seeding, and validation. Do not pip-install
+ into a guessed PM generation. The legacy pip → ensurepip → uv chain remains
+ for older Hermes versions and explicit =PYTHON= overrides; Debian 13 system
+ Python is still PEP 668 and is never the install target.
+- *Platform support.* The installer supports Linux (including WSL2) and
+ Apple-Silicon macOS. Native Windows and Android/Termux are unsupported.
+ The =sqlite-vec= dependency excludes Android in =plugin.yaml= because its
+ missing Android wheel otherwise makes Hermes PM's universal lock fail on
+ every host.
- *Filenames with spaces.* Everything under =Tools/= contains spaces —
quote paths in shell commands.
- *The =Tools/Open with IntelliJ IDEA= helper* opens this tree as an
hermes= fails even though Hermes is installed. The installer probes
=~/.local/bin/hermes=, =~/bin/hermes=, =/usr/local/bin=, =/usr/bin=
explicitly; keep that probing intact.
+- *Pitfall: current Hermes launchers are shell scripts.* A launcher whose
+ first line is =#!/bin/sh= is not a Python interpreter. Reading that shebang
+ made the old installer run dependency commands as =/bin/sh -m pip= and fail
+ every clean install. Current versions must go through Hermes PM and its
+ activation environment instead of launcher/shebang guessing.
- *Pitfall: bare Debian 13 lacks python3-venv.* =python3 -m venv .venv=
fails without the =python3.X-venv= apt package (no ensurepip) and —
worse — leaves a half-created =.venv= (python binary present, pip and
│ ├── __init__.py # Package init
│ ├── memory.py # Memory engine: embeddings + sqlite-vec
│ ├── provider.py # MemoryProvider subclass
+│ ├── bootstrap.py # Idempotent installer seed memories
│ ├── cli.py # Debug CLI behind "Show Retinue memory contents"
│ ├── import_holographic.py # Holographic memory_store.db importer
│ └── system_prompt_block.txt # Injected into the Hermes system prompt
├── test/
-│ └── test_standalone.py # End-to-end MemoryStore + provider tests
+│ ├── test_standalone.py # End-to-end MemoryStore + provider tests
+│ └── clean-install/ # Disposable Docker install + activation test
├── Tools/ # Development helpers (desktop scripts)
│ ├── lib/common.sh # Shared bash helpers: RETINUE_PY resolution
│ ├── Run tests # Standalone smoke test runner
+│ ├── Test clean installation # Official Hermes + Retinue in clean Docker
│ ├── Show Retinue memory contents # Debug CLI: dump/search all memories
│ ├── Import Holographic memories # Migrate facts from Holographic memory_store.db
│ ├── Open with IntelliJ IDEA
What it does, in order:
-1. Detects the active Hermes home from the =HERMES_HOME= environment variable,
- or falls back to =$HOME/.hermes=.
+1. Detects the active Hermes home from =HERMES_HOME=, or falls back to
+ =$HOME/.hermes=.
2. Creates =$HERMES_HOME/plugins/retinue/= and its =.hf-cache/= subdirectory
if they do not exist.
3. Removes every previously deployed item in the plugin directory except the
=PRESERVE= list (=.venv=, =.hf-cache=), so stale code cannot linger.
4. Copies the whitelist =(plugin.yaml __init__.py src)= from the workspace
into the plugin directory with =cp=.
-5. Resolves the Python interpreter that Hermes actually runs under (via the
- =PYTHON= environment variable, common venv layouts, or the =hermes=
- launcher shebang) and installs any missing =sqlite-vec= / =model2vec=
- dependencies into it, trying in order: =pip=, =ensurepip=-bootstrapped
- =pip=, then =uv pip install --python= (uv is probed on =PATH= and at
- =$HERMES_HOME/bin/uv= — Hermes bundles it there). The chain exists
- because the Hermes runtime venv is created by =uv venv= and ships
- without pip.
-6. Pre-downloads the embedding model into =.hf-cache/= by calling
- =retinue.memory.get_model()= with the Hermes Python — the same code path
- the plugin uses at runtime. The snapshot download skips the repo's
- unused ~512 MB ONNX export (=ignore_patterns=["onnx/*"]=) and loads from
- the returned local path, so the cache is ~540 MB and the first Hermes
- session does not stall. Failure here is non-fatal: a warning is printed
- and the model simply downloads on first use instead.
-7. Sets =memory.provider: retinue= with =hermes config set= if the CLI is
- available.
-8. Enables the =memory= toolset for the CLI platform with =hermes tools
- enable --platform cli memory=. Clean "Blank Slate" Hermes installs list
- =memory= in =agent.disabled_toolsets= and exclude it from
- =platform_toolsets.cli=, which makes Hermes withhold the
- =retinue_memory_*= tools and the system-prompt block from the model even
- with the provider selected; this step lifts both gates. Other platforms
- need their own =--platform <name>= run.
-9. Runs a lightweight import check to confirm the plugin loads.
+5. Sets =memory.provider: retinue= with =hermes config set=. This happens
+ before dependency preparation because PM discovers the active provider
+ from the configuration.
+6. Prepares Python dependencies. Current Hermes versions use Hermes PM
+ (=hermes pm install=), which builds the plugin's declared requirements into
+ the managed dependency generation. The activation interface
+ (=hermes --run-module pm.environments --format sh=) then selects the same
+ Python and site-packages a fresh Hermes process uses. The =sqlite-vec=
+ requirement excludes Android because that platform has no published wheel,
+ preventing an impossible branch in uv's universal lock. Older Hermes
+ versions retain the pip → =ensurepip=-bootstrapped pip → =uv= fallback.
+7. Pre-downloads the embedding model into =.hf-cache/= by calling
+ =retinue.memory.get_model()= with the Hermes Python. The snapshot skips the
+ unused ONNX export. Failure is non-fatal: the model can download on first
+ use instead.
+8. Calls =retinue.bootstrap= to add the Retinue explanation and quoted
+ installation-source memories when their exact content is absent. Existing
+ user memories are untouched; identical seeds are not duplicated. If the
+ model is unavailable, seeding waits for the next successful install.
+9. Enables the =memory= toolset for the CLI platform with =hermes tools
+ enable --platform cli memory=. Other platforms need their own
+ =--platform <name>= run.
+10. Runs a lightweight import check to confirm the plugin loads and exposes
+ =retinue_memory_add=, =retinue_memory_search=, =retinue_memory_delete=,
+ and =retinue_memory_stats=.
Every fallible step captures its combined output into a temp directory
(cleaned by an EXIT trap) and prints an excerpt on failure — stderr is
Prerequisites:
+- Linux required. Windows and Android/Termux are not supported.
- Hermes Agent must be installed. The =hermes= CLI does not need to be on
=PATH= — the installer also probes the standard locations
(=~/.local/bin/hermes=, =~/bin/hermes=, =/usr/local/bin=, =/usr/bin=),
which matters when installing over a non-login SSH shell where
=~/.profile= has not run.
-- Nothing else: the installer uses the Python interpreter that Hermes
- itself runs (=~/.hermes/hermes-agent/venv=), so it works even on a bare
- Debian/Ubuntu server without the =python3-venv= package.
+- Nothing else: the installer uses Hermes' own Python and dependency
+ manager.
Run the bundled installer from the Retinue source tree:
The installer performs five steps:
-1. Detects the active Hermes home from the =HERMES_HOME= environment variable
- or falls back to =$HOME/.hermes=.
-2. Resolves the Python interpreter that Hermes uses and installs =sqlite-vec=
- and =model2vec= there if they are missing — via pip when present, via
- =ensurepip=-bootstrapped pip otherwise, and finally via =uv= (Hermes
- bundles one at =~/.hermes/bin/uv=), which also covers uv-created Hermes
- venvs that ship without pip.
-3. Copies the Retinue source into =$HERMES_HOME/plugins/retinue/=, preserving
- any existing runtime data.
-4. Pre-downloads the embedding model
- (=minishlab/potion-multilingual-128M=, ~540 MB — the repo's unused
- ONNX export is skipped) into
- =$HERMES_HOME/plugins/retinue/.hf-cache/= so the first Hermes session
- does not stall on the download. The cache is preserved across
- re-installs, so this step is a no-op after the first successful run.
-5. Sets =memory.provider: retinue= and enables the =memory= toolset for the
- CLI platform (=hermes tools enable --platform cli memory=). A clean
- "Blank Slate" Hermes install disables the memory toolset, which would
- withhold the =retinue_memory_*= tools from the agent even with the
- provider selected. Other platforms (telegram, discord, ...) each need
- their own =hermes tools enable --platform <name> memory=.
+1. Detects the active Hermes home from =HERMES_HOME= (default:
+ =$HOME/.hermes=) and copies the Retinue plugin into its =plugins/retinue/=
+ directory, preserving the model cache and existing memory database.
+2. Sets =memory.provider: retinue= and prepares =sqlite-vec= plus =model2vec=.
+ Current Hermes versions use Hermes PM; older versions use pip,
+ =ensurepip=-bootstrapped pip, or =uv=.
+3. Pre-downloads =minishlab/potion-multilingual-128M= (~540 MB, without the
+ unused ONNX export) into =$HERMES_HOME/plugins/retinue/.hf-cache/= so the
+ first Hermes session does not stall. The cache survives reinstalls.
+4. Adds two memories when their exact content is absent: an explanation of
+ Retinue's database, embedding model, sqlite-vec index and automatic
+ prefetch; and the quoted source directory with the command for reinstalling
+ from that checkout. Reinstalling does not duplicate either memory.
+5. Enables the =memory= toolset for the CLI platform and validates the plugin
+ and all four =retinue_memory_*= tools. Other platforms (telegram, discord,
+ ...) each need their own =hermes tools enable --platform <name> memory=.
Verify the plugin is active:
For technical details, read: [[file:Development/index.org::#what-the-installer-does][What the installer does.]]
+** Testing a clean installation
+:PROPERTIES:
+:CUSTOM_ID: testing-a-clean-installation
+:ID: f28f8be8-63df-44a5-977e-40d6bbe61ec2
+:END:
+
+Run the Docker-based clean-install test from the Retinue source tree:
+
+#+begin_src sh
+cd /path/to/retinue
+bash "Tools/Test clean installation"
+#+end_src
+
+The test creates a fresh Debian 13 environment, installs the official stable
+Hermes release as a non-root user, and installs Retinue from this source tree.
+It uses no host Hermes configuration, credentials, plugins, Python environment,
+or model cache, and needs no LLM credentials.
+
+Both the first install and a second idempotency check must pass. The test
+verifies automatic provider activation, the CLI memory toolset, all four
+Retinue tools, exactly two seed memories without duplicates, and semantic
+search with Hugging Face access disabled.
+
+The first run downloads Hermes, its dependencies, and the embedding model;
+allow about 5–15 minutes. Later runs reuse the clean Hermes installation layer
+and are faster. To repeat that installation from scratch too:
+
+#+begin_src sh
+bash "Tools/Test clean installation" --no-cache
+#+end_src
+
** Migrating from Holographic memory
:PROPERTIES:
:CUSTOM_ID: migrating-from-holographic
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
PLUGIN_DIR="$HERMES_HOME/plugins/retinue"
+# Find the CLI before resolving Python. Current Hermes launchers are shell
+# wrappers around PM-managed Python, so their shebang is NOT an interpreter.
+# Probe standard locations explicitly for non-login SSH shells.
+find_hermes_cli() {
+ local found candidate
+ found="$(command -v hermes 2>/dev/null || true)"
+ if [[ -z "$found" ]]; then
+ for candidate in "$HOME/.local/bin/hermes" "$HOME/bin/hermes" \
+ /usr/local/bin/hermes /usr/bin/hermes; do
+ if [[ -x "$candidate" ]]; then
+ found="$candidate"
+ break
+ fi
+ done
+ fi
+ echo "$found"
+}
+
+HERMES_CLI="$(find_hermes_cli)"
+
# Resolve the Python interpreter that actually runs Hermes. Plugin deps must
# live in that environment, not the user's system Python.
resolve_hermes_python() {
command -v "$PYTHON" >/dev/null 2>&1 && { echo "$PYTHON"; return; }
fi
+ # Set by activate_pm_environment(): PATH now selects the same managed
+ # Python and PYTHONPATH the next Hermes process will use.
+ if (( PM_ENV_ACTIVE )); then
+ command -v python3
+ return
+ fi
+
# Common Hermes git install layout.
local venv_python="$HERMES_HOME/hermes-agent/venv/bin/python3"
if [[ -x "$venv_python" ]]; then
if [[ "$shebang" == \#\!* ]]; then
shebang="${shebang#\#!}"
shebang="$(echo -n "$shebang" | sed 's/^[[:space:]]*//')"
- if [[ -x "$shebang" ]]; then
+ if [[ "$shebang" == *python* && -x "$shebang" ]]; then
echo "$shebang"
return
fi
echo "python3"
}
-PYTHON="$(resolve_hermes_python)"
+PM_ENV_ACTIVE=0
+PYTHON="${PYTHON:-}"
+
+# Hermes PM owns current dependency generations. Its activation output is the
+# public shell interface for selecting the exact Python and site-packages that
+# a fresh Hermes process will use; never pip-install into a guessed generation.
+activate_pm_environment() {
+ local exports
+ if ! exports="$("$HERMES_CLI" --run-module pm.environments --format sh \
+ 2>"$DIAG_DIR/pm-environment.log")"; then
+ echo "ERROR: could not resolve Hermes' PM-managed Python environment."
+ show_log_tail "$DIAG_DIR/pm-environment.log" 20
+ return 1
+ fi
+ eval "$exports"
+ PM_ENV_ACTIVE=1
+}
while [[ $# -gt 0 ]]; do
case "$1" in
return 1
}
-ensure_python_deps
+# Select the provider before dependency preparation: Hermes PM discovers the
+# active memory provider from config.yaml and includes its plugin.yaml
+# python_dependencies in the managed environment.
+if [[ -n "$HERMES_CLI" ]]; then
+ echo "Setting memory.provider to retinue (via $HERMES_CLI)..."
+ if "$HERMES_CLI" config set memory.provider retinue >"$DIAG_DIR/config-set.log" 2>&1; then
+ echo " memory.provider set to retinue."
+ else
+ echo "ERROR: could not activate Retinue automatically."
+ echo " --- hermes config set output:"
+ show_log_tail "$DIAG_DIR/config-set.log"
+ exit 1
+ fi
+else
+ echo "Warning: hermes CLI not found. Set memory.provider: retinue manually in:"
+ echo " $HERMES_HOME/config.yaml"
+fi
+
+# Current Hermes uses PM-managed Python generations. An explicit `pm install`
+# is the supported way to add the newly selected provider's declared deps.
+# Older Hermes versions have no PM; those retain the pip/ensurepip/uv chain.
+USING_PM=0
+if [[ -z "${PYTHON:-}" && -n "$HERMES_CLI" ]] && \
+ "$HERMES_CLI" pm install --help >"$DIAG_DIR/pm-install-help.log" 2>&1; then
+ USING_PM=1
+ echo "Preparing Retinue dependencies through Hermes PM..."
+ if "$HERMES_CLI" pm install >"$DIAG_DIR/pm-install.log" 2>&1; then
+ show_log_tail "$DIAG_DIR/pm-install.log" 5
+ echo " Hermes PM dependency environment ready."
+ else
+ echo "ERROR: Hermes PM could not prepare Retinue's dependencies."
+ echo " --- hermes pm install output:"
+ show_log_tail "$DIAG_DIR/pm-install.log" 30
+ exit 1
+ fi
+ activate_pm_environment
+fi
+
+PYTHON="$(resolve_hermes_python)"
+if (( USING_PM )); then
+ if ! "$PYTHON" -c "import sqlite_vec, model2vec" \
+ >"$DIAG_DIR/pm-import-check.log" 2>&1; then
+ echo "ERROR: Hermes PM finished, but Retinue's dependencies are not importable."
+ show_log_tail "$DIAG_DIR/pm-import-check.log" 20
+ exit 1
+ fi
+else
+ ensure_python_deps
+fi
# Pre-download the embedding model into the plugin-local cache so the first
# Hermes session does not hang on a ~500 MB download.
show_log_tail "$log" 20
echo " It will download on first use inside Hermes."
echo " (Check network / Hugging Face connectivity.)"
+ return 1
fi
}
-preload_embedding_model
-
-# Set Hermes memory provider if the CLI is available.
-# Same non-login-shell problem as above: hermes usually lives in ~/.local/bin,
-# so probe the standard locations instead of relying on PATH alone.
-HERMES_CLI="$(command -v hermes 2>/dev/null || true)"
-if [[ -z "$HERMES_CLI" ]]; then
- for candidate in "$HOME/.local/bin/hermes" "$HOME/bin/hermes" \
- /usr/local/bin/hermes /usr/bin/hermes; do
- if [[ -x "$candidate" ]]; then
- HERMES_CLI="$candidate"
- break
- fi
- done
-fi
-
-if [[ -n "$HERMES_CLI" ]]; then
- echo "Setting memory.provider to retinue (via $HERMES_CLI)..."
- if "$HERMES_CLI" config set memory.provider retinue >"$DIAG_DIR/config-set.log" 2>&1; then
- echo " memory.provider set to retinue."
+seed_example_memories() {
+ echo "Seeding the Retinue explanation and installation-source memories..."
+ export HF_HOME="$PLUGIN_DIR/.hf-cache"
+ local log="$DIAG_DIR/seed-examples.log"
+ if HERMES_HOME="$HERMES_HOME" \
+ RETINUE_SOURCE_DIR="$SOURCE_DIR" \
+ PYTHONPATH="$PLUGIN_DIR/src${PYTHONPATH:+:$PYTHONPATH}" \
+ "$PYTHON" -m retinue.bootstrap >"$log" 2>&1; then
+ show_log_tail "$log" 3
else
- echo " Warning: could not set memory.provider automatically."
- echo " --- hermes config set output:"
- show_log_tail "$DIAG_DIR/config-set.log"
- echo " Set it manually in $HERMES_HOME/config.yaml:"
- echo " memory:"
- echo " provider: retinue"
+ echo "ERROR: could not seed the example memories."
+ show_log_tail "$log" 20
+ return 1
fi
+}
+
+if preload_embedding_model; then
+ seed_example_memories
else
- echo "Warning: hermes CLI not found. Set memory.provider: retinue manually in:"
- echo " $HERMES_HOME/config.yaml"
+ echo " Warning: example memories were not seeded because the model is unavailable."
+ echo " Re-run Install Retinue with network access to add them."
fi
# Enable the memory toolset for the CLI platform. Clean "Blank Slate" Hermes
--- /dev/null
+#!/usr/bin/env bash
+# Test clean installation - build a disposable Debian 13 image, install the
+# official Hermes release, then install Retinue twice and verify real activation.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+IMAGE="${RETINUE_TEST_IMAGE:-retinue-clean-install-test:latest}"
+
+if ! command -v docker >/dev/null 2>&1; then
+ echo "ERROR: Docker is required for the clean-install test." >&2
+ exit 1
+fi
+if ! docker info >/dev/null 2>&1; then
+ echo "ERROR: Docker is installed but its daemon is not reachable by $USER." >&2
+ echo " Check 'docker info' and your Docker group membership." >&2
+ exit 1
+fi
+
+# Extra arguments are forwarded to docker build. Use --no-cache to repeat even
+# the official Hermes installation from scratch; source edits already invalidate
+# every Retinue install and verification layer without that slower option.
+docker build \
+ --progress=plain \
+ --tag "$IMAGE" \
+ --file "$PROJECT_ROOT/test/clean-install/Dockerfile" \
+ "$@" \
+ "$PROJECT_ROOT"
name: retinue
-version: 1.2.0
+version: 1.3.0
description: "Semantic memory provider for Hermes Agent using model2vec and sqlite-vec."
hooks:
- sync_turn
# so Retinue's is_available() keeps passing after updates. Keep upper bounds
# per the Hermes dependency policy.
python_dependencies:
- - "sqlite-vec>=0.1.9,<0.2"
+ # sqlite-vec publishes Linux, macOS and Windows wheels, but no Android build.
+ # The marker keeps Hermes PM's universal lock from solving an impossible
+ # Android branch; Retinue remains unavailable there rather than breaking
+ # dependency installation on every supported host.
+ - "sqlite-vec>=0.1.9,<0.2; sys_platform != 'android'"
- "model2vec>=0.9,<0.10"
--- /dev/null
+"""One-time, idempotent memories created by the Retinue installer."""
+
+from __future__ import annotations
+
+import json
+import os
+import socket
+from pathlib import Path
+
+from retinue.memory import MemoryStore
+
+
+def _quoted(value: str) -> str:
+ """Quote a path or hostname without breaking on embedded quotes."""
+ return json.dumps(value, ensure_ascii=False)
+
+
+def seed_example_memories(
+ base_dir: str | Path,
+ source_dir: str | Path,
+ *,
+ hostname: str | None = None,
+) -> dict[str, int]:
+ """Add the two installer examples if their exact content is absent.
+
+ Existing user memories are never edited or removed. Repeating an install
+ from the same source into the same Hermes home adds nothing.
+ """
+ store = MemoryStore(Path(base_dir).resolve())
+ source = Path(source_dir).resolve()
+ host = hostname or socket.gethostname()
+ contents = [
+ (
+ "Retinue is the local semantic memory provider for Hermes Agent. "
+ f"For this installation, Retinue stores memories in {_quoted(str(store.db_path))}, "
+ "embeds text with minishlab/potion-multilingual-128M, indexes "
+ "embeddings with sqlite-vec, and uses prefetch to inject relevant "
+ "memories automatically before each turn."
+ ),
+ (
+ f"Retinue installation source on host {_quoted(host)} is "
+ f"{_quoted(str(source))}. To reinstall or update Retinue from this "
+ f"source checkout, run: bash {_quoted(str(source / 'Install Retinue'))}."
+ ),
+ ]
+
+ added = 0
+ existing = 0
+ try:
+ for content in contents:
+ if store.find_exact(content):
+ existing += 1
+ else:
+ store.add(content)
+ added += 1
+ total = len(store.list())
+ finally:
+ store.close()
+ return {"added": added, "existing": existing, "total": total}
+
+
+def main() -> int:
+ """Seed from installer-supplied paths; no command-line arguments needed."""
+ hermes_home = os.environ.get("HERMES_HOME")
+ source_dir = os.environ.get("RETINUE_SOURCE_DIR")
+ if not hermes_home:
+ raise SystemExit("HERMES_HOME is required to seed Retinue memories")
+ if not source_dir:
+ raise SystemExit("RETINUE_SOURCE_DIR is required to seed Retinue memories")
+ result = seed_example_memories(hermes_home, source_dir)
+ print(
+ "Example memories ready: "
+ f"{result['added']} added, {result['existing']} already present."
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
--- /dev/null
+FROM debian:trixie-slim
+
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ bash \
+ ca-certificates \
+ curl \
+ git \
+ libatomic1 \
+ python3 \
+ sudo \
+ tar \
+ && rm -rf /var/lib/apt/lists/*
+
+# Exercise the normal non-root installation path. Passwordless sudo is needed
+# only if Hermes has to install a missing operating-system library.
+RUN useradd --create-home --shell /bin/bash tester \
+ && echo 'tester ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/tester
+
+USER tester
+ENV HOME=/home/tester \
+ PATH=/home/tester/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+ CI=1
+WORKDIR /home/tester
+
+# This is the only Hermes installation in the image: no host checkout, config,
+# credentials, plugin, model cache, or Python environment is mounted in.
+RUN curl -fsSL https://hermes-agent.nousresearch.com/install.sh \
+ | bash -s -- --non-interactive --skip-browser --skip-computer-use
+
+WORKDIR /home/tester/retinue
+COPY --chown=tester:tester . /home/tester/retinue/
+
+# Verify the first install, then reinstall and verify again. The second pass
+# proves idempotency, including that installer-seeded memories do not multiply.
+RUN bash "Install Retinue" \
+ && eval "$(hermes --run-module pm.environments --format sh)" \
+ && export HF_HOME="$HOME/.hermes/plugins/retinue/.hf-cache" HF_HUB_OFFLINE=1 \
+ && python3 test/clean-install/verify.py
+
+RUN bash "Install Retinue" \
+ && eval "$(hermes --run-module pm.environments --format sh)" \
+ && export HF_HOME="$HOME/.hermes/plugins/retinue/.hf-cache" HF_HUB_OFFLINE=1 \
+ && python3 test/clean-install/verify.py
+
+CMD ["/bin/bash"]
--- /dev/null
+#!/usr/bin/env python3
+"""Verify a clean Retinue install through Hermes' real plugin loader.
+
+Run with the Python and environment produced by:
+ eval "$(hermes --run-module pm.environments --format sh)"
+No model provider or API credential is needed.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+from pathlib import Path
+from typing import Any, cast
+
+EXPECTED_TOOLS = {
+ "retinue_memory_add",
+ "retinue_memory_search",
+ "retinue_memory_delete",
+ "retinue_memory_stats",
+}
+SOURCE_DIR = Path(__file__).resolve().parents[2]
+HERMES_HOME = Path(
+ os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))
+).resolve()
+
+
+def run(command: list[str]) -> str:
+ result = subprocess.run(
+ command,
+ check=False,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ output = result.stdout
+ if result.returncode != 0:
+ raise AssertionError(
+ f"command exited {result.returncode}: {' '.join(command)}\n{output}"
+ )
+ return output
+
+
+def require(condition: bool, message: str) -> None:
+ if not condition:
+ raise AssertionError(message)
+
+
+def main() -> None:
+ status = run(["hermes", "memory", "status"])
+ require("Provider:" in status and "retinue" in status,
+ f"Retinue is not the selected provider:\n{status}")
+ require("available" in status and "✓" in status,
+ f"Retinue is not available through Hermes:\n{status}")
+
+ selected = run(["hermes", "config", "get", "memory.provider"])
+ require("retinue" in selected,
+ f"memory.provider was not activated automatically:\n{selected}")
+
+ tools = run(["hermes", "tools", "list", "--platform", "cli"])
+ memory_lines = [line for line in tools.splitlines()
+ if " memory " in line and "Memory" in line]
+ require(len(memory_lines) == 1 and "✓ enabled" in memory_lines[0],
+ f"the CLI memory toolset is not enabled:\n{tools}")
+
+ # This is Hermes' own discovery and construction path, not a direct import
+ # that could hide a broken plugin registration.
+ from plugins.memory import load_memory_provider
+
+ provider = cast(Any, load_memory_provider("retinue"))
+ require(provider is not None, "Hermes could not load the Retinue provider")
+ require(provider.is_available(), "Retinue reports unavailable after install")
+ require({schema["name"] for schema in provider.get_tool_schemas()} == EXPECTED_TOOLS,
+ "Retinue's four tools were not registered correctly")
+
+ provider.initialize("clean-install-verification", hermes_home=str(HERMES_HOME))
+ try:
+ stats = json.loads(provider.handle_tool_call("retinue_memory_stats", {}))
+ require(stats.get("success") is True, f"stats tool failed: {stats}")
+ result = stats["result"]
+ require(result["total"] == 2,
+ f"fresh install must contain exactly two seed memories; got {result}")
+ require(Path(result["db_path"]).resolve()
+ == HERMES_HOME / "retinue_memory.db",
+ f"Retinue opened the wrong database: {result}")
+
+ rows = provider._store.list() # noqa: SLF001 - inspect real persisted rows
+ contents = [row[1] for row in rows]
+ require(any(
+ "Retinue" in text
+ and "minishlab/potion-multilingual-128M" in text
+ and "sqlite-vec" in text
+ and "prefetch" in text
+ for text in contents
+ ),
+ f"the Retinue explanation seed is missing or incomplete: {contents}")
+ require(any(
+ "Retinue installation source" in text
+ and f'"{SOURCE_DIR}"' in text
+ and f'"{SOURCE_DIR}/Install Retinue"' in text
+ for text in contents
+ ),
+ f"the quoted installation-source seed is missing: {contents}")
+
+ found = json.loads(provider.handle_tool_call(
+ "retinue_memory_search",
+ {
+ "query": "How does Retinue store and automatically recall memories?",
+ "limit": 5,
+ "include_seen": True,
+ },
+ ))
+ require(found.get("success") is True and found["result"],
+ f"semantic search over the seed memories failed: {found}")
+ require(any("sqlite-vec" in row["content"] for row in found["result"]),
+ f"the Retinue explanation was not semantically retrievable: {found}")
+ finally:
+ provider.shutdown()
+
+ print("PASS: Hermes loads Retinue and selects it automatically")
+ print("PASS: the CLI memory toolset and all four Retinue tools are available")
+ print("PASS: exactly two useful seed memories persist and remain idempotent")
+ print("PASS: semantic search works with the preloaded model in offline mode")
+
+
+if __name__ == "__main__":
+ main()
shutil.rmtree(base_dir, ignore_errors=True)
+def test_bootstrap_seeds() -> None:
+ """Installer seeds are useful, quoted, and idempotent."""
+ from retinue.bootstrap import seed_example_memories
+
+ base_dir = Path(tempfile.mkdtemp(prefix="retinue-bootstrap-test-"))
+ try:
+ source_dir = base_dir / "Retinue source checkout"
+ first = seed_example_memories(
+ base_dir, source_dir, hostname="test-host"
+ )
+ assert first == {"added": 2, "existing": 0, "total": 2}, first
+
+ store = MemoryStore(base_dir)
+ rows = store.list()
+ contents = [row[1] for row in rows]
+ assert any(
+ "Retinue" in text
+ and str(base_dir / "retinue_memory.db") in text
+ and "minishlab/potion-multilingual-128M" in text
+ and "sqlite-vec" in text
+ and "prefetch" in text
+ for text in contents
+ ), contents
+ assert any(
+ 'host "test-host"' in text
+ and f'"{source_dir}"' in text
+ and f'"{source_dir}/Install Retinue"' in text
+ for text in contents
+ ), contents
+ first_ids = sorted(row[0] for row in rows)
+ store.close()
+
+ second = seed_example_memories(
+ base_dir, source_dir, hostname="test-host"
+ )
+ assert second == {"added": 0, "existing": 2, "total": 2}, second
+ store = MemoryStore(base_dir)
+ assert sorted(row[0] for row in store.list()) == first_ids
+ store.close()
+ print("\nInstaller bootstrap seed tests passed.")
+ finally:
+ shutil.rmtree(base_dir, ignore_errors=True)
+
+
def test_cli() -> None:
"""Exercise the "Show Retinue memory contents" CLI (src/retinue/cli.py)."""
base_dir = Path(tempfile.mkdtemp(prefix="retinue-cli-test-"))
test_on_memory_write()
test_prefetch_session_filter()
test_rolling_topic_query()
+ test_bootstrap_seeds()
test_cli()
print("\nAll standalone tests passed.")