eb1063da28
A pi extension for reading and writing persistent memory across agent systems. Reads its own canonical store (project ./.pi/memory/ + user ~/.pi/memory/, typed markdown with frontmatter — Claude's user/feedback/project/reference taxonomy) AND other agents' memory verbatim (Claude CLAUDE.md + ~/.claude/projects/<slug>/ memory/, Copilot, AGENTS.md, GEMINI.md, Aider CONVENTIONS.md). Writes only ever touch the canonical store; foreign files are never modified. Surfaces: memory_read / memory_search / memory_write / memory_forget tools, /memory-* commands, and a canonical index appended to the system prompt at agent start. Enum-valued args (type/scope/system) are lenient strings normalized in-tool rather than anyOf/const unions: some local models emit const-union values JSON-quoted (e.g. "\"feedback\""), which strict schema validation rejected before the tool ran. normalizeEnumArg strips the stray quoting; memory_read by name also falls back to a foreign file of that name. store.ts / sources.ts import only Node built-ins so tests load them directly under --experimental-strip-types. 31 unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
297 lines
11 KiB
Bash
Executable File
297 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# install-client.sh — Provision (or update) a pi client machine with the
|
|
# parts of pi-extensions you select. Idempotent, non-destructive by default
|
|
# only when --update-only is set.
|
|
#
|
|
# Run from a checked-out copy of the pi-extensions repo:
|
|
#
|
|
# scripts/install-client.sh # everything
|
|
# scripts/install-client.sh --components ai-server,certs
|
|
# scripts/install-client.sh --update-only # add new files only,
|
|
# # never replace existing
|
|
#
|
|
# Components:
|
|
# certs mTLS client cert + root CA from the Caddy host
|
|
# ssh SSH key-auth to the ai-server host (admin commands)
|
|
# ai-server the pi extension (mTLS llama.cpp router provider)
|
|
# dark-mechanicus theme TUI bundle (banner, indicator, status line, etc.)
|
|
# token-stats footer owner for context + token rate display
|
|
# memory cross-agent memory access extension (memory/)
|
|
# themes JSON theme palette files (themes/*.json)
|
|
# local-llama split local llama.cpp extension (llama.cpp/)
|
|
# ai-complete shell CLI for direct llama-server access (installed to
|
|
# --ai-complete-dir, default ~/bin/)
|
|
# shared shared TS utils — auto-included if dark-mechanicus is
|
|
# all shorthand for every component above (default if neither
|
|
# --components nor any --no-<x> flag is given)
|
|
#
|
|
# Modes:
|
|
# --update-only Do not replace existing files. Only add new ones. Useful
|
|
# after a `git pull` to surface new modules without
|
|
# clobbering local edits.
|
|
# --force Overwrite existing cert files (other components: same as
|
|
# no flag — rsync replaces unless --update-only).
|
|
#
|
|
# Options:
|
|
# --components LIST Comma-separated component names. Default: all.
|
|
# --update-only Non-destructive: only add missing files.
|
|
# --ai-complete-dir PATH Where to install ai-complete (default: ~/bin)
|
|
# --caddy-host USER@HOST Default: shahondin1624@192.168.2.2
|
|
# --caddy-cert-dir PATH Default: /mnt/ssdpool/@docker/caddy/certs
|
|
# --ai-server-host USER@HOST Default: ai-server@192.168.2.3
|
|
# --ai-server-url URL Default: https://ai.shahondin1624.de
|
|
# --pi-dir PATH Default: $HOME/.pi/agent
|
|
# --no-certs Same as omitting "certs" from --components
|
|
# --no-ssh-setup Same as omitting "ssh" from --components
|
|
# --force Overwrite existing cert files
|
|
# --skip-verify Skip the final mTLS health probe
|
|
# --help Show this message
|
|
|
|
set -euo pipefail
|
|
|
|
# ─── defaults ────────────────────────────────────────────────────────────
|
|
|
|
CADDY_HOST="${CADDY_HOST:-shahondin1624@192.168.2.2}"
|
|
CADDY_CERT_DIR="${CADDY_CERT_DIR:-/mnt/ssdpool/@docker/caddy/certs}"
|
|
AI_SERVER_HOST="${AI_SERVER_HOST:-ai-server@192.168.2.3}"
|
|
AI_SERVER_URL="${AI_SERVER_URL:-https://ai.shahondin1624.de}"
|
|
PI_DIR="${PI_DIR:-$HOME/.pi/agent}"
|
|
AI_COMPLETE_DIR="${AI_COMPLETE_DIR:-$HOME/bin}"
|
|
COMPONENTS=""
|
|
UPDATE_ONLY=0
|
|
FORCE=0
|
|
SKIP_VERIFY=0
|
|
LEGACY_NO_CERTS=0
|
|
LEGACY_NO_SSH=0
|
|
|
|
ALL_COMPONENTS=(certs ssh ai-server dark-mechanicus token-stats memory themes local-llama ai-complete shared)
|
|
|
|
usage() { sed -n '2,/^$/p' "$0" | sed 's/^#\{0,1\} \{0,1\}//'; exit 0; }
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--components) COMPONENTS="$2"; shift 2 ;;
|
|
--update-only) UPDATE_ONLY=1; shift ;;
|
|
--ai-complete-dir) AI_COMPLETE_DIR="$2"; shift 2 ;;
|
|
--caddy-host) CADDY_HOST="$2"; shift 2 ;;
|
|
--caddy-cert-dir) CADDY_CERT_DIR="$2"; shift 2 ;;
|
|
--ai-server-host) AI_SERVER_HOST="$2"; shift 2 ;;
|
|
--ai-server-url) AI_SERVER_URL="$2"; shift 2 ;;
|
|
--pi-dir) PI_DIR="$2"; shift 2 ;;
|
|
--no-ssh-setup) LEGACY_NO_SSH=1; shift ;;
|
|
--no-certs) LEGACY_NO_CERTS=1; shift ;;
|
|
--force) FORCE=1; shift ;;
|
|
--skip-verify) SKIP_VERIFY=1; shift ;;
|
|
-h|--help) usage ;;
|
|
*) echo "Unknown arg: $1 (try --help)" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
[[ -d ai-server ]] || { echo "Run from a checked-out pi-extensions repo (ai-server/ not found here)" >&2; exit 1; }
|
|
|
|
# ─── component selection ─────────────────────────────────────────────────
|
|
|
|
# If --components not given, default to all, then strip what --no-* removed
|
|
if [[ -z "$COMPONENTS" ]]; then
|
|
COMPONENTS="all"
|
|
fi
|
|
|
|
# Expand "all" -> every known component
|
|
if [[ "$COMPONENTS" == "all" || "$COMPONENTS" == *",all,"* || "$COMPONENTS" == "all,"* || "$COMPONENTS" == *",all" ]]; then
|
|
COMPONENTS=$(IFS=,; echo "${ALL_COMPONENTS[*]}")
|
|
fi
|
|
|
|
# Honor legacy --no-certs / --no-ssh-setup by removing from the list
|
|
if [[ $LEGACY_NO_CERTS -eq 1 ]]; then
|
|
COMPONENTS=$(echo "$COMPONENTS" | tr ',' '\n' | grep -vx 'certs' | paste -sd, -)
|
|
fi
|
|
if [[ $LEGACY_NO_SSH -eq 1 ]]; then
|
|
COMPONENTS=$(echo "$COMPONENTS" | tr ',' '\n' | grep -vx 'ssh' | paste -sd, -)
|
|
fi
|
|
|
|
# Auto-add 'shared' if dark-mechanicus, token-stats, or memory is selected (they import from ../shared/ or ./shared/)
|
|
if [[ ",$COMPONENTS," == *",dark-mechanicus,"* || ",$COMPONENTS," == *",token-stats,"* || ",$COMPONENTS," == *",memory,"* ]]; then
|
|
if [[ ",$COMPONENTS," != *",shared,"* ]]; then
|
|
COMPONENTS="$COMPONENTS,shared"
|
|
fi
|
|
fi
|
|
|
|
# Normalize and dedupe
|
|
COMPONENTS=$(echo "$COMPONENTS" | tr ',' '\n' | sed '/^$/d' | awk '!seen[$0]++' | paste -sd, -)
|
|
|
|
want() { [[ ",$COMPONENTS," == *",$1,"* ]]; }
|
|
|
|
# Validate component names
|
|
for c in $(echo "$COMPONENTS" | tr ',' ' '); do
|
|
found=0
|
|
for known in "${ALL_COMPONENTS[@]}"; do
|
|
[[ "$c" == "$known" ]] && { found=1; break; }
|
|
done
|
|
[[ $found -eq 1 ]] || { echo "Unknown component: '$c' (valid: ${ALL_COMPONENTS[*]}, all)" >&2; exit 1; }
|
|
done
|
|
|
|
# rsync flags differ per mode
|
|
if [[ $UPDATE_ONLY -eq 1 ]]; then
|
|
RSYNC_FLAGS=(-a --ignore-existing)
|
|
MODE_LABEL="update-only (existing files preserved)"
|
|
else
|
|
RSYNC_FLAGS=(-a)
|
|
MODE_LABEL="full sync (existing files replaced)"
|
|
fi
|
|
|
|
echo "==> pi-extensions client install"
|
|
echo " repo root: $REPO_ROOT"
|
|
echo " pi dir: $PI_DIR"
|
|
echo " components: $COMPONENTS"
|
|
echo " mode: $MODE_LABEL"
|
|
[[ $FORCE -eq 1 ]] && echo " cert force: on (will overwrite existing certs)"
|
|
|
|
mkdir -p "$PI_DIR/extensions"
|
|
|
|
# ─── components ──────────────────────────────────────────────────────────
|
|
|
|
if want certs; then
|
|
echo; echo "==> [certs] installing mTLS certs from $CADDY_HOST"
|
|
mkdir -p "$PI_DIR/certs"
|
|
chmod 700 "$PI_DIR/certs"
|
|
for pair in "client.crt:client.pem" "client.key:client-key.pem" "root-ca.pem:root-ca.pem"; do
|
|
src="${pair%%:*}"; dst="${pair##*:}"
|
|
target="$PI_DIR/certs/$dst"
|
|
# Cert preservation rules:
|
|
# - default : keep existing (don't clobber a working identity)
|
|
# - --force : overwrite
|
|
# - --update-only : keep existing (same as default)
|
|
if [[ -f "$target" && $FORCE -eq 0 ]]; then
|
|
echo " keep existing $dst (use --force to replace)"
|
|
continue
|
|
fi
|
|
echo " fetch $CADDY_HOST:$CADDY_CERT_DIR/$src -> $target"
|
|
scp -q "$CADDY_HOST:$CADDY_CERT_DIR/$src" "$target"
|
|
done
|
|
chmod 600 "$PI_DIR"/certs/*.pem 2>/dev/null || true
|
|
fi
|
|
|
|
if want ai-server; then
|
|
echo; echo "==> [ai-server] syncing extension"
|
|
rsync "${RSYNC_FLAGS[@]}" --exclude='.git' ai-server "$PI_DIR/extensions/"
|
|
fi
|
|
|
|
if want dark-mechanicus; then
|
|
if [[ -d dark-mechanicus ]]; then
|
|
echo; echo "==> [dark-mechanicus] syncing theme TUI bundle"
|
|
rsync "${RSYNC_FLAGS[@]}" --exclude='.git' dark-mechanicus "$PI_DIR/extensions/"
|
|
else
|
|
echo " (dark-mechanicus/ not in repo, skipping)"
|
|
fi
|
|
fi
|
|
|
|
if want token-stats; then
|
|
if [[ -d token-stats ]]; then
|
|
echo; echo "==> [token-stats] syncing footer extension"
|
|
rsync "${RSYNC_FLAGS[@]}" --exclude='.git' token-stats "$PI_DIR/extensions/"
|
|
else
|
|
echo " (token-stats/ not in repo, skipping)"
|
|
fi
|
|
fi
|
|
|
|
if want memory; then
|
|
if [[ -d memory ]]; then
|
|
echo; echo "==> [memory] syncing cross-agent memory extension"
|
|
rsync "${RSYNC_FLAGS[@]}" --exclude='.git' memory "$PI_DIR/extensions/"
|
|
else
|
|
echo " (memory/ not in repo, skipping)"
|
|
fi
|
|
fi
|
|
|
|
if want shared; then
|
|
if [[ -d shared ]]; then
|
|
echo; echo "==> [shared] syncing shared TS utils"
|
|
rsync "${RSYNC_FLAGS[@]}" --exclude='.git' shared "$PI_DIR/extensions/"
|
|
else
|
|
echo " (shared/ not in repo, skipping)"
|
|
fi
|
|
fi
|
|
|
|
if want themes; then
|
|
if [[ -d themes ]]; then
|
|
echo; echo "==> [themes] syncing palette files"
|
|
mkdir -p "$PI_DIR/themes"
|
|
rsync "${RSYNC_FLAGS[@]}" themes/ "$PI_DIR/themes/"
|
|
echo " $(ls themes/*.json 2>/dev/null | wc -l) theme file(s) installed"
|
|
fi
|
|
fi
|
|
|
|
if want local-llama; then
|
|
if [[ -d llama.cpp ]]; then
|
|
echo; echo "==> [local-llama] syncing llama.cpp extension"
|
|
rsync "${RSYNC_FLAGS[@]}" --exclude='.git' llama.cpp "$PI_DIR/extensions/"
|
|
else
|
|
echo " (llama.cpp/ not in repo, skipping)"
|
|
fi
|
|
fi
|
|
|
|
if want ai-complete; then
|
|
echo; echo "==> [ai-complete] installing CLI to $AI_COMPLETE_DIR"
|
|
mkdir -p "$AI_COMPLETE_DIR"
|
|
target="$AI_COMPLETE_DIR/ai-complete"
|
|
if [[ -f "$target" && $UPDATE_ONLY -eq 1 ]]; then
|
|
echo " keep existing $target (--update-only)"
|
|
else
|
|
install -m 755 scripts/ai-complete "$target"
|
|
echo " installed $target"
|
|
fi
|
|
# Add to PATH in shell rc if missing (idempotent)
|
|
case ":$PATH:" in
|
|
*":$AI_COMPLETE_DIR:"*) ;;
|
|
*)
|
|
rc=""
|
|
[[ -f "$HOME/.bashrc" ]] && rc="$HOME/.bashrc"
|
|
[[ -f "$HOME/.zshrc" ]] && rc="$HOME/.zshrc"
|
|
if [[ -n "$rc" ]] && ! grep -qF "$AI_COMPLETE_DIR" "$rc" 2>/dev/null; then
|
|
echo " appending PATH += $AI_COMPLETE_DIR to $rc"
|
|
echo "export PATH=\"$AI_COMPLETE_DIR:\$PATH\"" >> "$rc"
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if want ssh; then
|
|
echo; echo "==> [ssh] checking SSH key-auth to $AI_SERVER_HOST"
|
|
if ssh -o BatchMode=yes -o ConnectTimeout=5 "$AI_SERVER_HOST" 'true' 2>/dev/null; then
|
|
echo " key-auth already works"
|
|
else
|
|
if [[ ! -f "$HOME/.ssh/id_ed25519.pub" && ! -f "$HOME/.ssh/id_rsa.pub" ]]; then
|
|
echo " no local ssh key; generating ed25519"
|
|
ssh-keygen -t ed25519 -N '' -f "$HOME/.ssh/id_ed25519"
|
|
fi
|
|
echo " running ssh-copy-id (will prompt for password once)"
|
|
ssh-copy-id "$AI_SERVER_HOST"
|
|
fi
|
|
fi
|
|
|
|
# ─── verification ────────────────────────────────────────────────────────
|
|
|
|
if [[ $SKIP_VERIFY -eq 0 ]] && want certs; then
|
|
echo; echo "==> Verifying mTLS reachability"
|
|
# Server cert is LE-issued (publicly trusted) — rely on the system CA
|
|
# bundle, matching ai-server/{stream,admin}.ts which omit `ca:`. The local
|
|
# root-ca.pem is kept around only to satisfy loadCerts() in config.ts.
|
|
if curl -sS --max-time 5 \
|
|
--cert "$PI_DIR/certs/client.pem" \
|
|
--key "$PI_DIR/certs/client-key.pem" \
|
|
"$AI_SERVER_URL/health" | grep -q '"ok"'; then
|
|
echo " ✓ mTLS handshake + /health OK"
|
|
else
|
|
echo " ✗ could not verify — check cert files and server reachability"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
echo "==> Done."
|
|
echo " Next: open pi and /reload, or run 'pi' fresh."
|
|
want ai-complete && echo " ai-complete installed; open a new shell (or 'hash -r') to pick up PATH."
|