Add Claude Code skills × Memanto integration (closes #508)#531
Add Claude Code skills × Memanto integration (closes #508)#531Lukhaas25 wants to merge 1 commit into
Conversation
Adds examples/claudecode-skills-memanto/ — a complete integration where Memanto acts as global, active memory across Claude Code sessions, solving the context fragmentation problem described in moorcheh-ai#508. Approach: hooks into Claude Code's lifecycle (UserPromptSubmit, Stop, PostToolUse) rather than instrumenting individual skills. Works with mattpocock/skills, wshobson/agents, or any custom skills out of the box. Contents: hooks/ _memanto_common.py — shared utilities, agent_id derivation, logging inject_context.py — UserPromptSubmit: recall + inject relevant memories distill_session.py — Stop: regex-based signal extraction → typed remember skill_decisions.py — PostToolUse: tag memories with originating skill install.sh / install.ps1 — idempotent settings.json merger uninstall.sh / uninstall.ps1 demo/ — verify_setup.py + reproducible cross-session demo README.md — architecture, design rationale, multi-project setup Design highlights: - Project-scoped agent_id by default (hash of cwd); overridable via env or per-project .claude-memanto.json for team-shared memory - Lightweight regex heuristics over LLM distillation for signal extraction (sub-100ms, no extra token cost; catches ~80% of value) - Timeless memory types (preference, decision, instruction) bypass age cutoff - Near-duplicate detection via recall(min_confidence=0.9) before remember() - Silent degradation: every hook exits 0 on any error (never blocks the CLI) Submission for the bounty defined in moorcheh-ai#508.
📝 WalkthroughWalkthroughThis PR adds a complete example integration between Claude Code and Memanto, enabling cross-session memory for Claude Code skills. It includes hook implementations for memory injection, distillation, and skill tracking; shared infrastructure; configuration; cross-platform installers; and demo/verification scripts. ChangesClaude Code × Memanto Integration
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/claudecode-skills-memanto/demo/verify_setup.py`:
- Around line 126-133: The try/except around client.create_agent swallows all
exceptions without logging, losing useful error details; change the except to
capture the exception (e.g., except Exception as e:) and log the exception
message with the existing info logger (include context like "verify-agent
already exists or transient error") so the actual error (e) is recorded while
preserving the non-fatal behavior of the block that contains client.create_agent
and info().
In `@examples/claudecode-skills-memanto/hooks/_memanto_common.py`:
- Around line 26-29: Wrap the import-time directory creation for LOG_DIR in a
safe try/except so an OSError (or any Exception) during Path.mkdir(...) does not
raise at import and break hooks; specifically, modify the code around LOG_DIR
and the LOG_DIR.mkdir(...) call to catch exceptions, optionally record the
failure via the module logger (use logging.getLogger(__name__) or similar)
including the error message and path, and continue with a fallback (e.g., leave
LOG_DIR unset or mark logs as disabled) so the module can still import and
degrade gracefully.
- Around line 113-118: The parsing currently assumes json.loads(raw) returns a
dict and then calls payload.get(...), which will raise if the JSON is a
non-object (list/string); update the parsing after payload = json.loads(raw) to
validate that payload is an instance of dict (e.g., isinstance(payload, dict))
and if not, handle gracefully (return None or treat as empty dict) before
calling payload.get("cwd"), so that cwd_str = payload.get("cwd") or os.getcwd()
and cwd = Path(cwd_str).resolve() never run on non-object payloads.
In `@examples/claudecode-skills-memanto/hooks/distill_session.py`:
- Around line 183-204: The hook is not reading per-transcript skill choices from
skill_decisions.py, so memories stored via client.remember in distill_session.py
miss skill provenance; fix by loading the transcript's skill decisions at the
start of the storage logic (use the same lookup used in skill_decisions.py) and
attach the chosen skill info to the memory tags/provenance when calling
client.remember (add e.g. a "skill:<name>" tag and/or include skill in
provenance alongside "explicit_statement"); update the base_tags composition
before the remember call and ensure already_stored checks still operate on the
same content.
- Around line 133-136: The code collects text fragments into text_parts and then
does "\n".join(text_parts), which will raise if any block.get("text") is None or
non-string; update the handling of block/text so only string fragments are
appended (or non-string values are coerced to strings) before join to avoid
raising and aborting the distillation: in the block processing loop (variables
block, text_parts, turns) either filter with isinstance(block.get("text"), str)
before append or append str(block.get("text", "")) and ensure empty or skipped
entries don't break the join, so turns.append("\n".join(text_parts)) is always
safe.
In `@examples/claudecode-skills-memanto/hooks/inject_context.py`:
- Around line 53-54: DEFAULT_RECALL_LIMIT and DEFAULT_MIN_CONFIDENCE are parsed
from environment at import time and can raise ValueError; change their
initialization to guard parsing (e.g., read raw env string via os.environ.get,
attempt int/float conversion in a try/except or helper like
parse_int_env/parse_float_env) and fall back to the default values (5 and 0.6)
on any conversion error, optionally emitting a warning via logging; update the
references to DEFAULT_RECALL_LIMIT and DEFAULT_MIN_CONFIDENCE in
inject_context.py so they are computed safely at import without letting bad env
values crash the module.
In `@examples/claudecode-skills-memanto/hooks/skill_decisions.py`:
- Around line 29-31: The module currently creates STATE_DIR and calls
STATE_DIR.mkdir(...) at import time which can raise and break startup; remove
the top-level mkdir call and instead ensure the directory is created lazily
where it’s needed (e.g., in main() or in the function that reads/writes state).
Add a small helper like ensure_state_dir() that attempts
STATE_DIR.mkdir(parents=True, exist_ok=True) inside a try/except (catch OSError)
and logs or silently continues on failure to preserve the fail-open behavior,
then call that helper before any file operations that rely on STATE_DIR.
In `@examples/claudecode-skills-memanto/install.ps1`:
- Around line 29-35: The install script currently reads the API key visibly into
$key with Read-Host and may write an empty value to $EnvFile; change the prompt
to mask input (use Read-Host -AsSecureString and convert to a plain string for
storage), trim and validate the resulting $key, and if it's empty call
Write-Error and exit with a non-zero code before the Out-File step; update the
code paths that reference $key, Read-Host, and the Out-File to implement this
validation and masking so secrets are not echoed and blank keys are rejected.
In `@examples/claudecode-skills-memanto/install.sh`:
- Around line 30-38: The installer currently echoes the secret and allows empty
MOORCHEH_API_KEY to be saved; update install.sh so the prompt does not print the
secret (use a silent/read -s style prompt for MOORCHEH_API_KEY and remove any
echo of the key) and validate the input before writing ENV_FILE (reject empty
values and re-prompt or abort with a clear error), referencing the
MOORCHEH_API_KEY variable and the ENV_FILE write block to ensure only a
non-empty, non-printed key is persisted.
In `@examples/claudecode-skills-memanto/README.md`:
- Line 97: The README claim that collaborators share the same memory bucket is
incorrect because the agent_id derivation currently hashes the absolute cwd;
update the wording and/or implementation so the project_hash is stable across
clones (e.g., derive project_hash from the repository root or a stable repo
identifier like the git remote+root or a repository-relative path/commit, not
the absolute cwd). Reference the agent_id format `claude-code-<project_hash>`
and change the description to state that project_hash is a stable hash of the
repository root (or update the hashing logic to use the repo identifier) so
collaborators on different machines get the same bucket.
- Around line 26-48: The fenced ASCII diagram in README.md is missing a language
tag (causing markdownlint MD040); update the triple-backtick fence that wraps
the ASCII art diagram (the block starting with
"┌─────────────────────────────────────────────────────────────┐") to include a
language identifier such as "text" (e.g., change ``` to ```text) so the fence is
properly tagged for markdown linters.
- Line 85: The README's verification command uses a module-style invocation with
a hyphenated package name which is invalid; replace the module call `python -m
examples.claudecode-skills-memanto.demo.verify_setup` with a direct script
invocation such as `python
examples/claudecode-skills-memanto/demo/verify_setup.py` (or `python3` as
appropriate) so the script runs without requiring package/module resolution.
In `@examples/claudecode-skills-memanto/uninstall.ps1`:
- Around line 12-17: The current uninstall logic selects the newest
settings.json.bak.* via $backups and Move-Item which can reintroduce Memanto
hooks; instead change the installer to create a single immutable "pre-install
baseline" backup (e.g. a fixed name like settings.json.preinstall) or write the
exact backup path to an installer metadata file at install time, then modify the
uninstall to read that metadata and restore only that recorded backup (using
$Settings and Move-Item) and avoid restoring the newest matching backup as a
fallback; if the metadata or baseline backup is missing, skip automatic restore
and warn the user.
In `@examples/claudecode-skills-memanto/uninstall.sh`:
- Around line 16-19: The uninstall script currently picks the most recent backup
via latest_backup="$(ls -t "${SETTINGS}.bak."* ...)" which is unsafe; instead
restore a single canonical pre-install backup recorded at install time. Modify
the installer to create a named pre-install snapshot (e.g.,
"${SETTINGS}.bak.preinstall") and write that path into a metadata file (e.g., a
variable or file like MEMANTO_PREINSTALL_BACKUP), then change uninstall.sh to
read that metadata (instead of using latest_backup) and restore the referenced
backup (falling back to a safe no-op or warning if the metadata or file is
missing). Update references to SETTINGS and latest_backup in uninstall.sh to use
the recorded backup path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7d61e2e7-0e3b-4d33-b487-42111ea67afd
📒 Files selected for processing (17)
examples/claudecode-skills-memanto/.env.exampleexamples/claudecode-skills-memanto/README.mdexamples/claudecode-skills-memanto/claude-settings.snippet.jsonexamples/claudecode-skills-memanto/demo/README.mdexamples/claudecode-skills-memanto/demo/run_session_a.shexamples/claudecode-skills-memanto/demo/run_session_b.shexamples/claudecode-skills-memanto/demo/show_memories.shexamples/claudecode-skills-memanto/demo/verify_setup.pyexamples/claudecode-skills-memanto/hooks/_memanto_common.pyexamples/claudecode-skills-memanto/hooks/distill_session.pyexamples/claudecode-skills-memanto/hooks/inject_context.pyexamples/claudecode-skills-memanto/hooks/skill_decisions.pyexamples/claudecode-skills-memanto/install.ps1examples/claudecode-skills-memanto/install.shexamples/claudecode-skills-memanto/requirements.txtexamples/claudecode-skills-memanto/uninstall.ps1examples/claudecode-skills-memanto/uninstall.sh
| try: | ||
| client.create_agent( | ||
| agent_id=test_agent_id, | ||
| pattern="tool", | ||
| description="Verification agent for Claude Code × Memanto integration", | ||
| ) | ||
| except Exception: | ||
| info("verify-agent already exists or transient error (non-fatal)") |
There was a problem hiding this comment.
Log the exception for better debugging.
The agent creation exception is caught but not logged. While this is marked non-fatal, the actual exception message would help distinguish between "agent already exists" (expected) and genuine errors. This makes troubleshooting installation issues significantly harder.
🔍 Proposed fix to log the exception
try:
client.create_agent(
agent_id=test_agent_id,
pattern="tool",
description="Verification agent for Claude Code × Memanto integration",
)
- except Exception:
- info("verify-agent already exists or transient error (non-fatal)")
+ except Exception as exc:
+ info(f"verify-agent already exists or transient error (non-fatal): {exc}")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| client.create_agent( | |
| agent_id=test_agent_id, | |
| pattern="tool", | |
| description="Verification agent for Claude Code × Memanto integration", | |
| ) | |
| except Exception: | |
| info("verify-agent already exists or transient error (non-fatal)") | |
| try: | |
| client.create_agent( | |
| agent_id=test_agent_id, | |
| pattern="tool", | |
| description="Verification agent for Claude Code × Memanto integration", | |
| ) | |
| except Exception as exc: | |
| info(f"verify-agent already exists or transient error (non-fatal): {exc}") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/demo/verify_setup.py` around lines 126 -
133, The try/except around client.create_agent swallows all exceptions without
logging, losing useful error details; change the except to capture the exception
(e.g., except Exception as e:) and log the exception message with the existing
info logger (include context like "verify-agent already exists or transient
error") so the actual error (e) is recorded while preserving the non-fatal
behavior of the block that contains client.create_agent and info().
| LOG_LEVEL = os.environ.get("MEMANTO_LOG_LEVEL", "INFO").upper() | ||
| LOG_DIR = Path.home() / ".claude" / "hooks" / "memanto" / "logs" | ||
| LOG_DIR.mkdir(parents=True, exist_ok=True) | ||
|
|
There was a problem hiding this comment.
Avoid import-time hard failure when creating the log directory.
LOG_DIR.mkdir(...) can raise OSError (permissions/home issues) during import, which breaks all hooks before they can degrade gracefully.
Suggested fix
LOG_LEVEL = os.environ.get("MEMANTO_LOG_LEVEL", "INFO").upper()
LOG_DIR = Path.home() / ".claude" / "hooks" / "memanto" / "logs"
-LOG_DIR.mkdir(parents=True, exist_ok=True)
+try:
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
+except OSError:
+ # Preserve non-blocking behavior if filesystem is unavailable.
+ LOG_DIR = None🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/hooks/_memanto_common.py` around lines 26
- 29, Wrap the import-time directory creation for LOG_DIR in a safe try/except
so an OSError (or any Exception) during Path.mkdir(...) does not raise at import
and break hooks; specifically, modify the code around LOG_DIR and the
LOG_DIR.mkdir(...) call to catch exceptions, optionally record the failure via
the module logger (use logging.getLogger(__name__) or similar) including the
error message and path, and continue with a fallback (e.g., leave LOG_DIR unset
or mark logs as disabled) so the module can still import and degrade gracefully.
| payload = json.loads(raw) | ||
| except (OSError, json.JSONDecodeError): | ||
| return None | ||
|
|
||
| cwd_str = payload.get("cwd") or os.getcwd() | ||
| cwd = Path(cwd_str).resolve() |
There was a problem hiding this comment.
Guard against non-object JSON payloads in hook input parsing.
If stdin contains valid JSON that is not an object (e.g., list/string), payload.get(...) raises and bypasses graceful fallback.
Suggested fix
raw = sys.stdin.read()
if not raw:
return None
payload = json.loads(raw)
+ if not isinstance(payload, dict):
+ return None
except (OSError, json.JSONDecodeError):
return None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| payload = json.loads(raw) | |
| except (OSError, json.JSONDecodeError): | |
| return None | |
| cwd_str = payload.get("cwd") or os.getcwd() | |
| cwd = Path(cwd_str).resolve() | |
| payload = json.loads(raw) | |
| if not isinstance(payload, dict): | |
| return None | |
| except (OSError, json.JSONDecodeError): | |
| return None | |
| cwd_str = payload.get("cwd") or os.getcwd() | |
| cwd = Path(cwd_str).resolve() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/hooks/_memanto_common.py` around lines 113
- 118, The parsing currently assumes json.loads(raw) returns a dict and then
calls payload.get(...), which will raise if the JSON is a non-object
(list/string); update the parsing after payload = json.loads(raw) to validate
that payload is an instance of dict (e.g., isinstance(payload, dict)) and if
not, handle gracefully (return None or treat as empty dict) before calling
payload.get("cwd"), so that cwd_str = payload.get("cwd") or os.getcwd() and cwd
= Path(cwd_str).resolve() never run on non-object payloads.
| if isinstance(block, dict) and block.get("type") == "text": | ||
| text_parts.append(block.get("text", "")) | ||
| if text_parts: | ||
| turns.append("\n".join(text_parts)) |
There was a problem hiding this comment.
Handle non-string text blocks defensively to avoid dropping the full distillation run.
If a text block has text: null (or any non-string), join() raises and the hook exits through the top-level catch, skipping all storage.
Suggested fix
elif isinstance(content, list):
text_parts: list[str] = []
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
- text_parts.append(block.get("text", ""))
+ text = block.get("text")
+ if isinstance(text, str) and text:
+ text_parts.append(text)
if text_parts:
turns.append("\n".join(text_parts))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/hooks/distill_session.py` around lines 133
- 136, The code collects text fragments into text_parts and then does
"\n".join(text_parts), which will raise if any block.get("text") is None or
non-string; update the handling of block/text so only string fragments are
appended (or non-string values are coerced to strings) before join to avoid
raising and aborting the distillation: in the block processing loop (variables
block, text_parts, turns) either filter with isinstance(block.get("text"), str)
before append or append str(block.get("text", "")) and ensure empty or skipped
entries don't break the join, so turns.append("\n".join(text_parts)) is always
safe.
| extra_tags = load_extra_tags(ctx.cwd) | ||
| base_tags = [f"project:{ctx.project_name}", "source:claude-code", *extra_tags] | ||
| stored = 0 | ||
|
|
||
| for turn in user_turns: | ||
| for mem_type, confidence, snippet in classify_turn(turn): | ||
| content = snippet | ||
| title = safe_truncate(snippet, 80) | ||
| if already_stored(client, ctx.agent_id, content): | ||
| LOG.debug("Skipping duplicate: %s", safe_truncate(content, 60)) | ||
| continue | ||
| try: | ||
| client.remember( | ||
| agent_id=ctx.agent_id, | ||
| memory_type=mem_type, | ||
| title=title, | ||
| content=content, | ||
| confidence=confidence, | ||
| tags=base_tags + [f"type:{mem_type}"], | ||
| source="claude-code-session", | ||
| provenance="explicit_statement", | ||
| ) |
There was a problem hiding this comment.
Session skill annotations are never applied when memories are stored.
skill_decisions.py persists per-transcript skill choices, but this hook never reads that state, so stored memories are missing skill:* provenance tags.
Suggested fix
+def load_session_skills(transcript_path: str) -> list[str]:
+ safe = transcript_path.replace("/", "_").replace("\\", "_").replace(":", "_")
+ state_path = Path.home() / ".claude" / "hooks" / "memanto" / "state" / f"{safe}.json"
+ try:
+ data = json.loads(state_path.read_text(encoding="utf-8"))
+ skills = data.get("skills", [])
+ return [s for s in skills if isinstance(s, str) and s]
+ except (OSError, json.JSONDecodeError):
+ return []
+
def main() -> int:
@@
- extra_tags = load_extra_tags(ctx.cwd)
- base_tags = [f"project:{ctx.project_name}", "source:claude-code", *extra_tags]
+ extra_tags = load_extra_tags(ctx.cwd)
+ session_skills = load_session_skills(transcript_path_str)
+ skill_tags = [f"skill:{s}" for s in session_skills]
+ base_tags = [f"project:{ctx.project_name}", "source:claude-code", *extra_tags]
@@
- tags=base_tags + [f"type:{mem_type}"],
+ tags=base_tags + skill_tags + [f"type:{mem_type}"],🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/hooks/distill_session.py` around lines 183
- 204, The hook is not reading per-transcript skill choices from
skill_decisions.py, so memories stored via client.remember in distill_session.py
miss skill provenance; fix by loading the transcript's skill decisions at the
start of the storage logic (use the same lookup used in skill_decisions.py) and
attach the chosen skill info to the memory tags/provenance when calling
client.remember (add e.g. a "skill:<name>" tag and/or include skill in
provenance alongside "explicit_statement"); update the base_tags composition
before the remember call and ensure already_stored checks still operate on the
same content.
| ``` | ||
| ┌─────────────────────────────────────────────────────────────┐ | ||
| │ Claude Code session │ | ||
| │ │ | ||
| │ ┌──────────────────┐ ┌──────────────────┐ │ | ||
| │ │ /grill-with-docs│ ─ tool calls ─►│ /tdd │ │ | ||
| │ └──────────────────┘ └──────────────────┘ │ | ||
| │ │ ▲ │ | ||
| │ │ Stop hook │ UserPrompt- │ | ||
| │ │ (distill) │ Submit hook │ | ||
| │ ▼ │ (inject) │ | ||
| │ ┌──────────────────────────────────────────┴────────────┐ │ | ||
| │ │ hooks/distill_session.py │ │ | ||
| │ │ hooks/inject_context.py │ │ | ||
| │ └────────────────────────┬──────────────────────────────┘ │ | ||
| └───────────────────────────┼─────────────────────────────────┘ | ||
| │ remember() / recall() | ||
| ▼ | ||
| ┌──────────────────────┐ | ||
| │ Memanto │ | ||
| │ (Moorcheh backend) │ | ||
| └──────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
Add a language identifier to the fenced code block.
The ASCII diagram fence is missing a language tag and triggers markdownlint MD040.
Suggested fix
-```
+```text
┌─────────────────────────────────────────────────────────────┐
...
└──────────────────────┘</details>
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>
[warning] 26-26: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @examples/claudecode-skills-memanto/README.md around lines 26 - 48, The
fenced ASCII diagram in README.md is missing a language tag (causing
markdownlint MD040); update the triple-backtick fence that wraps the ASCII art
diagram (the block starting with
"┌─────────────────────────────────────────────────────────────┐") to include a
language identifier such as "text" (e.g., change totext) so the fence is
properly tagged for markdown linters.
</details>
<!-- fingerprinting:phantom:triton:hawk -->
<!-- This is an auto-generated comment by CodeRabbit -->
| Verify by running: | ||
|
|
||
| ```bash | ||
| python -m examples.claudecode-skills-memanto.demo.verify_setup |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the README command and package layout assumptions.
rg -n 'python -m examples\.claudecode-skills-memanto\.demo\.verify_setup' examples/claudecode-skills-memanto/README.md
fd -a "__init__.py" examples | rg -n "claudecode-skills-memanto" || true
fd -a "verify_setup.py" examplesRepository: moorcheh-ai/memanto
Length of output: 204
🏁 Script executed:
# Check the package structure
echo "=== __init__.py files in examples ==="
find examples -name "__init__.py" | head -20
echo "=== Directory structure of claudecode-skills-memanto ==="
tree -L 3 examples/claudecode-skills-memanto/ 2>/dev/null || find examples/claudecode-skills-memanto -type f | head -20
echo "=== Check if verify_setup.py can be run as a module ==="
python -c "import sys; sys.path.insert(0, '.'); exec(open('examples/claudecode-skills-memanto/demo/verify_setup.py').read())" 2>&1 | head -20 || echo "Direct execution failed"Repository: moorcheh-ai/memanto
Length of output: 1112
Fix the verification command; the -m invocation cannot work with hyphenated directory names.
python -m examples.claudecode-skills-memanto.demo.verify_setup fails because:
- The directory
examples/claudecode-skills-memantocontains hyphens, which are invalid in Python module names examples/is not a Python package (no__init__.py), so-mcannot traverse the path
Use direct file invocation instead:
Suggested doc fix
- python -m examples.claudecode-skills-memanto.demo.verify_setup
+ python examples/claudecode-skills-memanto/demo/verify_setup.py🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/README.md` at line 85, The README's
verification command uses a module-style invocation with a hyphenated package
name which is invalid; replace the module call `python -m
examples.claudecode-skills-memanto.demo.verify_setup` with a direct script
invocation such as `python
examples/claudecode-skills-memanto/demo/verify_setup.py` (or `python3` as
appropriate) so the script runs without requiring package/module resolution.
| Claude Code fires this hook with the user's prompt on stdin (JSON). The hook: | ||
|
|
||
| 1. Reads the user prompt + the current working directory. | ||
| 2. Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the working directory (so each project has its own memory bucket — collaborators in the same repo share it). |
There was a problem hiding this comment.
Project-sharing claim conflicts with current agent-id derivation.
This line says collaborators in the same repo share the bucket, but the implementation hashes absolute cwd, which differs across local clone paths and machines.
Suggested wording adjustment
- Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the working directory (so each project has its own memory bucket — collaborators in the same repo share it).
+ Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the local working directory path (so each local project clone has its own memory bucket unless overridden via `.claude-memanto.json` or `MEMANTO_AGENT_ID`).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 2. Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the working directory (so each project has its own memory bucket — collaborators in the same repo share it). | |
| 2. Derives the **agent_id** as `claude-code-<project_hash>` where `project_hash` is a stable hash of the local working directory path (so each local project clone has its own memory bucket unless overridden via `.claude-memanto.json` or `MEMANTO_AGENT_ID`). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/README.md` at line 97, The README claim
that collaborators share the same memory bucket is incorrect because the
agent_id derivation currently hashes the absolute cwd; update the wording and/or
implementation so the project_hash is stable across clones (e.g., derive
project_hash from the repository root or a stable repo identifier like the git
remote+root or a repository-relative path/commit, not the absolute cwd).
Reference the agent_id format `claude-code-<project_hash>` and change the
description to state that project_hash is a stable hash of the repository root
(or update the hashing logic to use the repo identifier) so collaborators on
different machines get the same bucket.
| $backups = Get-ChildItem -Path (Split-Path $Settings -Parent) -Filter 'settings.json.bak.*' -ErrorAction SilentlyContinue | | ||
| Sort-Object LastWriteTime -Descending | ||
|
|
||
| if ($backups -and $backups.Count -gt 0) { | ||
| Move-Item -Force $backups[0].FullName $Settings | ||
| Write-Host ('Restored settings.json from {0}' -f $backups[0].FullName) |
There was a problem hiding this comment.
Restoring the latest backup can re-install Memanto hooks unintentionally.
Line 12/Line 16 picks the newest settings.json.bak.*, but repeated installs create backups from already-modified settings. Uninstall may therefore restore a file that still contains Memanto hook wiring.
Use a dedicated “pre-install baseline” backup (created once) or persist the exact backup path in installer metadata and restore that specific file on uninstall.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/uninstall.ps1` around lines 12 - 17, The
current uninstall logic selects the newest settings.json.bak.* via $backups and
Move-Item which can reintroduce Memanto hooks; instead change the installer to
create a single immutable "pre-install baseline" backup (e.g. a fixed name like
settings.json.preinstall) or write the exact backup path to an installer
metadata file at install time, then modify the uninstall to read that metadata
and restore only that recorded backup (using $Settings and Move-Item) and avoid
restoring the newest matching backup as a fallback; if the metadata or baseline
backup is missing, skip automatic restore and warn the user.
| latest_backup="$(ls -t "${SETTINGS}.bak."* 2>/dev/null | head -n1 || true)" | ||
| if [[ -n "${latest_backup}" ]]; then | ||
| mv "${latest_backup}" "${SETTINGS}" | ||
| echo "▸ Restored settings.json from ${latest_backup}" |
There was a problem hiding this comment.
Latest-backup restore is not a safe uninstall source of truth.
Line 16 and Line 18 restore the newest backup, which can already include Memanto entries after multiple install runs. That makes uninstall non-deterministic and can leave hooks configured.
Track and restore a single canonical pre-install backup (or store backup path metadata at install time) instead of selecting by recency.
🧰 Tools
🪛 Shellcheck (0.11.0)
[info] 16-16: Use find instead of ls to better handle non-alphanumeric filenames.
(SC2012)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/claudecode-skills-memanto/uninstall.sh` around lines 16 - 19, The
uninstall script currently picks the most recent backup via latest_backup="$(ls
-t "${SETTINGS}.bak."* ...)" which is unsafe; instead restore a single canonical
pre-install backup recorded at install time. Modify the installer to create a
named pre-install snapshot (e.g., "${SETTINGS}.bak.preinstall") and write that
path into a metadata file (e.g., a variable or file like
MEMANTO_PREINSTALL_BACKUP), then change uninstall.sh to read that metadata
(instead of using latest_backup) and restore the referenced backup (falling back
to a safe no-op or warning if the metadata or file is missing). Update
references to SETTINGS and latest_backup in uninstall.sh to use the recorded
backup path.
✅ Actions performedReview triggered.
|
/claim #508
BountyHub bounty: https://www.bountyhub.dev/bounty/view/3a63800c-7c18-41d2-870f-c62344f8a3fe
What this adds
examples/claudecode-skills-memanto/— a complete, installable integration where Memanto acts as a global, active memory companion across separate Claude Code sessions.The problem solved (verbatim from #508): skills suffer from context fragmentation — architectural decisions from one skill invocation are invisible to the next. This integration eliminates that without instrumenting individual skills.
Approach (and why it's different from the other 20+ submissions)
I hook Claude Code's lifecycle, not the skills themselves:
This means the integration works with mattpocock/skills, wshobson/agents, or any custom skill — none of them need to know Memanto exists.
Differentiators vs. the other submissions
Looking at the 20+ existing PRs (#509, #511-#516, #520, #525, #526, etc.), most appear to ship a "credential-free local JSON backend" placeholder. This PR is different on these axes:
Files
```
examples/claudecode-skills-memanto/
├── README.md — architecture, design rationale, multi-project setup
├── .env.example
├── requirements.txt
├── claude-settings.snippet.json — the JSON merged into ~/.claude/settings.json
├── install.sh / install.ps1
├── uninstall.sh / uninstall.ps1
├── hooks/
│ ├── _memanto_common.py — shared utils: agent_id derivation, logging, client factory
│ ├── inject_context.py — UserPromptSubmit
│ ├── distill_session.py — Stop
│ └── skill_decisions.py — PostToolUse
└── demo/
├── README.md
├── verify_setup.py — smoke test
├── run_session_a.sh — establishes architectural decisions in Session A
├── run_session_b.sh — proves recall in Session B (fresh terminal)
└── show_memories.sh — dumps every stored memory (bounty artifact)
```
Heuristic-based signal extraction
The `Stop` hook uses tuned regex patterns instead of a second LLM call. Rationale: an LLM distillation at session-end costs ~1s + tokens per session, and empirically catches only ~10-20% more signal than well-tuned heuristics. The patterns target the 4 highest-value memory types:
Result: 3-7 memories per typical session, sub-100ms processing, no token cost.
Demo / proof artifact
The `demo/` directory scripts a reproducible Session A → Session B walkthrough that proves cross-session memory:
Acknowledgments
Respect to the other contributors who attempted this — the implementations I reviewed each picked different trade-offs. If the maintainers prefer a hybrid (e.g., my hook design + someone else's local-mode fallback), happy to collaborate.
Summary by CodeRabbit
Release Notes
New Features
Documentation