#!/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 ) # 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)"