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.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. 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
+ }