Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit 0ce2502

Browse files
zircoteclaude
andcommitted
feat(hooks): add visual indicators and fix validate cleanup
Visual indicators (emoji icons) added to all hooks: - 📚 SessionStart: memory count status - 💾 UserPromptSubmit: capture confirmation - 🔍 PostToolUse: related memories found - 🛑 Stop: uncaptured content warning validate command improvements: - Auto-cleanup test memories after validation - Verify cleanup via search instead of count comparison - Use reindex() instead of non-existent sync() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 780e86a commit 0ce2502

7 files changed

Lines changed: 101 additions & 18 deletions

File tree

commands/validate.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,39 @@ print("## 8. Cleanup")
332332
print("-" * 40)
333333
334334
if test_memory_id:
335-
test_pass("Test cleanup", f"Test memory {test_memory_id[:16]}... will persist")
336-
log("To remove: git notes --ref=refs/notes/mem/learnings remove <commit>", 1)
335+
try:
336+
# Parse memory ID: {namespace}:{commit_sha}:{index}
337+
parts = test_memory_id.split(":")
338+
if len(parts) >= 2:
339+
namespace = parts[0]
340+
commit_sha = parts[1]
341+
342+
# Remove the git note
343+
log(f"Removing test note from refs/notes/mem/{namespace}...", 1)
344+
rm_result = subprocess.run(
345+
["git", "notes", f"--ref=refs/notes/mem/{namespace}", "remove", commit_sha],
346+
capture_output=True, text=True
347+
)
348+
349+
if rm_result.returncode == 0:
350+
# Reindex to update SQLite index
351+
sync_svc = get_sync_service()
352+
sync_svc.reindex()
353+
354+
# Verify the test note is gone by searching for the marker
355+
verify_results = recall.search(query=test_marker, k=5, namespace=namespace)
356+
still_exists = any(test_marker in (r.content or '') for r in verify_results)
357+
358+
if not still_exists:
359+
test_pass("Test cleanup", f"Removed {test_memory_id[:20]}...")
360+
else:
361+
test_warn("Test cleanup", "Note removed but still in index")
362+
else:
363+
test_warn("Test cleanup", f"git notes remove failed: {rm_result.stderr.strip()}")
364+
else:
365+
test_warn("Test cleanup", f"Invalid memory ID format: {test_memory_id}")
366+
except Exception as e:
367+
test_warn("Test cleanup", f"Cleanup failed: {e}")
337368
else:
338369
test_pass("Test cleanup", "No test memory to clean up")
339370

hooks/sessionstart.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ def build_context_message(status: dict) -> str:
109109
return "\n".join(lines)
110110

111111

112+
def build_user_message(status: dict) -> str:
113+
"""Build a concise user-visible status message.
114+
115+
Args:
116+
status: Memory system status dict.
117+
118+
Returns:
119+
Short status message with emoji indicator.
120+
"""
121+
if status.get("initialized"):
122+
total = status.get("total_memories", 0)
123+
return f"📚 Memory system: {total} memories indexed"
124+
else:
125+
return "📚 Memory system: not initialized"
126+
127+
112128
def main() -> None:
113129
"""Main hook entry point."""
114130
# Read hook input from stdin (not used but must be consumed)
@@ -120,13 +136,15 @@ def main() -> None:
120136
# Get memory system status
121137
status = get_memory_status()
122138

123-
# Build context message
139+
# Build context message (for Claude) and user message (visible)
124140
context_message = build_context_message(status)
141+
user_message = build_user_message(status)
125142

126-
# Output the context injection
143+
# Output the context injection with user-visible message
127144
output = {
128145
"continue": True,
129146
"additionalContext": context_message,
147+
"message": user_message,
130148
}
131149

132150
print(json.dumps(output))

hooks/userpromptsubmit.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,14 @@ def main() -> None:
110110
# Capture the memory
111111
result = capture_memory(content)
112112

113-
# Output result (Claude Code will display any messages)
113+
# Output result with visual indicator
114114
output = {"continue": True}
115115
if result.get("success"):
116-
output["message"] = result.get("message", "Memory captured")
116+
# Extract summary for display (first 50 chars of content)
117+
summary = content[:50] + "..." if len(content) > 50 else content
118+
output["message"] = f"💾 Captured to learnings: \"{summary}\""
117119
else:
118-
output["warning"] = f"Failed to capture: {result.get('error', 'Unknown error')}"
120+
output["warning"] = f"💾 Capture failed: {result.get('error', 'Unknown error')}"
119121

120122
print(json.dumps(output))
121123

