#!/bin/sh # installed by herdr # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=devin # HERDR_INTEGRATION_VERSION=2 set -eu action="${1:-}" hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-devin-hook.XXXXXX")" || exit 0 trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM cat >"$hook_input_file" 2>/dev/null || true case "$action" in session) ;; *) exit 0 ;; esac [ "${HERDR_ENV:-}" = "1" ] || exit 0 [ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0 [ -n "${HERDR_PANE_ID:-}" ] || exit 0 command -v python3 >/dev/null 2>&1 || exit 0 HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY' from __future__ import annotations import json import os import random import socket import subprocess import time SOURCE = "herdr:devin" AGENT = "devin" def load_hook_input(path: str | None) -> dict: if not path: return {} try: with open(path, encoding="utf-8") as handle: content = handle.read() if not content.strip(): return {} parsed = json.loads(content) return parsed if isinstance(parsed, dict) else {} except Exception: return {} def load_session_list(project_dir: str | None): injected = os.environ.get("HERDR_DEVIN_LIST_JSON") if injected is not None: try: parsed = json.loads(injected) return parsed if isinstance(parsed, list) else [] except Exception: return [] cmd = ["devin", "list", "--format", "json"] try: result = subprocess.run( cmd, cwd=project_dir or None, capture_output=True, text=True, timeout=2, check=False, ) except Exception: return [] if result.returncode != 0: return [] try: parsed = json.loads(result.stdout) return parsed if isinstance(parsed, list) else [] except Exception: return [] def normalize_path(path: str | None) -> str | None: if not isinstance(path, str) or not path: return None try: return os.path.realpath(path) except Exception: return path def hook_session_id(hook_input: dict) -> str | None: for key in ("session_id", "sessionId"): value = hook_input.get(key) if isinstance(value, str) and value: return value return None def hook_event_name(hook_input: dict) -> str: value = hook_input.get("hook_event_name") return value if isinstance(value, str) else "" def allow_session_list_fallback(hook_input: dict) -> bool: event = hook_event_name(hook_input) if event == "UserPromptSubmit": return False if event == "SessionStart" and hook_input.get("source") == "startup": return False return True def resolve_session_id(project_dir: str, hook_input: dict) -> str | None: direct = hook_session_id(hook_input) if direct: return direct if not allow_session_list_fallback(hook_input): return None entries = load_session_list(project_dir) project_dir = normalize_path(project_dir) for entry in entries: if not isinstance(entry, dict): continue session_id = entry.get("id") if not isinstance(session_id, str) or not session_id: continue working_directory = normalize_path(entry.get("working_directory")) if working_directory == project_dir: return session_id return None pane_id = os.environ.get("HERDR_PANE_ID") socket_path = os.environ.get("HERDR_SOCKET_PATH") project_dir = os.environ.get("DEVIN_PROJECT_DIR") or os.getcwd() hook_input = load_hook_input(os.environ.get("HERDR_HOOK_INPUT_FILE")) if not pane_id or not socket_path: raise SystemExit(0) request_id = f"{SOURCE}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" report_seq = time.time_ns() session_id = resolve_session_id(project_dir, hook_input) if not session_id: raise SystemExit(0) request = { "id": request_id, "method": "pane.report_agent_session", "params": { "pane_id": pane_id, "source": SOURCE, "agent": AGENT, "agent_session_id": session_id, "seq": report_seq, }, } try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.settimeout(0.5) client.connect(socket_path) client.sendall((json.dumps(request) + "\n").encode()) try: client.recv(4096) except Exception: pass client.close() except Exception: pass PY