Files
mem0/mem0-plugin/scripts/on_session_start.sh
T
Mgeeeek db1b733c24 feat(plugin): holistic import of on-disk Claude state into mem0
When a user installs mem0-plugin, mem0 starts empty even though Claude
Code has been quietly accumulating their CLAUDE.md instructions,
auto-memory, and subagent learnings on disk for months. The existing
hooks capture live state going forward but cannot backfill what already
exists.

This commit adds a one-shot importer that walks every Claude state
surface and backfills it into mem0:

- `mem0-plugin/scripts/import_claude_state.py` (new, ~575 lines):
  discovers CLAUDE.md hierarchy (+ @imports), .claude/rules,
  ~/.claude/projects/*/memory, and ~/.claude/agent-memory; chunks each
  file at markdown headings (H2 default, H3 for oversize sections,
  small-sibling merge, code-fence safe); tags chunks with metadata
  matching the existing mem0-mcp skill vocabulary (convention /
  user_preference / task_learning / anti_pattern / decision); POSTs
  sequentially to /v1/memories/ with infer=True (server-side dedup).
  Marker file at ~/.mem0/imports/claude-state.json tracks content-hash
  per chunk for idempotent re-runs and crash-resume safety.

  Flags: --dry-run, --reset, --no-infer, --source <type>.

- `mem0-plugin/scripts/on_session_start.sh`: adds a one-time nudge
  block after the existing bootstrap output. Emits the holistic-import
  prompt only when ~/.mem0/imports/claude-state.json doesn't exist
  and the SOURCE isn't "compact". Propagates to Cursor and Codex for
  free (shared bash entry point).

- README, CHANGELOG, plugin.json bumped to 0.2.0.

Patterned exactly on the existing on_pre_compact.py: stdlib only
(urllib, hashlib, pathlib, argparse), reuses _identity.resolve_user_id,
same logging setup, same ~/.mem0/ storage convention. Zero new packages,
zero new deps.

Smoke-tested locally: --help renders, missing MEM0_API_KEY exits 1
with helpful message, hook emits nudge when marker absent and stays
silent when marker present.

Research and design context: https://www.notion.so/Claude-State-Holistic-Import-Research-Findings-366f22c70c9081d7abdcc6b94a80e8d9
2026-05-20 19:53:28 +05:30

107 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Hook: SessionStart (matcher: startup|resume|compact)
#
# Bootstraps mem0 context at the start of every session.
# Output becomes part of Claude's context so it calls mem0 MCP tools.
#
# Input: JSON on stdin with session_id, source, transcript_path, model, cwd
# Output: Text injected into Claude's context (exit 0)
# Intentionally omit -e so the script always outputs a bootstrap prompt
# even if jq is missing or stdin is malformed.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
# Skip the bootstrap entirely if no API key is configured -- the agent
# would otherwise be told to call mem0 MCP tools that will all fail.
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
INPUT=$(cat)
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
# Identity line is emitted before every bootstrap variant so the agent
# uses the same user_id the hooks resolved. Without this, the agent's
# search_memories/add_memory MCP calls may bind to a different bucket
# than what the hooks write to.
echo "## Mem0 Identity"
echo ""
echo "Active user_id: \`$MEM0_RESOLVED_USER_ID\`"
echo ""
echo "Always include \`{\"user_id\": \"$MEM0_RESOLVED_USER_ID\"}\` (wrapped in an \`AND\` clause) in every \`search_memories\` filter and as \`user_id\` on every \`add_memory\` call. This keeps the agent's MCP calls aligned with the bucket the hooks write to."
echo ""
if [ "$SOURCE" = "startup" ]; then
cat <<'EOF'
## Mem0 Session Bootstrap
You have access to persistent memory via the mem0 MCP tools. Before doing anything else:
1. Call `search_memories` with a query related to the current project or user request to load relevant context.
2. Review the returned memories to understand what has been learned in prior sessions.
3. If appropriate, call `get_memories` to browse all stored memories for this user.
IMPORTANT: Do NOT skip this step. Always bootstrap context first.
EOF
elif [ "$SOURCE" = "resume" ]; then
cat <<'EOF'
## Mem0 Session Resumed
This is a resumed session. Your prior context is already loaded. Before continuing:
1. Call `search_memories` with a query related to the current task to refresh relevant memories.
2. If significant time has passed, search for recent project-wide updates.
Continue where you left off.
EOF
elif [ "$SOURCE" = "compact" ]; then
# Capture the just-generated compact summary in the background.
# PreCompact fires too early to see this entry; SessionStart-compact
# is the first place isCompactSummary=true is in the transcript.
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null &
cat <<'EOF'
## Mem0 Post-Compaction Recovery
Context was just compacted. The Claude Code-generated compact summary
is being captured to mem0 in the background as `metadata.type=compact_summary`.
1. Call `search_memories` to reload context, layering up to three angles:
- `metadata.type=session_state` -- the rich pre-compaction summary you wrote
- `metadata.type=compact_summary` -- the platform-generated condensed summary just now
- `metadata.type=decision` / `anti_pattern` -- specific facts you stored during the session
2. Continue working from the recovered context.
EOF
fi
# ── Holistic import nudge ────────────────────────────────────────────────
# One-time nudge to backfill existing CLAUDE.md / MEMORY.md / agent-memory
# into mem0. Silent after the marker file appears (i.e., after first run).
# Only emit on startup or resume -- during compact the user is mid-conversation.
if [ "$SOURCE" != "compact" ] && [ ! -f "$HOME/.mem0/imports/claude-state.json" ]; then
cat <<EOF
## Holistic import available
On-disk Claude state (CLAUDE.md, ~/.claude/projects/*/memory, ~/.claude/agent-memory)
has never been imported into mem0. To preview what would be imported, run:
python3 "$SCRIPT_DIR/import_claude_state.py" --dry-run
Then drop \`--dry-run\` to import. This nudge disappears after the first
successful run. Pass \`--reset\` to re-import from scratch.
EOF
fi
exit 0