src/git_notes_memory/hooks/post_tool_use_handler.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def _format_memories_xml(
230230
return builder.to_string()
231231

232232

233-
def _write_output(context: str | None = None) -> None:
233+
def _write_output(context: str | None = None, memory_count: int = 0) -> None:
234234
"""Write hook output to stdout.
235235
236236
Args:
@@ -243,6 +243,9 @@ def _write_output(context: str | None = None) -> None:
243243
"additionalContext": context,
244244
}
245245
}
246+
# Add user-visible message when memories are found
247+
if memory_count > 0:
248+
output["message"] = f"🔍 Found {memory_count} related memories"
246249
else:
247250
output = {"continue": True}
248251

@@ -329,8 +332,8 @@ def main() -> None:
329332
# Format as XML
330333
context = _format_memories_xml(results, file_path)
331334

332-
# Output result
333-
_write_output(context)
335+
# Output result with memory count for user message
336+
_write_output(context, memory_count=len(results))
334337

335338
except json.JSONDecodeError as e:
336339
logger.error("Failed to parse hook input: %s", e)

src/git_notes_memory/hooks/session_start_handler.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@
3434
import sys
3535
from typing import Any
3636

37-
from git_notes_memory.config import HOOK_SESSION_START_TIMEOUT
37+
from git_notes_memory.config import HOOK_SESSION_START_TIMEOUT, get_index_path
3838
from git_notes_memory.hooks.config_loader import load_hook_config
3939
from git_notes_memory.hooks.context_builder import ContextBuilder
4040
from git_notes_memory.hooks.guidance_builder import GuidanceBuilder
4141
from git_notes_memory.hooks.project_detector import detect_project
42+
from git_notes_memory.index import IndexService
4243

4344
__all__ = ["main"]
4445

@@ -117,18 +118,43 @@ def _validate_input(data: dict[str, Any]) -> bool:
117118
return all(field in data and data[field] for field in required_fields)
118119

119120

120-
def _write_output(context: str) -> None:
121+
def _get_memory_count() -> int:
122+
"""Get total memory count from index.
123+
124+
Returns:
125+
Number of memories indexed, or 0 if index doesn't exist.
126+
"""
127+
try:
128+
index_path = get_index_path()
129+
if not index_path.exists():
130+
return 0
131+
index = IndexService(index_path)
132+
index.initialize()
133+
stats = index.get_stats()
134+
index.close()
135+
return stats.total_memories
136+
except Exception:
137+
return 0
138+
139+
140+
def _write_output(context: str, memory_count: int = 0) -> None:
121141
"""Write hook output to stdout.
122142
123143
Args:
124144
context: XML context string to inject.
145+
memory_count: Number of memories in the system.
125146
"""
126-
output = {
147+
output: dict[str, Any] = {
127148
"hookSpecificOutput": {
128149
"hookEventName": "SessionStart",
129150
"additionalContext": context,
130151
}
131152
}
153+
# Add user-visible status message
154+
if memory_count > 0:
155+
output["message"] = f"📚 Memory system: {memory_count} memories indexed"
156+
else:
157+
output["message"] = "📚 Memory system: initialized"
132158
print(json.dumps(output))
133159

134160

@@ -214,8 +240,11 @@ def main() -> None:
214240

215241
logger.debug("Total context (%d chars)", len(full_context))
216242

217-
# Output result
218-
_write_output(full_context)
243+
# Get memory count for status message
244+
memory_count = _get_memory_count()
245+
246+
# Output result with memory count
247+
_write_output(full_context, memory_count=memory_count)
219248

220249
except json.JSONDecodeError as e:
221250
logger.error("Failed to parse hook input: %s", e)

src/git_notes_memory/hooks/stop_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def _write_output(
256256

257257
# Add a message about uncaptured content
258258
output["message"] = (
259-
f"Found {len(uncaptured)} potentially uncaptured memory(s) "
259+
f"🛑 Found {len(uncaptured)} potentially uncaptured memory(s) "
260260
"from this session. Consider using /remember to capture them."
261261
)
262262

@@ -267,7 +267,7 @@ def _write_output(
267267
hook_output["syncStats"] = stats
268268
indexed = stats.get("indexed", 0)
269269
if indexed > 0:
270-
sync_msg = f"Index synced: {indexed} memories indexed"
270+
sync_msg = f"📚 Index synced: {indexed} memories indexed"
271271
if "message" in output:
272272
output["message"] += f"\n{sync_msg}"
273273
else:

src/git_notes_memory/hooks/user_prompt_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def _write_output(
258258
# Report captured memories
259259
successful = [c for c in captured if c.get("success")]
260260
if successful:
261-
output["message"] = f"Captured {len(successful)} memory(s) automatically"
261+
output["message"] = f"💾 Captured {len(successful)} memory(s) automatically"
262262
output["hookSpecificOutput"] = {
263263
"hookEventName": "UserPromptSubmit",
264264
"capturedMemories": captured,

0 commit comments

Comments
 (0)