Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/agent_learner/adapters/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import shlex
from pathlib import Path

from .common import ensure_dir, merge_json_file, write_text
from .common import ensure_dir, merge_json_file, upsert_hook, write_text


AUTO_SESSION_LEARNING = """#!/usr/bin/env python3
Expand Down Expand Up @@ -146,3 +146,27 @@ def install_claude_adapter_with_scope(target_root: Path, *, scope: str = "projec

def install_claude_adapter(target_root: Path) -> list[Path]:
return install_claude_adapter_with_scope(target_root, scope="project")


# ---------------------------------------------------------------------------
# Phase 2: lightweight hook installer (mirrors hermit adapter pattern)
# ---------------------------------------------------------------------------

_CLAUDE_HOOK_COMMAND = (
"agent-learner process --adapter claude"
" --session-id $CLAUDE_SESSION_ID"
" --cwd $CWD"
" --model-id $CLAUDE_MODEL"
" --auto"
)


def install_claude_hooks(project_root: Path, *, scope: str = "project") -> Path:
"""Install agent-learner Stop hook into .claude/settings.json.

- scope="project": {project_root}/.claude/settings.json
- Idempotent: updates existing agent-learner hook if present.
- Preserves other Stop hooks.
"""
settings_path = project_root / ".claude" / "settings.json"
return upsert_hook(settings_path, "Stop", _CLAUDE_HOOK_COMMAND, hook_format="claude")
54 changes: 54 additions & 0 deletions src/agent_learner/adapters/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,57 @@ def merge_lists(existing: list, incoming: list) -> list:
result.append(item)
seen.add(key)
return result


def is_agent_learner_hook(hook: dict) -> bool:
"""Check whether a hook entry was installed by agent-learner (both old and new formats)."""
if "agent-learner" in hook.get("command", ""):
return True
for inner in hook.get("hooks", []):
if "agent-learner" in inner.get("command", ""):
return True
return False


def _make_hook_entry(command: str, *, hook_format: str) -> dict:
if hook_format == "claude":
return {
"matcher": ".*",
"hooks": [{"type": "command", "command": command}],
}
return {"command": command}


def upsert_hook(settings_path: Path, event_name: str, command: str, *, hook_format: str = "simple") -> Path:
"""Idempotently install or update an agent-learner hook in a JSON settings file.

hook_format:
"simple" — {"command": "..."} (Hermit, Codex)
"claude" — {"matcher": ".*", "hooks": [{"type": "command", "command": "..."}]} (Claude Code)
"""
ensure_dir(settings_path.parent)

if settings_path.exists():
data = json.loads(settings_path.read_text(encoding="utf-8"))
else:
data = {}

hooks = data.setdefault("hooks", {})
event_hooks: list[dict] = hooks.setdefault(event_name, [])

new_hook = _make_hook_entry(command, hook_format=hook_format)
replaced = False
for i, hook in enumerate(event_hooks):
if is_agent_learner_hook(hook):
event_hooks[i] = new_hook
replaced = True
break
if not replaced:
event_hooks.append(new_hook)

hooks[event_name] = event_hooks
settings_path.write_text(
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
return settings_path
Loading