Ctrl K
ComunidadGuías…

scripts/xkb_install_agent_hook.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Install (or remove) the XKB hook in an agent.

Wires ``xkb_agent_hook.py`` into Claude Code's ``UserPromptSubmit`` and
``Stop`` events so every turn recalls from XKB and is captured back into it,
without the agent having to remember to call anything.

Install is idempotent: re-running replaces the previous entries rather than
stacking duplicates, and ``--uninstall`` removes exactly what was added.
Settings are rewritten atomically (temp file then rename) so an interrupted
run cannot leave the agent with a truncated settings file.

Usage:
    python3 scripts/xkb_install_agent_hook.py --status
    python3 scripts/xkb_install_agent_hook.py --install
    python3 scripts/xkb_install_agent_hook.py --install --url http://127.0.0.1:18972 --token <token>
    python3 scripts/xkb_install_agent_hook.py --uninstall
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from pathlib import Path

SCRIPTS_DIR = Path(__file__).resolve().parent
HOOK_SCRIPT = SCRIPTS_DIR / "xkb_agent_hook.py"
HOOK_CONFIG = SCRIPTS_DIR / "xkb-agent-hook-config.json"
EVENTS = ("UserPromptSubmit", "Stop")
TIMEOUT_SECONDS = 50  # session-open (6s) + recall (40s) + process overhead


def agent_home() -> Path:
    return Path(os.getenv("CLAUDE_CONFIG_DIR") or (Path.home() / ".claude"))


def command() -> str:
    """Quote the interpreter and script so paths with spaces survive."""
    return f'"{sys.executable}" "{HOOK_SCRIPT}"'


def is_ours(entry: dict) -> bool:
    return isinstance(entry, dict) and HOOK_SCRIPT.name in str(entry.get("command", ""))


def strip_ours(groups: object) -> list:
    """Drop previously installed XKB hooks, preserving everything else."""
    if not isinstance(groups, list):
        return []
    kept = []
    for group in groups:
        if not isinstance(group, dict):
            kept.append(group)
            continue
        hooks = group.get("hooks")
        if not isinstance(hooks, list):
            kept.append(group)
            continue
        remaining = [hook for hook in hooks if not is_ours(hook)]
        if remaining:
            kept.append({**group, "hooks": remaining})
        elif not hooks:
            kept.append(group)
    return kept


def read_json(path: Path) -> dict:
    try:
        data = json.loads(path.read_text(encoding="utf-8") or "{}")
    except FileNotFoundError:
        return {}
    except json.JSONDecodeError as exc:
        raise SystemExit(f"{path} is not valid JSON; refusing to overwrite it ({exc})")
    return data if isinstance(data, dict) else {}


def write_json(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temp = path.with_suffix(path.suffix + f".{os.getpid()}.tmp")
    temp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    temp.replace(path)


def install(url: str, token: str, source: str) -> int:
    if not HOOK_SCRIPT.is_file():
        raise SystemExit(f"hook script not found: {HOOK_SCRIPT}")
    settings_path = agent_home() / "settings.json"
    settings = read_json(settings_path)
    hooks = settings.get("hooks")
    hooks = dict(hooks) if isinstance(hooks, dict) else {}
    for event in EVENTS:
        hooks[event] = strip_ours(hooks.get(event)) + [
            {"matcher": "", "hooks": [{"type": "command", "command": command(), "timeout": TIMEOUT_SECONDS}]}
        ]
    settings["hooks"] = hooks
    write_json(settings_path, settings)

    config = {"url": url, "source": source}
    if token:
        config["token"] = token
    write_json(HOOK_CONFIG, config)
    # The config can hold a bearer token, so keep it owner-readable only.
    try:
        HOOK_CONFIG.chmod(0o600)
    except OSError:
        pass

    print(json.dumps({
        "installed": True, "settings": str(settings_path), "events": list(EVENTS),
        "config": str(HOOK_CONFIG), "url": url, "token": bool(token),
    }, ensure_ascii=False))
    return 0


def uninstall() -> int:
    settings_path = agent_home() / "settings.json"
    settings = read_json(settings_path)
    hooks = settings.get("hooks")
    if isinstance(hooks, dict):
        for event in EVENTS:
            remaining = strip_ours(hooks.get(event))
            if remaining:
                hooks[event] = remaining
            else:
                hooks.pop(event, None)
        if hooks:
            settings["hooks"] = hooks
        else:
            settings.pop("hooks", None)
        write_json(settings_path, settings)
    HOOK_CONFIG.unlink(missing_ok=True)
    print(json.dumps({"installed": False, "settings": str(settings_path)}, ensure_ascii=False))
    return 0


def status() -> int:
    settings = read_json(agent_home() / "settings.json")
    hooks = settings.get("hooks") if isinstance(settings.get("hooks"), dict) else {}
    installed = {
        event: any(is_ours(hook)
                   for group in (hooks.get(event) or []) if isinstance(group, dict)
                   for hook in (group.get("hooks") or []))
        for event in EVENTS
    }
    print(json.dumps({
        "agent_home": str(agent_home()),
        "hook_script": str(HOOK_SCRIPT),
        "installed": installed,
        "config_present": HOOK_CONFIG.is_file(),
    }, ensure_ascii=False, indent=2))
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    mode = parser.add_mutually_exclusive_group(required=True)
    mode.add_argument("--install", action="store_true")
    mode.add_argument("--uninstall", action="store_true")
    mode.add_argument("--status", action="store_true")
    parser.add_argument("--url", default=os.getenv("XKB_MEMORY_SERVICE_URL", "http://127.0.0.1:18972"))
    parser.add_argument("--token", default=os.getenv("XKB_SERVICE_TOKEN", ""))
    parser.add_argument("--source", default="claude-code", help="agent id recorded on every captured turn")
    args = parser.parse_args()
    if args.status:
        return status()
    if args.uninstall:
        return uninstall()
    return install(args.url, args.token, args.source)


import xkb_usage  # noqa: E402  — 量測誰在跑,見 scripts/xkb_usage.py

if __name__ == "__main__":
    xkb_usage.record(__file__)
    raise SystemExit(main())