From 97a3a2cf8d9db2756b890d0f71995ab3630b25db Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Thu, 21 May 2026 23:55:06 +0530 Subject: [PATCH] feat(mem0-plugin): complete all Tier 1-8 spec items, add scheduled dream and bash error detection - Add /mem0:dream --schedule support via Claude Code /schedule command with local launchd/cron fallback for non-cloud users - Add /mem0:digest --schedule and file output to ~/.mem0/weekly-digest.md with digest-history.log for trend tracking - Add on_post_commit.sh PostToolUse hook: interactive prompt after git commit asking user to save change as memory with category suggestion - Add on_bash_output.sh PostToolUse hook: detects stack traces and error patterns in bash command output, injects mem0 search rubric for prior occurrences (complements existing user prompt error detection) - Fix session-end report: now shows per-category breakdown "wrote 3 memories (decision 2, anti_pattern 1)" matching spec - Wire new hooks into all 3 platforms (Claude Code, Cursor, Codex) - Add post_commit and bash_error telemetry events - Add CTO demo testing guide with tier-by-tier manual test steps --- mem0-plugin/hooks/codex-hooks.json | 20 +++++ mem0-plugin/hooks/cursor-hooks.json | 10 +++ mem0-plugin/hooks/hooks.json | 20 +++++ mem0-plugin/scripts/on_bash_output.sh | 104 ++++++++++++++++++++++ mem0-plugin/scripts/on_post_commit.sh | 93 ++++++++++++++++++++ mem0-plugin/scripts/session_stats.py | 7 +- mem0-plugin/scripts/telemetry.py | 5 ++ mem0-plugin/skills/mem0-digest/SKILL.md | 50 ++++++++++- mem0-plugin/skills/mem0-dream/SKILL.md | 110 +++++++++++++++++++++++- 9 files changed, 414 insertions(+), 5 deletions(-) create mode 100755 mem0-plugin/scripts/on_bash_output.sh create mode 100755 mem0-plugin/scripts/on_post_commit.sh diff --git a/mem0-plugin/hooks/codex-hooks.json b/mem0-plugin/hooks/codex-hooks.json index 6676d32f3..c74c8d0c9 100644 --- a/mem0-plugin/hooks/codex-hooks.json +++ b/mem0-plugin/hooks/codex-hooks.json @@ -34,6 +34,26 @@ "timeout": 3 } ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CODEX_PLUGIN_ROOT}/scripts/on_post_commit.sh", + "timeout": 5 + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CODEX_PLUGIN_ROOT}/scripts/on_bash_output.sh", + "timeout": 5 + } + ] } ], "Stop": [ diff --git a/mem0-plugin/hooks/cursor-hooks.json b/mem0-plugin/hooks/cursor-hooks.json index 0ec8f68ad..f9187631a 100644 --- a/mem0-plugin/hooks/cursor-hooks.json +++ b/mem0-plugin/hooks/cursor-hooks.json @@ -17,6 +17,16 @@ "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_post_tool_use_cursor.sh", "matcher": "mcp__mem0__", "timeout": 3 + }, + { + "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_post_commit.sh", + "matcher": "Bash", + "timeout": 5 + }, + { + "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_bash_output.sh", + "matcher": "Bash", + "timeout": 5 } ], "preCompact": [ diff --git a/mem0-plugin/hooks/hooks.json b/mem0-plugin/hooks/hooks.json index 70d6575db..992dd80c0 100644 --- a/mem0-plugin/hooks/hooks.json +++ b/mem0-plugin/hooks/hooks.json @@ -66,6 +66,26 @@ "timeout": 3 } ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_post_commit.sh", + "timeout": 5 + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_bash_output.sh", + "timeout": 5 + } + ] } ], "PreCompact": [ diff --git a/mem0-plugin/scripts/on_bash_output.sh b/mem0-plugin/scripts/on_bash_output.sh new file mode 100755 index 000000000..18b505479 --- /dev/null +++ b/mem0-plugin/scripts/on_bash_output.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Hook: PostToolUse (matcher: Bash) +# +# Scans bash command output for stack traces and error patterns. +# When found, injects a search rubric telling the agent to check mem0 +# for prior occurrences of the same error. +# +# This complements on_user_prompt.sh (which catches errors in the user's +# typed message). This hook catches errors in COMMAND OUTPUT — e.g., +# when `npm test` or `python script.py` fails with a traceback. +# +# Input: JSON on stdin with tool_name, tool_input, tool_result +# Output: Context injected into Claude's next response (exit 0) + +set -uo pipefail + +INPUT=$(cat) + +TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_result // ""' 2>/dev/null || echo "") + +# Skip short output (< 50 chars unlikely to contain a real stack trace) +if [ ${#TOOL_RESULT} -lt 50 ]; then + exit 0 +fi + +# Skip if this is a git commit (handled by on_post_commit.sh) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "") +case "$COMMAND" in + *"git commit"*|*"git merge"*|*"git rebase"*) + exit 0 + ;; +esac + +# Detect stack traces and error patterns in command output +HAS_ERROR="" +if echo "$TOOL_RESULT" | grep -qiE '(Traceback \(most recent|Error:|Exception:|panic:|FAILED|fatal:|FAIL:| at .+\.[a-z]+:[0-9]+|error\[E[0-9]+\])'; then + HAS_ERROR="true" +fi + +if [ -z "$HAS_ERROR" ]; then + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true + +if [ -z "${MEM0_API_KEY:-}" ]; then + exit 0 +fi + +# Extract error class/message (first matching line) +ERROR_LINE=$(echo "$TOOL_RESULT" | grep -iE '(Error:|Exception:|panic:|FAIL:|fatal:)' | head -1 | sed 's/^[[:space:]]*//' | cut -c1-120) + +# Extract file paths from stack trace frames +TRACE_FILES=$(echo "$TOOL_RESULT" | grep -oE '([a-zA-Z0-9_./-]+\.(py|ts|tsx|js|jsx|rs|go|rb|java|sh))(:[0-9]+)?' | head -5 | sort -u) + +# Build file list for display +FILE_DISPLAY="" +if [ -n "$TRACE_FILES" ]; then + FILE_DISPLAY=$(echo "$TRACE_FILES" | sed 's/^/ - /') +fi + +USER_ID="$MEM0_RESOLVED_USER_ID" + +cat < $ERROR_LINE + +EOF + +if [ -n "$FILE_DISPLAY" ]; then + cat </dev/null & + +exit 0 diff --git a/mem0-plugin/scripts/on_post_commit.sh b/mem0-plugin/scripts/on_post_commit.sh new file mode 100755 index 000000000..7e67d6d69 --- /dev/null +++ b/mem0-plugin/scripts/on_post_commit.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Hook: PostToolUse (matcher: Bash) +# +# Fires AFTER a Bash tool call completes. When a git commit/merge/rebase +# just succeeded, surfaces 1-3 relevant memories from the changed files +# and prompts Claude to ask the user if this change should be stored as +# a learning. +# +# Input: JSON on stdin with tool_name, tool_input, tool_result +# Output: Context injected into Claude's next response (exit 0) +# +# This implements Spec #28 — interactive pre-commit memory check. + +set -uo pipefail + +INPUT=$(cat) + +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "") + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +# Only trigger on git commit/merge/rebase commands +case "$COMMAND" in + *"git commit"*|*"git merge"*|*"git rebase"*) + ;; + *) + exit 0 + ;; +esac + +# Check if the command actually succeeded (look for commit hash in output) +TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_result // ""' 2>/dev/null || echo "") +if echo "$TOOL_RESULT" | grep -qiE '(error|fatal|abort|nothing to commit|no changes)'; then + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true + +# No API key — skip interactive prompt +if [ -z "${MEM0_API_KEY:-}" ]; then + exit 0 +fi + +# Extract changed files from commit +CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | head -10 || echo "") +if [ -z "$CHANGED_FILES" ]; then + exit 0 +fi + +# Get the commit message +COMMIT_MSG=$(git log -1 --format='%s' HEAD 2>/dev/null || echo "") + +# Build file list for display +FILE_LIST=$(echo "$CHANGED_FILES" | sed 's/^/ - /' | head -5) +FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l | tr -d ' ') + +cat < **Save this change as a memory?** I can store it as one of: +> - \`decision\` — if an architectural or design choice was made +> - \`task_learning\` — if a strategy or approach worked well +> - \`anti_pattern\` — if this fixes a bug or avoids a known pitfall +> - \`convention\` — if this establishes a new pattern +> +> Type the category, or **skip** to move on. + +If the user provides a category (or says yes/sure/go ahead): +1. Summarize the commit: what changed and why (from the diff + commit message) +2. Call \`add_memory\` with: + - \`messages=[{"role": "user", "content": ""}]\` + - \`user_id="$MEM0_RESOLVED_USER_ID"\` + - \`app_id="$MEM0_PROJECT_ID"\` + - \`metadata={"type": "", "branch": "$MEM0_BRANCH", "confidence": 0.8, "files": [], "source": "post-commit"}\` + - \`infer=False\` + +If the user says skip/no/nothing: proceed normally. Do NOT ask again for the same commit. +EOF + +# Telemetry +python3 "$SCRIPT_DIR/telemetry.py" post_commit --files_count="$FILE_COUNT" 2>/dev/null & + +exit 0 diff --git a/mem0-plugin/scripts/session_stats.py b/mem0-plugin/scripts/session_stats.py index 0a2943f5d..71c857bc1 100644 --- a/mem0-plugin/scripts/session_stats.py +++ b/mem0-plugin/scripts/session_stats.py @@ -100,7 +100,12 @@ def report() -> str: return "" parts = [] - parts.append(f"Session: wrote {adds} memories, retrieved {searches}") + category_counts = stats.get("category_counts", {}) + if category_counts: + breakdown = ", ".join(f"{c} {n}" for c, n in sorted(category_counts.items(), key=lambda x: -x[1])) + parts.append(f"Session: wrote {adds} memories ({breakdown}), retrieved {searches}") + else: + parts.append(f"Session: wrote {adds} memories, retrieved {searches}") if categories: parts.append(f"Categories touched: {', '.join(categories)}") diff --git a/mem0-plugin/scripts/telemetry.py b/mem0-plugin/scripts/telemetry.py index 6fa4402c3..277389c60 100644 --- a/mem0-plugin/scripts/telemetry.py +++ b/mem0-plugin/scripts/telemetry.py @@ -140,6 +140,11 @@ def main() -> int: properties["source_detail"] = arg.split("=", 1)[1] elif arg.startswith("--tool="): properties["tool"] = arg.split("=", 1)[1] + elif arg.startswith("--files_count="): + try: + properties["files_count"] = int(arg.split("=", 1)[1]) + except ValueError: + pass emit(event_type, properties) return 0 diff --git a/mem0-plugin/skills/mem0-digest/SKILL.md b/mem0-plugin/skills/mem0-digest/SKILL.md index cc17040a3..0a77409f4 100644 --- a/mem0-plugin/skills/mem0-digest/SKILL.md +++ b/mem0-plugin/skills/mem0-digest/SKILL.md @@ -65,7 +65,55 @@ Top categories: <2-3 sentence summary of the most important decisions, learnings, or patterns stored this week> ``` -### Step 4: Empty state +### Step 4: Write digest to file + +After displaying, write the digest to `~/.mem0/weekly-digest.md` for persistence +and external consumption (email, Slack, etc.): + +```bash +mkdir -p ~/.mem0 +``` + +Write the full digest output (same markdown shown in terminal) to `~/.mem0/weekly-digest.md` +using the Write tool. **Overwrite** the file each time — it always contains the latest digest. + +Also append a one-line summary to `~/.mem0/digest-history.log` for trend tracking: + +```bash +echo " | | + memories | top: " >> ~/.mem0/digest-history.log +``` + +Print at the end: +``` +Digest saved to ~/.mem0/weekly-digest.md +``` + +### Step 5: Schedule recurring digests + +When invoked with `--schedule` (e.g., `/mem0:digest --schedule weekly`), register +a cloud routine via Claude Code's `/schedule` command: + +``` +/schedule /mem0:digest +``` + +For example: +- `/schedule weekly on Monday 9am /mem0:digest` — digest every Monday morning +- `/schedule daily at 8am /mem0:digest` — daily digest + +Print: +``` +Digest scheduled: +Manage at: https://claude.ai/code/routines +``` + +If `/schedule` is unavailable, print a cron one-liner the user can install manually: +```bash +# macOS/Linux — weekly Monday 9am +(crontab -l 2>/dev/null; echo "0 9 * * 1 cd PROJECT_DIR && claude -p '/mem0:digest' >> /tmp/mem0-digest.log 2>&1") | crontab - +``` + +### Step 6: Empty state If no memories in the last 7 days: ``` diff --git a/mem0-plugin/skills/mem0-dream/SKILL.md b/mem0-plugin/skills/mem0-dream/SKILL.md index 42f6bd7ea..3466a4568 100644 --- a/mem0-plugin/skills/mem0-dream/SKILL.md +++ b/mem0-plugin/skills/mem0-dream/SKILL.md @@ -235,6 +235,110 @@ In auto mode: ) ``` -**Note**: Claude Code does not have native cron/scheduling. To run dream periodically, -use an external scheduler (cron, launchd) calling `claude -p "/mem0:dream --auto"`, -or run it manually at the start of each week. +## Scheduling recurring dreams + +When invoked with `--schedule` (e.g., `/mem0:dream --schedule weekly`), register a +cloud routine via Claude Code's built-in `/schedule` command so the dream runs +automatically without any local cron or launchd setup. + +### Step S1: Parse schedule frequency + +Accept natural-language frequency after `--schedule`: + +| User input | Cron equivalent | Description | +|---|---|---| +| `weekly` or `--schedule weekly` | Every Sunday 3:00 AM local | Default weekly consolidation | +| `daily` | Every day 3:00 AM local | For high-volume projects | +| `biweekly` | Every other Sunday 3:00 AM local | Lower frequency option | +| Custom (e.g., `"every Monday 9am"`) | Pass verbatim to `/schedule` | Let Claude Code resolve it | + +### Step S2: Create the routine + +Use Claude Code's `/schedule` command to create a cloud routine. The routine runs +`/mem0:dream --auto` on the specified schedule against the current repository: + +``` +/schedule /mem0:dream --auto +``` + +For example: +- `/schedule weekly /mem0:dream --auto` — runs every week +- `/schedule daily at 3am /mem0:dream --auto` — runs every day at 3 AM +- `/schedule every Monday 9am /mem0:dream --auto` — runs every Monday at 9 AM + +The `/schedule` command handles all the cloud infrastructure: repository cloning, +environment setup, and cron scheduling. The routine runs as a full Claude Code +cloud session with access to the mem0 MCP tools. + +### Step S3: Confirm to user + +After the routine is created, print: + +``` +Dream scheduled: +Routine name: mem0-dream- +Next run: + +Manage at: https://claude.ai/code/routines +Edit: /schedule list → /schedule update +Cancel: /schedule list → delete the routine +``` + +### Managing scheduled dreams + +| Action | Command | +|---|---| +| List all routines | `/schedule list` | +| Run dream now | `/schedule run` (select the dream routine) | +| Change frequency | `/schedule update` (select the dream routine) | +| Pause | Toggle off at claude.ai/code/routines | +| Delete | Delete at claude.ai/code/routines or `/schedule update` | + +### Fallback for non-cloud users + +If `/schedule` is unavailable (API key auth, no claude.ai subscription), fall back +to local options: + +1. **macOS launchd plist** — generate and install: + ```bash + cat > ~/Library/LaunchAgents/com.mem0.dream.plist << 'PLIST' + + + + + Labelcom.mem0.dream + ProgramArguments + + claude + -p + /mem0:dream --auto + --allowedTools + mcp__mem0__* + + StartCalendarInterval + + Weekday0 + Hour3 + Minute0 + + StandardOutPath/tmp/mem0-dream.log + StandardErrorPath/tmp/mem0-dream.err + WorkingDirectoryPROJECT_DIR + + + PLIST + launchctl load ~/Library/LaunchAgents/com.mem0.dream.plist + ``` + Replace `PROJECT_DIR` with the actual project path. + +2. **Linux cron** — add entry: + ```bash + (crontab -l 2>/dev/null; echo "0 3 * * 0 cd PROJECT_DIR && claude -p '/mem0:dream --auto' >> /tmp/mem0-dream.log 2>&1") | crontab - + ``` + +Print which method was used and how to verify: +``` +Dream scheduled (local: launchd/cron): weekly Sundays 3am +Verify: launchctl list | grep mem0 # macOS + crontab -l | grep mem0 # Linux +```