runtime-memory 3.0.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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
"""Lifecycle hooks for Claude Code integration.
|
|
2
|
+
|
|
3
|
+
.. deprecated:: 2.0.0
|
|
4
|
+
This module is deprecated in favor of the native Claude Code 2.1.1+
|
|
5
|
+
hook system defined in hooks/hooks.json. The native hook system:
|
|
6
|
+
- Doesn't require external scripts
|
|
7
|
+
- Is managed by Claude Code directly
|
|
8
|
+
- Supports environment variables like $CLAUDE_SESSION_ID
|
|
9
|
+
|
|
10
|
+
Migration guide:
|
|
11
|
+
- pre-session → hooks.json SessionStart
|
|
12
|
+
- post-session → hooks.json SessionEnd
|
|
13
|
+
- pre-compact → hooks.json PreCompact
|
|
14
|
+
|
|
15
|
+
Provides hooks that integrate with Claude Code's lifecycle:
|
|
16
|
+
- pre-session: Inject relevant context before starting
|
|
17
|
+
- post-session: Extract learnings after ending
|
|
18
|
+
- pre-compact: Extract learnings before context window compaction
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
25
|
+
# Emit deprecation warning when module is imported
|
|
26
|
+
warnings.warn(
|
|
27
|
+
"The hooks module is deprecated as of v2.0.0. "
|
|
28
|
+
"Use the native hook system in hooks/hooks.json instead.",
|
|
29
|
+
DeprecationWarning,
|
|
30
|
+
stacklevel=2,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import shutil
|
|
36
|
+
import stat
|
|
37
|
+
import sys
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from datetime import UTC, datetime
|
|
40
|
+
from enum import Enum
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import TYPE_CHECKING, Any
|
|
43
|
+
|
|
44
|
+
from runtime_memory.core.logging import get_logger
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from runtime_memory.core.engine import MemoryEngine
|
|
48
|
+
from runtime_memory.extraction.extractor import MemoryExtractor
|
|
49
|
+
|
|
50
|
+
logger = get_logger(__name__)
|
|
51
|
+
|
|
52
|
+
# Default paths
|
|
53
|
+
DEFAULT_HOOKS_DIR = Path.home() / ".claude" / "hooks"
|
|
54
|
+
DEFAULT_STATE_FILE = Path.home() / ".claude" / "runtime-memory-state.json"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HookType(str, Enum):
|
|
58
|
+
"""Types of lifecycle hooks."""
|
|
59
|
+
|
|
60
|
+
PRE_SESSION = "pre-session"
|
|
61
|
+
"""Hook that runs before a session starts."""
|
|
62
|
+
|
|
63
|
+
POST_SESSION = "post-session"
|
|
64
|
+
"""Hook that runs after a session ends."""
|
|
65
|
+
|
|
66
|
+
PRE_COMPACT = "pre-compact"
|
|
67
|
+
"""Hook that runs before context window compaction."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class HookConfig:
|
|
72
|
+
"""Configuration for hooks."""
|
|
73
|
+
|
|
74
|
+
# Hook installation
|
|
75
|
+
hooks_dir: Path = field(default_factory=lambda: DEFAULT_HOOKS_DIR)
|
|
76
|
+
"""Directory where hooks are installed."""
|
|
77
|
+
|
|
78
|
+
state_file: Path = field(default_factory=lambda: DEFAULT_STATE_FILE)
|
|
79
|
+
"""File for tracking hook state."""
|
|
80
|
+
|
|
81
|
+
# Pre-session settings
|
|
82
|
+
inject_context: bool = True
|
|
83
|
+
"""Whether to inject context in pre-session hook."""
|
|
84
|
+
|
|
85
|
+
max_context_memories: int = 15
|
|
86
|
+
"""Maximum memories to include in context."""
|
|
87
|
+
|
|
88
|
+
context_query: str | None = None
|
|
89
|
+
"""Optional query to filter context memories."""
|
|
90
|
+
|
|
91
|
+
# Post-session settings
|
|
92
|
+
extract_memories: bool = True
|
|
93
|
+
"""Whether to extract memories in post-session hook."""
|
|
94
|
+
|
|
95
|
+
# Pre-compact settings
|
|
96
|
+
save_before_compact: bool = True
|
|
97
|
+
"""Whether to extract memories before compaction."""
|
|
98
|
+
|
|
99
|
+
# Output settings
|
|
100
|
+
output_format: str = "markdown"
|
|
101
|
+
"""Output format for context: 'markdown', 'json', 'text'."""
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
"""Ensure paths are Path objects."""
|
|
105
|
+
if isinstance(self.hooks_dir, str):
|
|
106
|
+
self.hooks_dir = Path(self.hooks_dir)
|
|
107
|
+
if isinstance(self.state_file, str):
|
|
108
|
+
self.state_file = Path(self.state_file)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class HookState:
|
|
113
|
+
"""State tracking for hooks."""
|
|
114
|
+
|
|
115
|
+
last_session_id: str | None = None
|
|
116
|
+
"""ID of the last session."""
|
|
117
|
+
|
|
118
|
+
last_session_start: datetime | None = None
|
|
119
|
+
"""When the last session started."""
|
|
120
|
+
|
|
121
|
+
last_extraction: datetime | None = None
|
|
122
|
+
"""When memories were last extracted."""
|
|
123
|
+
|
|
124
|
+
last_context_injection: datetime | None = None
|
|
125
|
+
"""When context was last injected."""
|
|
126
|
+
|
|
127
|
+
memories_extracted_total: int = 0
|
|
128
|
+
"""Total memories extracted across all sessions."""
|
|
129
|
+
|
|
130
|
+
sessions_processed: int = 0
|
|
131
|
+
"""Number of sessions processed."""
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> dict[str, Any]:
|
|
134
|
+
"""Convert to dictionary."""
|
|
135
|
+
return {
|
|
136
|
+
"last_session_id": self.last_session_id,
|
|
137
|
+
"last_session_start": self.last_session_start.isoformat() if self.last_session_start else None,
|
|
138
|
+
"last_extraction": self.last_extraction.isoformat() if self.last_extraction else None,
|
|
139
|
+
"last_context_injection": self.last_context_injection.isoformat() if self.last_context_injection else None,
|
|
140
|
+
"memories_extracted_total": self.memories_extracted_total,
|
|
141
|
+
"sessions_processed": self.sessions_processed,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_dict(cls, data: dict[str, Any]) -> HookState:
|
|
146
|
+
"""Create from dictionary."""
|
|
147
|
+
return cls(
|
|
148
|
+
last_session_id=data.get("last_session_id"),
|
|
149
|
+
last_session_start=datetime.fromisoformat(data["last_session_start"]) if data.get("last_session_start") else None,
|
|
150
|
+
last_extraction=datetime.fromisoformat(data["last_extraction"]) if data.get("last_extraction") else None,
|
|
151
|
+
last_context_injection=datetime.fromisoformat(data["last_context_injection"]) if data.get("last_context_injection") else None,
|
|
152
|
+
memories_extracted_total=data.get("memories_extracted_total", 0),
|
|
153
|
+
sessions_processed=data.get("sessions_processed", 0),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class HookResult:
|
|
159
|
+
"""Result of hook execution."""
|
|
160
|
+
|
|
161
|
+
success: bool
|
|
162
|
+
"""Whether the hook executed successfully."""
|
|
163
|
+
|
|
164
|
+
hook_type: HookType
|
|
165
|
+
"""Type of hook that was executed."""
|
|
166
|
+
|
|
167
|
+
output: str
|
|
168
|
+
"""Output from the hook (e.g., context for pre-session)."""
|
|
169
|
+
|
|
170
|
+
memories_processed: int = 0
|
|
171
|
+
"""Number of memories processed."""
|
|
172
|
+
|
|
173
|
+
error: str | None = None
|
|
174
|
+
"""Error message if hook failed."""
|
|
175
|
+
|
|
176
|
+
duration_ms: float = 0.0
|
|
177
|
+
"""Execution duration in milliseconds."""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class HookError(Exception):
|
|
181
|
+
"""Base exception for hook errors."""
|
|
182
|
+
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class HookNotInstalledError(HookError):
|
|
187
|
+
"""Raised when hook is not installed."""
|
|
188
|
+
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class MemoryLayerHooks:
|
|
193
|
+
"""Manages lifecycle hooks for Claude Code integration."""
|
|
194
|
+
|
|
195
|
+
# Hook script templates
|
|
196
|
+
HOOK_SCRIPT_TEMPLATE = '''#!/usr/bin/env python3
|
|
197
|
+
"""Runtime Memory {hook_type} hook.
|
|
198
|
+
|
|
199
|
+
Auto-generated by runtime-memory. Do not edit directly.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
import sys
|
|
203
|
+
import json
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
# Read input from stdin
|
|
207
|
+
input_data = sys.stdin.read()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Import memory layer
|
|
211
|
+
from runtime_memory.claude_code.hooks import MemoryLayerHooks
|
|
212
|
+
|
|
213
|
+
# Execute hook
|
|
214
|
+
hooks = MemoryLayerHooks()
|
|
215
|
+
result = hooks.execute_{hook_func}(input_data)
|
|
216
|
+
|
|
217
|
+
# Output result
|
|
218
|
+
if result.output:
|
|
219
|
+
print(result.output)
|
|
220
|
+
|
|
221
|
+
sys.exit(0 if result.success else 1)
|
|
222
|
+
|
|
223
|
+
except ImportError as e:
|
|
224
|
+
print(f"Error: runtime-memory not installed: {{e}}", file=sys.stderr)
|
|
225
|
+
sys.exit(1)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
print(f"Error: {{e}}", file=sys.stderr)
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|
|
232
|
+
'''
|
|
233
|
+
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
config: HookConfig | None = None,
|
|
237
|
+
engine: MemoryEngine | None = None,
|
|
238
|
+
extractor: MemoryExtractor | None = None,
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Initialize hooks manager.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
config: Hook configuration.
|
|
244
|
+
engine: Memory engine instance.
|
|
245
|
+
extractor: Memory extractor instance.
|
|
246
|
+
"""
|
|
247
|
+
self.config = config or HookConfig()
|
|
248
|
+
self._engine = engine
|
|
249
|
+
self._extractor = extractor
|
|
250
|
+
self._state: HookState | None = None
|
|
251
|
+
|
|
252
|
+
# =========================================================================
|
|
253
|
+
# State Management
|
|
254
|
+
# =========================================================================
|
|
255
|
+
|
|
256
|
+
def _load_state(self) -> HookState:
|
|
257
|
+
"""Load state from file."""
|
|
258
|
+
if self._state is not None:
|
|
259
|
+
return self._state
|
|
260
|
+
|
|
261
|
+
if self.config.state_file.exists():
|
|
262
|
+
try:
|
|
263
|
+
data = json.loads(self.config.state_file.read_text())
|
|
264
|
+
self._state = HookState.from_dict(data)
|
|
265
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
266
|
+
logger.warning(f"Failed to load hook state: {e}")
|
|
267
|
+
self._state = HookState()
|
|
268
|
+
else:
|
|
269
|
+
self._state = HookState()
|
|
270
|
+
|
|
271
|
+
return self._state
|
|
272
|
+
|
|
273
|
+
def _save_state(self) -> None:
|
|
274
|
+
"""Save state to file."""
|
|
275
|
+
if self._state is None:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
self.config.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
self.config.state_file.write_text(
|
|
280
|
+
json.dumps(self._state.to_dict(), indent=2)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def state(self) -> HookState:
|
|
285
|
+
"""Get current state."""
|
|
286
|
+
return self._load_state()
|
|
287
|
+
|
|
288
|
+
# =========================================================================
|
|
289
|
+
# Hook Installation
|
|
290
|
+
# =========================================================================
|
|
291
|
+
|
|
292
|
+
def install_hooks(self, hook_types: list[HookType] | None = None) -> dict[HookType, Path]:
|
|
293
|
+
"""Install hooks to Claude Code hooks directory.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
hook_types: Types of hooks to install (default: all).
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Dictionary mapping hook types to installed paths.
|
|
300
|
+
"""
|
|
301
|
+
if hook_types is None:
|
|
302
|
+
hook_types = list(HookType)
|
|
303
|
+
|
|
304
|
+
# Ensure hooks directory exists
|
|
305
|
+
self.config.hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
|
|
307
|
+
installed: dict[HookType, Path] = {}
|
|
308
|
+
|
|
309
|
+
for hook_type in hook_types:
|
|
310
|
+
hook_path = self._install_hook(hook_type)
|
|
311
|
+
installed[hook_type] = hook_path
|
|
312
|
+
logger.info(f"Installed {hook_type.value} hook at {hook_path}")
|
|
313
|
+
|
|
314
|
+
return installed
|
|
315
|
+
|
|
316
|
+
def _install_hook(self, hook_type: HookType) -> Path:
|
|
317
|
+
"""Install a single hook.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
hook_type: Type of hook to install.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Path to installed hook.
|
|
324
|
+
"""
|
|
325
|
+
# Determine function name
|
|
326
|
+
hook_func_map = {
|
|
327
|
+
HookType.PRE_SESSION: "pre_session",
|
|
328
|
+
HookType.POST_SESSION: "post_session",
|
|
329
|
+
HookType.PRE_COMPACT: "pre_compact",
|
|
330
|
+
}
|
|
331
|
+
hook_func = hook_func_map[hook_type]
|
|
332
|
+
|
|
333
|
+
# Generate script
|
|
334
|
+
script = self.HOOK_SCRIPT_TEMPLATE.format(
|
|
335
|
+
hook_type=hook_type.value,
|
|
336
|
+
hook_func=hook_func,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Write hook file
|
|
340
|
+
hook_path = self.config.hooks_dir / f"runtime-memory-{hook_type.value}.py"
|
|
341
|
+
hook_path.write_text(script)
|
|
342
|
+
|
|
343
|
+
# Make executable
|
|
344
|
+
hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
345
|
+
|
|
346
|
+
return hook_path
|
|
347
|
+
|
|
348
|
+
def uninstall_hooks(self, hook_types: list[HookType] | None = None) -> list[HookType]:
|
|
349
|
+
"""Uninstall hooks.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
hook_types: Types of hooks to uninstall (default: all).
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List of uninstalled hook types.
|
|
356
|
+
"""
|
|
357
|
+
if hook_types is None:
|
|
358
|
+
hook_types = list(HookType)
|
|
359
|
+
|
|
360
|
+
uninstalled: list[HookType] = []
|
|
361
|
+
|
|
362
|
+
for hook_type in hook_types:
|
|
363
|
+
hook_path = self.config.hooks_dir / f"runtime-memory-{hook_type.value}.py"
|
|
364
|
+
if hook_path.exists():
|
|
365
|
+
hook_path.unlink()
|
|
366
|
+
uninstalled.append(hook_type)
|
|
367
|
+
logger.info(f"Uninstalled {hook_type.value} hook")
|
|
368
|
+
|
|
369
|
+
return uninstalled
|
|
370
|
+
|
|
371
|
+
def is_installed(self, hook_type: HookType) -> bool:
|
|
372
|
+
"""Check if a hook is installed.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
hook_type: Type of hook to check.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if hook is installed.
|
|
379
|
+
"""
|
|
380
|
+
hook_path = self.config.hooks_dir / f"runtime-memory-{hook_type.value}.py"
|
|
381
|
+
return hook_path.exists()
|
|
382
|
+
|
|
383
|
+
def get_installed_hooks(self) -> list[HookType]:
|
|
384
|
+
"""Get list of installed hooks.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
List of installed hook types.
|
|
388
|
+
"""
|
|
389
|
+
return [ht for ht in HookType if self.is_installed(ht)]
|
|
390
|
+
|
|
391
|
+
# =========================================================================
|
|
392
|
+
# Hook Execution
|
|
393
|
+
# =========================================================================
|
|
394
|
+
|
|
395
|
+
def execute_pre_session(self, input_data: str = "") -> HookResult:
|
|
396
|
+
"""Execute pre-session hook.
|
|
397
|
+
|
|
398
|
+
Injects relevant context before a session starts.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
input_data: Input data (e.g., session info JSON).
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
HookResult with context to inject.
|
|
405
|
+
"""
|
|
406
|
+
import time
|
|
407
|
+
start_time = time.time()
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
# Parse input data if provided
|
|
411
|
+
session_info: dict[str, Any] = {}
|
|
412
|
+
if input_data.strip():
|
|
413
|
+
try:
|
|
414
|
+
session_info = json.loads(input_data)
|
|
415
|
+
except json.JSONDecodeError:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# Get project from session info
|
|
419
|
+
project = session_info.get("project")
|
|
420
|
+
|
|
421
|
+
# Update state
|
|
422
|
+
state = self._load_state()
|
|
423
|
+
state.last_session_id = session_info.get("session_id")
|
|
424
|
+
state.last_session_start = datetime.now(UTC)
|
|
425
|
+
self._save_state()
|
|
426
|
+
|
|
427
|
+
# Get context if enabled
|
|
428
|
+
output = ""
|
|
429
|
+
memories_processed = 0
|
|
430
|
+
|
|
431
|
+
if self.config.inject_context and self._engine:
|
|
432
|
+
import asyncio
|
|
433
|
+
context = asyncio.run(
|
|
434
|
+
self._engine.get_context(
|
|
435
|
+
query=self.config.context_query,
|
|
436
|
+
project=project,
|
|
437
|
+
max_memories=self.config.max_context_memories,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
memories_processed = context.included_count
|
|
441
|
+
|
|
442
|
+
# Format output
|
|
443
|
+
if self.config.output_format == "markdown":
|
|
444
|
+
output = context.to_markdown()
|
|
445
|
+
elif self.config.output_format == "json":
|
|
446
|
+
output = json.dumps(context.to_dict(), indent=2)
|
|
447
|
+
else:
|
|
448
|
+
output = "\n".join(m.content for m in context.memories)
|
|
449
|
+
|
|
450
|
+
# Update state
|
|
451
|
+
state.last_context_injection = datetime.now(UTC)
|
|
452
|
+
self._save_state()
|
|
453
|
+
|
|
454
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
455
|
+
logger.info(f"Pre-session hook: injected {memories_processed} memories")
|
|
456
|
+
|
|
457
|
+
return HookResult(
|
|
458
|
+
success=True,
|
|
459
|
+
hook_type=HookType.PRE_SESSION,
|
|
460
|
+
output=output,
|
|
461
|
+
memories_processed=memories_processed,
|
|
462
|
+
duration_ms=duration_ms,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.error(f"Pre-session hook failed: {e}")
|
|
467
|
+
return HookResult(
|
|
468
|
+
success=False,
|
|
469
|
+
hook_type=HookType.PRE_SESSION,
|
|
470
|
+
output="",
|
|
471
|
+
error=str(e),
|
|
472
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def execute_post_session(self, input_data: str = "") -> HookResult:
|
|
476
|
+
"""Execute post-session hook.
|
|
477
|
+
|
|
478
|
+
Extracts learnings after a session ends.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
input_data: Session transcript or info JSON.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
HookResult with extraction summary.
|
|
485
|
+
"""
|
|
486
|
+
import time
|
|
487
|
+
start_time = time.time()
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
# Parse input - could be JSON or plain transcript
|
|
491
|
+
transcript = input_data
|
|
492
|
+
project: str | None = None
|
|
493
|
+
|
|
494
|
+
if input_data.strip().startswith("{"):
|
|
495
|
+
try:
|
|
496
|
+
session_info = json.loads(input_data)
|
|
497
|
+
transcript = session_info.get("transcript", "")
|
|
498
|
+
project = session_info.get("project")
|
|
499
|
+
except json.JSONDecodeError:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
# Extract memories if enabled
|
|
503
|
+
memories_processed = 0
|
|
504
|
+
output = ""
|
|
505
|
+
|
|
506
|
+
if self.config.extract_memories and self._extractor and self._engine:
|
|
507
|
+
import asyncio
|
|
508
|
+
result = asyncio.run(
|
|
509
|
+
self._extractor.extract_and_store(
|
|
510
|
+
transcript=transcript,
|
|
511
|
+
engine=self._engine,
|
|
512
|
+
project=project,
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if result.success:
|
|
517
|
+
memories_processed = result.memory_count
|
|
518
|
+
output = f"Extracted {memories_processed} memories:\n"
|
|
519
|
+
for memory in result.memories:
|
|
520
|
+
output += f"- [{memory.category.value}] {memory.content[:100]}...\n"
|
|
521
|
+
|
|
522
|
+
# Update state
|
|
523
|
+
state = self._load_state()
|
|
524
|
+
state.last_extraction = datetime.now(UTC)
|
|
525
|
+
state.memories_extracted_total += memories_processed
|
|
526
|
+
state.sessions_processed += 1
|
|
527
|
+
self._save_state()
|
|
528
|
+
else:
|
|
529
|
+
output = f"Extraction failed: {result.error}"
|
|
530
|
+
|
|
531
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
532
|
+
logger.info(f"Post-session hook: extracted {memories_processed} memories")
|
|
533
|
+
|
|
534
|
+
return HookResult(
|
|
535
|
+
success=True,
|
|
536
|
+
hook_type=HookType.POST_SESSION,
|
|
537
|
+
output=output,
|
|
538
|
+
memories_processed=memories_processed,
|
|
539
|
+
duration_ms=duration_ms,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
except Exception as e:
|
|
543
|
+
logger.error(f"Post-session hook failed: {e}")
|
|
544
|
+
return HookResult(
|
|
545
|
+
success=False,
|
|
546
|
+
hook_type=HookType.POST_SESSION,
|
|
547
|
+
output="",
|
|
548
|
+
error=str(e),
|
|
549
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def execute_pre_compact(self, input_data: str = "") -> HookResult:
|
|
553
|
+
"""Execute pre-compact hook.
|
|
554
|
+
|
|
555
|
+
Saves context before context window compaction.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
input_data: Current context/transcript to save.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
HookResult with save summary.
|
|
562
|
+
"""
|
|
563
|
+
import time
|
|
564
|
+
start_time = time.time()
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Parse input
|
|
568
|
+
transcript = input_data
|
|
569
|
+
project: str | None = None
|
|
570
|
+
|
|
571
|
+
if input_data.strip().startswith("{"):
|
|
572
|
+
try:
|
|
573
|
+
compact_info = json.loads(input_data)
|
|
574
|
+
transcript = compact_info.get("context", compact_info.get("transcript", ""))
|
|
575
|
+
project = compact_info.get("project")
|
|
576
|
+
except json.JSONDecodeError:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
# Extract memories before compact if enabled
|
|
580
|
+
memories_processed = 0
|
|
581
|
+
output = ""
|
|
582
|
+
|
|
583
|
+
if self.config.save_before_compact and self._extractor and self._engine:
|
|
584
|
+
import asyncio
|
|
585
|
+
result = asyncio.run(
|
|
586
|
+
self._extractor.extract_and_store(
|
|
587
|
+
transcript=transcript,
|
|
588
|
+
engine=self._engine,
|
|
589
|
+
project=project,
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if result.success:
|
|
594
|
+
memories_processed = result.memory_count
|
|
595
|
+
output = f"Saved {memories_processed} memories before compaction"
|
|
596
|
+
|
|
597
|
+
# Update state
|
|
598
|
+
state = self._load_state()
|
|
599
|
+
state.last_extraction = datetime.now(UTC)
|
|
600
|
+
state.memories_extracted_total += memories_processed
|
|
601
|
+
self._save_state()
|
|
602
|
+
else:
|
|
603
|
+
output = f"Pre-compact extraction failed: {result.error}"
|
|
604
|
+
|
|
605
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
606
|
+
logger.info(f"Pre-compact hook: saved {memories_processed} memories")
|
|
607
|
+
|
|
608
|
+
return HookResult(
|
|
609
|
+
success=True,
|
|
610
|
+
hook_type=HookType.PRE_COMPACT,
|
|
611
|
+
output=output,
|
|
612
|
+
memories_processed=memories_processed,
|
|
613
|
+
duration_ms=duration_ms,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
except Exception as e:
|
|
617
|
+
logger.error(f"Pre-compact hook failed: {e}")
|
|
618
|
+
return HookResult(
|
|
619
|
+
success=False,
|
|
620
|
+
hook_type=HookType.PRE_COMPACT,
|
|
621
|
+
output="",
|
|
622
|
+
error=str(e),
|
|
623
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# =========================================================================
|
|
627
|
+
# Configuration Management
|
|
628
|
+
# =========================================================================
|
|
629
|
+
|
|
630
|
+
def get_hook_config_json(self) -> str:
|
|
631
|
+
"""Get hook configuration as JSON for Claude Code settings.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
JSON configuration string.
|
|
635
|
+
"""
|
|
636
|
+
config = {
|
|
637
|
+
"hooks": {
|
|
638
|
+
"pre-session": {
|
|
639
|
+
"command": f"python3 {self.config.hooks_dir / 'runtime-memory-pre-session.py'}",
|
|
640
|
+
"timeout": 10000,
|
|
641
|
+
"enabled": self.is_installed(HookType.PRE_SESSION),
|
|
642
|
+
},
|
|
643
|
+
"post-session": {
|
|
644
|
+
"command": f"python3 {self.config.hooks_dir / 'runtime-memory-post-session.py'}",
|
|
645
|
+
"timeout": 30000,
|
|
646
|
+
"enabled": self.is_installed(HookType.POST_SESSION),
|
|
647
|
+
},
|
|
648
|
+
"pre-compact": {
|
|
649
|
+
"command": f"python3 {self.config.hooks_dir / 'runtime-memory-pre-compact.py'}",
|
|
650
|
+
"timeout": 30000,
|
|
651
|
+
"enabled": self.is_installed(HookType.PRE_COMPACT),
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return json.dumps(config, indent=2)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# =============================================================================
|
|
659
|
+
# Convenience Functions
|
|
660
|
+
# =============================================================================
|
|
661
|
+
|
|
662
|
+
def install_all_hooks(
|
|
663
|
+
hooks_dir: Path | None = None,
|
|
664
|
+
engine: MemoryEngine | None = None,
|
|
665
|
+
extractor: MemoryExtractor | None = None,
|
|
666
|
+
) -> dict[HookType, Path]:
|
|
667
|
+
"""Install all hooks.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
hooks_dir: Directory to install hooks.
|
|
671
|
+
engine: Memory engine instance.
|
|
672
|
+
extractor: Memory extractor instance.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
Dictionary of installed hooks.
|
|
676
|
+
"""
|
|
677
|
+
config = HookConfig()
|
|
678
|
+
if hooks_dir:
|
|
679
|
+
config.hooks_dir = hooks_dir
|
|
680
|
+
|
|
681
|
+
hooks = MemoryLayerHooks(config=config, engine=engine, extractor=extractor)
|
|
682
|
+
return hooks.install_hooks()
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def uninstall_all_hooks(hooks_dir: Path | None = None) -> list[HookType]:
|
|
686
|
+
"""Uninstall all hooks.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
hooks_dir: Directory where hooks are installed.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
List of uninstalled hook types.
|
|
693
|
+
"""
|
|
694
|
+
config = HookConfig()
|
|
695
|
+
if hooks_dir:
|
|
696
|
+
config.hooks_dir = hooks_dir
|
|
697
|
+
|
|
698
|
+
hooks = MemoryLayerHooks(config=config)
|
|
699
|
+
return hooks.uninstall_hooks()
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def get_hook_status(hooks_dir: Path | None = None) -> dict[str, Any]:
|
|
703
|
+
"""Get status of all hooks.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
hooks_dir: Directory where hooks are installed.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
Status dictionary.
|
|
710
|
+
"""
|
|
711
|
+
config = HookConfig()
|
|
712
|
+
if hooks_dir:
|
|
713
|
+
config.hooks_dir = hooks_dir
|
|
714
|
+
|
|
715
|
+
hooks = MemoryLayerHooks(config=config)
|
|
716
|
+
state = hooks.state
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
"installed_hooks": [ht.value for ht in hooks.get_installed_hooks()],
|
|
720
|
+
"hooks_dir": str(config.hooks_dir),
|
|
721
|
+
"state": state.to_dict(),
|
|
722
|
+
}
|