cognitive-memory/scripts/maintain-all-graphs.sh
Cal Corum d8dd1f35a5 feat: complete multi-graph support across CLI, scripts, and systemd timers
Non-default graphs were second-class citizens — timers only maintained the
default graph, git sync ignored named graphs, there was no way to create
a graph without editing config manually, cross-graph edge errors were
confusing, and utility scripts were hardcoded to the default graph.

- Add `graph-create` CLI command + `create_graph()` in common.py, with
  custom path registration written to the default graph's _config.json
- Add `scripts/maintain-all-graphs.sh` to loop decay/core/embed/reflect
  over all discovered graphs; update systemd services to call it
- Refactor `memory-git-sync.sh` into sync_repo() function that iterates
  default + all named graphs with .git directories
- Improve cross-graph edge ValueError to explain the same-graph constraint
- Add --graph flag to edge-proposer.py and session_memory.py
- Update systemd/README.md with portable paths and new architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:35:55 -06:00

136 lines
4.6 KiB
Bash
Executable File

#!/bin/bash
# Run cognitive memory maintenance (decay, core, embed, reflect) for ALL graphs.
#
# Called by systemd service units instead of individual claude-memory commands,
# so that named graphs receive the same maintenance as the default graph.
#
# Usage:
# maintain-all-graphs.sh [--daily | --embed | --weekly]
#
# --daily (default) Run decay + core for every graph
# --embed Run embed --if-changed for every graph
# --weekly Run reflect for every graph
#
# Named graph directories that do not exist on disk are silently skipped.
# The default graph is always processed first regardless of disk discovery.
set -euo pipefail
# ── configuration ──────────────────────────────────────────────────────────────
CLAUDE_MEMORY="${CLAUDE_MEMORY_BIN:-/home/cal/.local/bin/claude-memory}"
export PATH="/home/cal/.local/bin:$PATH"
MODE="${1:---daily}"
# ── helpers ────────────────────────────────────────────────────────────────────
log() {
echo "maintain-all-graphs [$(date '+%H:%M:%S')]: $*"
}
# Run a claude-memory command for one graph.
# $1 = graph name ("default" omits --graph flag, others pass --graph <name>)
# remaining args = claude-memory subcommand + flags
run_for_graph() {
local graph_name="$1"
shift
local graph_flag=()
if [ "$graph_name" != "default" ]; then
graph_flag=(--graph "$graph_name")
fi
log "[$graph_name] $*"
"$CLAUDE_MEMORY" "${graph_flag[@]}" "$@"
}
# ── discover graphs ────────────────────────────────────────────────────────────
#
# claude-memory graphs outputs a JSON array:
# [{"name": "default", "path": "/home/cal/.local/share/cognitive-memory"}, ...]
#
# We parse it with only sh/awk — no jq dependency.
# Strategy: extract "name" and "path" values line by line from pretty-printed JSON.
graphs_json=$("$CLAUDE_MEMORY" graphs 2>/dev/null) || {
log "ERROR: 'claude-memory graphs' failed; aborting"
exit 1
}
# Build parallel arrays: GRAPH_NAMES and GRAPH_PATHS
GRAPH_NAMES=()
GRAPH_PATHS=()
# State machine: after seeing "name": capture value; after "path": capture value.
# Each object ends when we collect both; append to arrays.
_name=""
_path=""
while IFS= read -r line; do
# Strip leading/trailing whitespace
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
# Match: "name": "value"
if [[ "$line" =~ ^\"name\":\ *\"([^\"]+)\" ]]; then
_name="${BASH_REMATCH[1]}"
fi
# Match: "path": "value" (path may contain slashes — [^"]+ is fine)
if [[ "$line" =~ ^\"path\":\ *\"([^\"]+)\" ]]; then
_path="${BASH_REMATCH[1]}"
fi
# When we have both, flush to arrays (object boundary)
if [ -n "$_name" ] && [ -n "$_path" ]; then
GRAPH_NAMES+=("$_name")
GRAPH_PATHS+=("$_path")
_name=""
_path=""
fi
done <<< "$graphs_json"
if [ "${#GRAPH_NAMES[@]}" -eq 0 ]; then
log "ERROR: no graphs discovered; aborting"
exit 1
fi
log "discovered ${#GRAPH_NAMES[@]} graph(s): ${GRAPH_NAMES[*]}"
# ── main loop ─────────────────────────────────────────────────────────────────
ERRORS=0
for i in "${!GRAPH_NAMES[@]}"; do
name="${GRAPH_NAMES[$i]}"
path="${GRAPH_PATHS[$i]}"
# Skip graphs whose directory doesn't exist on disk (named graphs may not
# be initialised yet on this machine).
if [ "$name" != "default" ] && [ ! -d "$path" ]; then
log "[$name] skipping — directory not found: $path"
continue
fi
case "$MODE" in
--daily)
run_for_graph "$name" decay || { log "[$name] decay FAILED"; ERRORS=$((ERRORS+1)); }
run_for_graph "$name" core || { log "[$name] core FAILED"; ERRORS=$((ERRORS+1)); }
;;
--embed)
run_for_graph "$name" embed --if-changed || { log "[$name] embed FAILED"; ERRORS=$((ERRORS+1)); }
;;
--weekly)
run_for_graph "$name" reflect || { log "[$name] reflect FAILED"; ERRORS=$((ERRORS+1)); }
;;
*)
echo "Usage: $0 [--daily | --embed | --weekly]" >&2
exit 1
;;
esac
done
if [ "$ERRORS" -gt 0 ]; then
log "completed with $ERRORS error(s)"
exit 1
fi
log "done (mode: $MODE)"