hermes-agentmemory-capture 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ """hermes-agentmemory-capture: captura automática hacia AgentMemory.
2
+
3
+ Port del plugin `agentmemory-capture` de OpenCode. Suscribe hooks del
4
+ ciclo de Hermes y los reenvía como observaciones al backend
5
+ (`POST /agentmemory/observe`). Todo es fail-open: un fallo de red o del
6
+ backend jamás interrumpe el turno (los callbacks devuelven None y el
7
+ transporte traga excepciones).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+
15
+ from .capture import observe
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def _project() -> tuple[str, str]:
21
+ """(project, cwd): basename del directorio de trabajo actual."""
22
+ cwd = os.getcwd()
23
+ return os.path.basename(cwd) or cwd, cwd
24
+
25
+
26
+ def _sid(kwargs: dict) -> str:
27
+ return str(kwargs.get("session_id") or kwargs.get("task_id") or "unknown")
28
+
29
+
30
+ def on_session_start(**kwargs) -> None:
31
+ project, cwd = _project()
32
+ observe(_sid(kwargs), "session_created",
33
+ {"profile": kwargs.get("profile_name"), "surface": kwargs.get("surface")},
34
+ project, cwd)
35
+
36
+
37
+ def on_session_end(**kwargs) -> None:
38
+ project, cwd = _project()
39
+ observe(_sid(kwargs), "session_end",
40
+ {"reason": kwargs.get("reason")}, project, cwd)
41
+
42
+
43
+ def pre_tool_call(tool_name: str = "", args: dict | None = None, **kwargs) -> None:
44
+ # IMPORTANTE: devolver None. Un dict con {"action": "block"} vetaría
45
+ # la herramienta; este plugin solo observa.
46
+ project, cwd = _project()
47
+ observe(_sid(kwargs), "pre_tool_call",
48
+ {"tool": tool_name, "args": args or {}}, project, cwd)
49
+
50
+
51
+ def post_tool_call(tool_name: str = "", args: dict | None = None, result=None,
52
+ status: str | None = None, duration_ms: int = 0, **kwargs) -> None:
53
+ project, cwd = _project()
54
+ ok = (status or "ok") == "ok"
55
+ observe(_sid(kwargs), "post_tool_use" if ok else "post_tool_failure",
56
+ {"tool": tool_name, "args": args or {}, "result": result,
57
+ "status": status or "ok", "duration_ms": duration_ms},
58
+ project, cwd)
59
+
60
+
61
+ def pre_llm_call(**kwargs) -> None:
62
+ project, cwd = _project()
63
+ observe(_sid(kwargs), "prompt_submit",
64
+ {"model": kwargs.get("model")}, project, cwd)
65
+
66
+
67
+ def post_llm_call(**kwargs) -> None:
68
+ project, cwd = _project()
69
+ observe(_sid(kwargs), "assistant_message",
70
+ {"model": kwargs.get("model"),
71
+ "stop_reason": kwargs.get("stop_reason")}, project, cwd)
72
+
73
+
74
+ def subagent_start(**kwargs) -> None:
75
+ project, cwd = _project()
76
+ observe(_sid(kwargs), "subagent_start", {"goal": kwargs.get("goal")}, project, cwd)
77
+
78
+
79
+ def subagent_stop(**kwargs) -> None:
80
+ project, cwd = _project()
81
+ observe(_sid(kwargs), "subagent_stop",
82
+ {"status": kwargs.get("status")}, project, cwd)
83
+
84
+
85
+ HOOKS = {
86
+ "on_session_start": on_session_start,
87
+ "on_session_end": on_session_end,
88
+ "pre_tool_call": pre_tool_call,
89
+ "post_tool_call": post_tool_call,
90
+ "pre_llm_call": pre_llm_call,
91
+ "post_llm_call": post_llm_call,
92
+ "subagent_start": subagent_start,
93
+ "subagent_stop": subagent_stop,
94
+ }
95
+
96
+
97
+ def register(ctx) -> None:
98
+ """Entry point del plugin — lo llama el loader de Hermes."""
99
+ for name, callback in HOOKS.items():
100
+ ctx.register_hook(name, callback)
101
+ logger.info("hermes-agentmemory-capture: %d hooks registrados", len(HOOKS))
102
+
103
+
104
+ __all__ = ["register", "HOOKS"]
@@ -0,0 +1,108 @@
1
+ """Transporte fail-open hacia el backend AgentMemory.
2
+
3
+ Replica el comportamiento de `post()` en
4
+ `agentmemory-capture.ts` de OpenCode: POST best-effort con timeout corto,
5
+ Bearer con AGENTMEMORY_SECRET, y resolución del secreto (env → ~/.agentmemory).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import urllib.request
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ TIMEOUT_S = 5
20
+ MAX_DATA_CHARS = 4000
21
+
22
+
23
+ def backend_url() -> str:
24
+ return (os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111").rstrip("/")
25
+
26
+
27
+ def _usable(value: object) -> str:
28
+ text = value.strip() if isinstance(value, str) else ""
29
+ if not text or (text.startswith("${") and text.endswith("}")):
30
+ return ""
31
+ return text
32
+
33
+
34
+ def _file_secret() -> str:
35
+ """Secreto guardado por el propio backend (~/.agentmemory/.env o secret)."""
36
+ home = Path.home() / ".agentmemory"
37
+ try:
38
+ for line in (home / ".env").read_text(encoding="utf-8").splitlines():
39
+ line = line.strip()
40
+ if not line or line.startswith("#") or "=" not in line:
41
+ continue
42
+ key, _, value = line.partition("=")
43
+ if key.removeprefix("export ").strip() != "AGENTMEMORY_SECRET":
44
+ continue
45
+ value = value.strip()
46
+ if value[:1] in ("'", '"'):
47
+ end = value.find(value[0], 1)
48
+ value = value[1:end] if end > 0 else value[1:]
49
+ found = _usable(value)
50
+ if found:
51
+ return found
52
+ except OSError:
53
+ pass
54
+ try:
55
+ return _usable((home / "secret").read_text(encoding="utf-8"))
56
+ except OSError:
57
+ return ""
58
+
59
+
60
+ def _is_loopback(url: str) -> bool:
61
+ return "localhost" in url or "127.0.0.1" in url or "::1" in url
62
+
63
+
64
+ def resolve_secret() -> str:
65
+ configured = _usable(os.environ.get("AGENTMEMORY_SECRET", ""))
66
+ if configured:
67
+ return configured
68
+ if _is_loopback(backend_url()):
69
+ return _file_secret()
70
+ return ""
71
+
72
+
73
+ def _trim(value: object) -> object:
74
+ if isinstance(value, str) and len(value) > MAX_DATA_CHARS:
75
+ return value[:MAX_DATA_CHARS] + "…[truncado]"
76
+ if isinstance(value, dict):
77
+ return {k: _trim(v) for k, v in value.items()}
78
+ if isinstance(value, (list, tuple)):
79
+ return [_trim(v) for v in value]
80
+ return value
81
+
82
+
83
+ def observe(session_id: str, hook_type: str, data: dict, project: str = "", cwd: str = "") -> bool:
84
+ """Envía una observación al backend. Nunca lanza: fail-open por diseño."""
85
+ try:
86
+ secret = resolve_secret()
87
+ body = json.dumps({
88
+ "hookType": hook_type,
89
+ "sessionId": session_id or "unknown",
90
+ "project": project,
91
+ "cwd": cwd,
92
+ "timestamp": datetime.now(timezone.utc).isoformat(),
93
+ "data": _trim(data),
94
+ }).encode("utf-8")
95
+ headers = {"Content-Type": "application/json"}
96
+ if secret:
97
+ headers["Authorization"] = f"Bearer {secret}"
98
+ request = urllib.request.Request(
99
+ f"{backend_url()}/agentmemory/observe",
100
+ data=body,
101
+ headers=headers,
102
+ method="POST",
103
+ )
104
+ with urllib.request.urlopen(request, timeout=TIMEOUT_S) as response:
105
+ return 200 <= response.status < 300
106
+ except Exception as exc: # noqa: BLE001 — un hook jamás debe romper el turno
107
+ logger.debug("agentmemory-capture: POST /observe falló: %s", exc)
108
+ return False
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-agentmemory-capture
3
+ Version: 0.1.0
4
+ Summary: Captura automática de sesiones y herramientas de Hermes hacia AgentMemory
5
+ Author: herjarsa
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # hermes-agentmemory-capture
13
+
14
+ Port del plugin `agentmemory-capture` de OpenCode a Hermes: captura
15
+ automática de sesiones y uso de herramientas hacia AgentMemory.
16
+
17
+ Estado: **funcional (HITO 2)** — 8 hooks activos, transporte verificado
18
+ contra backend real. Ver `SPEC.md` para el mapeo y gaps.
19
+
20
+ ## Requisitos
21
+
22
+ - Backend AgentMemory corriendo (este equipo: `http://localhost:3111` vía pm2).
23
+ - Env vars (sin hardcodear secretos):
24
+ - `AGENTMEMORY_URL` (ej. `http://localhost:3111`)
25
+ - `AGENTMEMORY_SECRET`
26
+
27
+ ## Instalación
28
+
29
+ ```bash
30
+ hermes plugins install herjarsa/hermes-agentmemory-capture --yes-deps --enable
31
+ hermes plugins list # debe salir enabled
32
+ ```
33
+
34
+ ### Vía pip (PyPI)
35
+
36
+ ```bash
37
+ pip install hermes-agentmemory-capture
38
+ ```
39
+
40
+ Descubierto automáticamente por Hermes vía entry point
41
+ `hermes_agent.plugins` (requiere reiniciar la sesión).
42
+
43
+ ## Verificación
44
+
45
+ Captura de prueba → comprobar con las tools MCP `memory_*` o el viewer
46
+ de AgentMemory que el turno quedó registrado.
@@ -0,0 +1,8 @@
1
+ hermes_agentmemory_capture/__init__.py,sha256=kcNeWw4CpkoG1akaxibspxtWB8i_V7Gk8FGxbEbO77c,3346
2
+ hermes_agentmemory_capture/capture.py,sha256=2yMMSbNK5NiysYCnhYVxDkzOoqfVQ5MmiTKygM9r8FM,3548
3
+ hermes_agentmemory_capture-0.1.0.dist-info/licenses/LICENSE,sha256=3aCX5s5aqXhK_is4S0O6OgDYYXECyUgRsy9XXhmQlXk,1065
4
+ hermes_agentmemory_capture-0.1.0.dist-info/METADATA,sha256=2cSVXalq0MHZ5Jm_uNjaONT4zb2cuG1i4u88Wn_4r6I,1278
5
+ hermes_agentmemory_capture-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ hermes_agentmemory_capture-0.1.0.dist-info/entry_points.txt,sha256=Ulu07p0d0NbuOohkHBPuIaWnlsPsphTDSoeYSYlox84,79
7
+ hermes_agentmemory_capture-0.1.0.dist-info/top_level.txt,sha256=7KvxGQ67xzuDsQLRerAPh5nGf9ow7VteiUEZ3HM0WwM,27
8
+ hermes_agentmemory_capture-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [hermes_agent.plugins]
2
+ hermes-agentmemory-capture = hermes_agentmemory_capture
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 herjarsa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ hermes_agentmemory_capture