Skip to content

Commit 7c311ee

Browse files
jpheinclaude
andcommitted
fix: sanitize wing param, cross-platform paths, tighten test assertions
Addresses Copilot review feedback on #659. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 201c46b commit 7c311ee

3 files changed

Lines changed: 60 additions & 5 deletions

File tree

mempalace/hooks_cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ def _wing_from_transcript_path(transcript_path: str) -> str:
133133
~/.claude/projects/-home-<user>-Projects-<project>/session.jsonl
134134
We extract <project> as the wing name. Falls back to "sessions".
135135
"""
136-
match = re.search(r"-Projects-([^/]+?)(?:/|$)", transcript_path)
136+
# Normalize path separators for cross-platform (Windows backslashes)
137+
normalized = transcript_path.replace("\\", "/")
138+
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
137139
if match:
138140
return match.group(1).lower().replace(" ", "_")
139141
return "sessions"

mempalace/mcp_server.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def tool_diary_write(agent_name: str, entry: str, topic: str = "general", wing:
554554
return {"success": False, "error": str(e)}
555555

556556
if wing:
557-
wing = wing.lower().replace(" ", "_")
557+
wing = sanitize_name(wing)
558558
else:
559559
wing = f"wing_{agent_name.lower().replace(' ', '_')}"
560560
room = "diary"
@@ -612,10 +612,20 @@ def tool_diary_read(agent_name: str, last_n: int = 10, wing: str = ""):
612612
"""
613613
Read an agent's recent diary entries. Returns the last N entries
614614
in chronological order — the agent's personal journal.
615+
616+
When ``wing`` is provided, reads from that wing instead of the
617+
agent's default ``wing_<agent_name>`` wing. This lets hooks
618+
direct diary reads to a project-specific wing derived from
619+
the transcript path.
615620
"""
616-
if wing:
617-
wing = wing.lower().replace(" ", "_")
618-
else:
621+
try:
622+
agent_name = sanitize_name(agent_name, "agent_name")
623+
if wing:
624+
wing = sanitize_name(wing)
625+
except ValueError as e:
626+
return {"success": False, "error": str(e)}
627+
628+
if not wing:
619629
wing = f"wing_{agent_name.lower().replace(' ', '_')}"
620630
col = _get_collection()
621631
if not col:

tests/test_hooks_cli.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
_maybe_auto_ingest,
1616
_parse_harness_input,
1717
_sanitize_session_id,
18+
_wing_from_transcript_path,
1819
hook_stop,
1920
hook_session_start,
2021
hook_precompact,
@@ -170,6 +171,26 @@ def test_stop_hook_blocks_at_interval(tmp_path):
170171
)
171172
assert result["decision"] == "block"
172173
assert result["reason"].startswith(STOP_BLOCK_REASON)
174+
# Default wing when transcript path doesn't match Claude Code pattern
175+
assert "wing=sessions" in result["reason"]
176+
177+
178+
def test_stop_hook_derives_wing_from_transcript_path(tmp_path):
179+
"""When transcript path looks like a Claude Code path, wing is derived from it."""
180+
project_dir = tmp_path / ".claude" / "projects" / "-home-jp-Projects-myproject"
181+
project_dir.mkdir(parents=True)
182+
transcript = project_dir / "session.jsonl"
183+
_write_transcript(
184+
transcript,
185+
[{"message": {"role": "user", "content": f"msg {i}"}} for i in range(SAVE_INTERVAL)],
186+
)
187+
result = _capture_hook_output(
188+
hook_stop,
189+
{"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)},
190+
state_dir=tmp_path,
191+
)
192+
assert result["decision"] == "block"
193+
assert "wing=myproject" in result["reason"]
173194

174195

175196
def test_stop_hook_tracks_save_point(tmp_path):
@@ -214,6 +235,28 @@ def test_precompact_always_blocks(tmp_path):
214235
assert result["reason"] == PRECOMPACT_BLOCK_REASON
215236

216237

238+
# --- _wing_from_transcript_path ---
239+
240+
241+
def test_wing_from_transcript_path_extracts_project():
242+
path = "/home/jp/.claude/projects/-home-jp-Projects-memorypalace/session.jsonl"
243+
assert _wing_from_transcript_path(path) == "memorypalace"
244+
245+
246+
def test_wing_from_transcript_path_fallback():
247+
assert _wing_from_transcript_path("/some/random/path.jsonl") == "sessions"
248+
249+
250+
def test_wing_from_transcript_path_windows_backslashes():
251+
path = "C:\\Users\\jp\\.claude\\projects\\-home-jp-Projects-myapp\\session.jsonl"
252+
assert _wing_from_transcript_path(path) == "myapp"
253+
254+
255+
def test_wing_from_transcript_path_lowercases():
256+
path = "/home/jp/.claude/projects/-home-jp-Projects-MyProject/session.jsonl"
257+
assert _wing_from_transcript_path(path) == "myproject"
258+
259+
217260
# --- _log ---
218261

219262

0 commit comments

Comments
 (0)