#!/usr/bin/env bash
# autodream — periodic memory consolidation for Claude Code sessions.
# Run from cron every few minutes. When session logs have been idle for
# IDLE_MINS and at least MIN_LINES new lines exist since the last pass,
# asks `claude -p` to lift durable facts into a memory vault, then pushes.
# Manual run: autodream.sh --force  (skips the idle and volume gates)

set -e

# ---- config -----------------------------------------------------------------
VAULT="${MEMORY_VAULT_DIR:-$HOME/memory-vault}"          # folder (ideally a git repo) containing memory/
TRANSCRIPTS="${TRANSCRIPT_DIR:-$HOME/.claude/projects}"  # where session .jsonl logs live
IDLE_MINS="${IDLE_MINS:-15}"
MIN_LINES="${MIN_LINES:-50}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
CLAUDE_ARGS="${CLAUDE_ARGS:-}"                           # extra flags, e.g. a permission mode for headless runs
# ------------------------------------------------------------------------------

[ -d "$VAULT" ] || { echo "autodream: vault not found: $VAULT" >&2; exit 1; }

STATE="$VAULT/.dream-state"
LOG="$VAULT/.dream.log"
LOCK="$VAULT/.dream.lock"
FORCE=0
[ "${1:-}" = "--force" ] && FORCE=1

log() { printf '%s [autodream] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$LOG"; }
mtime() { stat -f %m "$@" 2>/dev/null || stat -c %Y "$@"; }  # BSD stat, then GNU

# single-instance lock
if ! ( set -o noclobber; echo "$$" > "$LOCK" ) 2>/dev/null; then
  log "another instance running (pid $(cat "$LOCK" 2>/dev/null)), bailing"
  exit 0
fi
trap 'rm -f "$LOCK"' EXIT

SESSIONS=()
while IFS= read -r f; do SESSIONS+=("$f"); done \
  < <(find "$TRANSCRIPTS" -type f -name '*.jsonl' 2>/dev/null)
if [ "${#SESSIONS[@]}" -eq 0 ]; then
  log "no session transcripts found under $TRANSCRIPTS"
  exit 0
fi

# idle gate: most recent mtime across all session files
NOW=$(date +%s)
MOST_RECENT=$(mtime "${SESSIONS[@]}" | sort -n | tail -1)
IDLE_FOR=$(( NOW - MOST_RECENT ))
if [ "$FORCE" -eq 0 ] && [ "$IDLE_FOR" -lt $(( IDLE_MINS * 60 )) ]; then
  log "still active (idle ${IDLE_FOR}s), skipping"
  exit 0
fi

# state: last consolidation timestamp + last total line count
LAST_TS=0
LAST_LINES=0
# shellcheck disable=SC1090
[ -f "$STATE" ] && . "$STATE"

# volume gate: enough new transcript lines since last pass
CURRENT_LINES=$(wc -l "${SESSIONS[@]}" 2>/dev/null | tail -1 | awk '{print $1}')
NEW_LINES=$(( CURRENT_LINES - LAST_LINES ))
if [ "$FORCE" -eq 0 ] && [ "$NEW_LINES" -lt "$MIN_LINES" ]; then
  log "only $NEW_LINES new lines (need $MIN_LINES), skipping"
  exit 0
fi

# session files modified since the last pass
RECENT_SESSIONS=()
for s in "${SESSIONS[@]}"; do
  [ "$(mtime "$s")" -gt "$LAST_TS" ] && RECENT_SESSIONS+=("$s")
done
if [ "${#RECENT_SESSIONS[@]}" -eq 0 ]; then
  log "no sessions newer than last consolidation, skipping"
  exit 0
fi

log "consolidating ${#RECENT_SESSIONS[@]} sessions, $NEW_LINES new lines (idle ${IDLE_FOR}s)"

PROMPT=$(cat <<EOF
You are running as autodream, a periodic memory consolidation pass.

The user just stopped working. Scan the recent session transcripts listed below, identify anything memory-worthy that is NOT already captured in the memory vault, and update the vault accordingly.

Vault location: $VAULT/memory/
Memory format: read $VAULT/memory/MEMORY.md to see the index. Each memory is its own .md file with YAML frontmatter (name, description, type in {user, feedback, project, reference}). After adding or updating any memory file, update MEMORY.md with a one-line entry.

Recent session transcripts to scan:
$(printf '  - %s\n' "${RECENT_SESSIONS[@]}")

Rules:
- Be conservative. Only save things that will matter in a future conversation.
- DO NOT save: code patterns, file paths, debugging recipes, ephemeral task state.
- DO save: stable facts about the user, durable preferences and feedback, project context that is not in code, references to external systems.
- If a memory already exists on the topic, UPDATE it rather than duplicating.
- When you add or substantially update a memory, add [[memory/<name>]] wiki-links between related memories, but only where a real semantic relationship exists (same person, same system, same project, same external reference).
- If nothing is worth saving, print "autodream: nothing to consolidate" and stop without editing any files.

After you finish, just stop. The wrapper script will commit and push.
EOF
)

# shellcheck disable=SC2086  # CLAUDE_ARGS is intentionally word-split
if ! echo "$PROMPT" | "$CLAUDE_BIN" -p $CLAUDE_ARGS >> "$LOG" 2>&1; then
  log "claude invocation failed"
  exit 1
fi

{
  echo "LAST_TS=$NOW"
  echo "LAST_LINES=$CURRENT_LINES"
} > "$STATE"

if [ -d "$VAULT/.git" ]; then
  git -C "$VAULT" add -A >> "$LOG" 2>&1 || true
  git -C "$VAULT" commit -m "autodream consolidation" >> "$LOG" 2>&1 || true  # nothing-to-commit is fine
  git -C "$VAULT" push >> "$LOG" 2>&1 || log "git push failed (will retry next pass)"
fi
log "done"

# src: ezra, contact: ezkru69@mail.com
