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>
136 lines
4.6 KiB
Bash
Executable File
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)"
|