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,495 @@
1
+ """Claude Code tasks adapter for Runtime Memory integration.
2
+
3
+ This module provides integration with Claude Code's native todos system,
4
+ enabling automatic outcome capture when tasks complete.
5
+
6
+ Example:
7
+ >>> from runtime_memory.tasks import ClaudeCodeAdapter
8
+ >>> adapter = ClaudeCodeAdapter(engine)
9
+ >>> await adapter.initialize()
10
+ >>>
11
+ >>> # List tasks
12
+ >>> tasks = adapter.list_tasks()
13
+ >>>
14
+ >>> # Get context for current task
15
+ >>> context = await adapter.get_unified_context()
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING
23
+
24
+ from runtime_memory.core.models import Memory
25
+ from runtime_memory.tasks.claude_code_parser import ClaudeCodeParser
26
+ from runtime_memory.tasks.linking import TaskMemoryLinker
27
+ from runtime_memory.tasks.models import (
28
+ CLAUDE_CODE_STATUS_TO_OUTCOME,
29
+ ClaudeCodeTask,
30
+ ClaudeCodeTaskStatus,
31
+ TaskContext,
32
+ TaskSource,
33
+ TaskSyncResult,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from runtime_memory.core.engine import MemoryEngine
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class ClaudeCodeAdapter:
43
+ """Adapter for Claude Code todos integration.
44
+
45
+ Provides similar interface to BeadsAdapter but for Claude Code's
46
+ native task system at ~/.claude/todos/.
47
+
48
+ Key differences from Beads:
49
+ - Simpler status model (pending, in_progress, completed)
50
+ - No dependencies/blocking
51
+ - Tasks organized by session/agent
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ engine: MemoryEngine,
57
+ todos_dir: Path | str | None = None,
58
+ task_list_id: str | None = None,
59
+ auto_outcome_enabled: bool = True,
60
+ ) -> None:
61
+ """Initialize the Claude Code adapter.
62
+
63
+ Args:
64
+ engine: The Runtime Memory engine.
65
+ todos_dir: Explicit path to todos directory (auto-discovers if None).
66
+ task_list_id: Filter to specific task list (session/agent).
67
+ auto_outcome_enabled: Whether to auto-record outcomes on task completion.
68
+ """
69
+ self._engine = engine
70
+ self._todos_dir = Path(todos_dir) if todos_dir else None
71
+ self._task_list_id = task_list_id
72
+
73
+ # Initialize components
74
+ self._parser = ClaudeCodeParser(self._todos_dir, task_list_id)
75
+ self._linker: TaskMemoryLinker | None = None
76
+
77
+ # Configuration
78
+ self.auto_outcome_enabled = auto_outcome_enabled
79
+
80
+ self._initialized = False
81
+
82
+ async def initialize(self) -> None:
83
+ """Initialize all components.
84
+
85
+ Creates the linker table and sets up components.
86
+ Must be called before using the adapter.
87
+ """
88
+ if self._initialized:
89
+ return
90
+
91
+ # Get db_path from engine's storage
92
+ db_path = self._engine._storage.db_path
93
+
94
+ # Initialize linker
95
+ self._linker = TaskMemoryLinker(db_path)
96
+ await self._linker.initialize()
97
+
98
+ self._initialized = True
99
+ logger.debug("ClaudeCodeAdapter initialized")
100
+
101
+ def _ensure_initialized(self) -> None:
102
+ """Ensure the adapter is initialized."""
103
+ if not self._initialized:
104
+ raise RuntimeError(
105
+ "ClaudeCodeAdapter not initialized. Call initialize() first."
106
+ )
107
+
108
+ @property
109
+ def is_available(self) -> bool:
110
+ """Check if Claude Code todos are available."""
111
+ return self._parser.is_available()
112
+
113
+ # =========================================================================
114
+ # Task Operations
115
+ # =========================================================================
116
+
117
+ def get_task(self, task_id: str) -> ClaudeCodeTask | None:
118
+ """Get a task by ID.
119
+
120
+ Args:
121
+ task_id: The task ID (e.g., "cc-abc12345-0").
122
+
123
+ Returns:
124
+ The task or None if not found.
125
+ """
126
+ return self._parser.get_task(task_id)
127
+
128
+ def list_tasks(
129
+ self,
130
+ status: ClaudeCodeTaskStatus | None = None,
131
+ session_id: str | None = None,
132
+ ) -> list[ClaudeCodeTask]:
133
+ """List all tasks, optionally filtered.
134
+
135
+ Args:
136
+ status: Optional status filter.
137
+ session_id: Optional session ID filter.
138
+
139
+ Returns:
140
+ List of matching tasks.
141
+ """
142
+ return self._parser.list_tasks(status=status, session_id=session_id)
143
+
144
+ def get_ready_tasks(self) -> list[ClaudeCodeTask]:
145
+ """Get tasks that are ready to work on.
146
+
147
+ Returns:
148
+ List of tasks with pending status.
149
+ """
150
+ return self._parser.get_ready_tasks()
151
+
152
+ def get_current_task(self) -> ClaudeCodeTask | None:
153
+ """Get the currently active task (in_progress).
154
+
155
+ Returns:
156
+ The in-progress task, or None if no task is active.
157
+ """
158
+ in_progress = self._parser.get_in_progress_tasks()
159
+ return in_progress[0] if in_progress else None
160
+
161
+ def get_sessions(self) -> list[str]:
162
+ """Get list of unique session IDs.
163
+
164
+ Returns:
165
+ List of session IDs that have tasks.
166
+ """
167
+ return self._parser.get_sessions()
168
+
169
+ # =========================================================================
170
+ # Memory Linking
171
+ # =========================================================================
172
+
173
+ async def link_memory_to_task(
174
+ self,
175
+ task_id: str,
176
+ memory_id: str,
177
+ context: str | None = None,
178
+ ) -> None:
179
+ """Link a memory to a task.
180
+
181
+ Args:
182
+ task_id: The task ID.
183
+ memory_id: The Runtime Memory memory ID.
184
+ context: Optional context about how memory was used.
185
+ """
186
+ self._ensure_initialized()
187
+ await self._linker.link(task_id, memory_id, context)
188
+
189
+ async def link_memories_to_task(
190
+ self,
191
+ task_id: str,
192
+ memory_ids: list[str],
193
+ context: str | None = None,
194
+ ) -> None:
195
+ """Link multiple memories to a task.
196
+
197
+ Args:
198
+ task_id: The task ID.
199
+ memory_ids: List of Runtime Memory memory IDs.
200
+ context: Optional context about how memories were used.
201
+ """
202
+ self._ensure_initialized()
203
+ await self._linker.link_many(task_id, memory_ids, context)
204
+
205
+ async def get_task_memories(self, task_id: str) -> list[Memory]:
206
+ """Get all memories linked to a task.
207
+
208
+ Args:
209
+ task_id: The task ID.
210
+
211
+ Returns:
212
+ List of Memory objects linked to the task.
213
+ """
214
+ self._ensure_initialized()
215
+ links = await self._linker.get_memories_for_task(task_id)
216
+ memories = []
217
+ for link in links:
218
+ try:
219
+ memory = await self._engine.get(link.memory_id)
220
+ memories.append(memory)
221
+ except Exception:
222
+ logger.debug(f"Memory {link.memory_id} not found, skipping")
223
+ return memories
224
+
225
+ async def auto_link_search_results(
226
+ self,
227
+ task_id: str,
228
+ memory_ids: list[str],
229
+ ) -> None:
230
+ """Automatically link search results to the current task.
231
+
232
+ Args:
233
+ task_id: The task ID.
234
+ memory_ids: Memory IDs from search results.
235
+ """
236
+ self._ensure_initialized()
237
+ await self._linker.link_many(task_id, memory_ids, context="search_result")
238
+
239
+ # =========================================================================
240
+ # Outcome Capture
241
+ # =========================================================================
242
+
243
+ async def on_task_completed(self, task_id: str) -> int:
244
+ """Handle a task being marked as completed.
245
+
246
+ Records "worked" outcome for all linked memories.
247
+
248
+ Args:
249
+ task_id: The task ID.
250
+
251
+ Returns:
252
+ Number of memories that had outcomes recorded.
253
+ """
254
+ self._ensure_initialized()
255
+
256
+ if not self.auto_outcome_enabled:
257
+ return 0
258
+
259
+ # Get unresolved links for this task
260
+ links = await self._linker.get_unresolved_links(task_id)
261
+ if not links:
262
+ return 0
263
+
264
+ # Record outcome for all linked memories
265
+ memory_ids = [link.memory_id for link in links]
266
+ from runtime_memory.core.models import Outcome
267
+
268
+ await self._engine.record_outcome(memory_ids, Outcome.WORKED)
269
+
270
+ # Mark links as resolved
271
+ await self._linker.record_task_outcome(task_id, "worked")
272
+
273
+ logger.info(f"Recorded 'worked' outcome for {len(links)} memories on task {task_id}")
274
+ return len(links)
275
+
276
+ async def sync(self) -> TaskSyncResult:
277
+ """Sync outcomes for all completed tasks.
278
+
279
+ Scans completed tasks and records outcomes for any that
280
+ have unresolved memory links.
281
+
282
+ Returns:
283
+ TaskSyncResult with sync statistics.
284
+ """
285
+ self._ensure_initialized()
286
+
287
+ result = TaskSyncResult(source=TaskSource.CLAUDE_CODE)
288
+ completed_tasks = self._parser.get_completed_tasks()
289
+ result.tasks_found = len(self._parser.list_tasks())
290
+
291
+ for task in completed_tasks:
292
+ try:
293
+ count = await self.on_task_completed(task.id)
294
+ if count > 0:
295
+ result.tasks_synced += 1
296
+ result.outcomes_recorded += count
297
+ except Exception as e:
298
+ result.errors.append(f"Failed to sync task {task.id}: {e}")
299
+
300
+ return result
301
+
302
+ # =========================================================================
303
+ # Context Generation
304
+ # =========================================================================
305
+
306
+ async def get_unified_context(
307
+ self,
308
+ task_id: str | None = None,
309
+ max_memories: int = 10,
310
+ ) -> TaskContext | None:
311
+ """Get unified context combining task info and relevant memories.
312
+
313
+ Args:
314
+ task_id: The task ID to get context for. If None, uses current task.
315
+ max_memories: Maximum number of memories to include.
316
+
317
+ Returns:
318
+ TaskContext object or None if no task found.
319
+ """
320
+ self._ensure_initialized()
321
+
322
+ # Get task
323
+ if task_id:
324
+ task = self.get_task(task_id)
325
+ else:
326
+ task = self.get_current_task()
327
+
328
+ if not task:
329
+ return None
330
+
331
+ # Get linked memories
332
+ memories = await self.get_task_memories(task.id)
333
+
334
+ # If no linked memories, search for relevant ones
335
+ if not memories and task.content:
336
+ search_query = task.content[:200]
337
+ results = await self._engine.search(
338
+ search_query,
339
+ limit=max_memories,
340
+ min_score=0.5,
341
+ track_usage=False,
342
+ )
343
+ memories = [r.memory for r in results if r.score >= 0.5]
344
+
345
+ # Limit memories
346
+ memories = memories[:max_memories]
347
+
348
+ # Create context
349
+ context = TaskContext(
350
+ task=task,
351
+ memories=memories,
352
+ source=TaskSource.CLAUDE_CODE,
353
+ )
354
+ context.formatted = context.to_markdown()
355
+
356
+ return context
357
+
358
+ async def get_context_for_injection(
359
+ self,
360
+ task_id: str | None = None,
361
+ max_memories: int = 10,
362
+ ) -> str:
363
+ """Get formatted context string for injection into prompts.
364
+
365
+ Args:
366
+ task_id: The task ID to get context for. If None, uses current task.
367
+ max_memories: Maximum number of memories to include.
368
+
369
+ Returns:
370
+ Formatted markdown string for context injection.
371
+ """
372
+ context = await self.get_unified_context(task_id, max_memories)
373
+ if not context:
374
+ return ""
375
+ return context.formatted
376
+
377
+ # =========================================================================
378
+ # Statistics
379
+ # =========================================================================
380
+
381
+ async def get_stats(self) -> dict:
382
+ """Get statistics about Claude Code integration.
383
+
384
+ Returns:
385
+ Dict with statistics.
386
+ """
387
+ self._ensure_initialized()
388
+
389
+ # Parser stats
390
+ parser_stats = self._parser.get_stats()
391
+
392
+ # Link stats
393
+ link_stats = await self._linker.get_stats()
394
+
395
+ return {
396
+ "claude_code_available": self.is_available,
397
+ "todos_dir": str(self._parser.todos_dir) if self._parser.todos_dir else None,
398
+ "tasks": parser_stats,
399
+ "links": link_stats,
400
+ "auto_outcome_enabled": self.auto_outcome_enabled,
401
+ }
402
+
403
+ def refresh(self) -> int:
404
+ """Refresh task cache from disk.
405
+
406
+ Returns:
407
+ Number of tasks loaded.
408
+ """
409
+ return self._parser.refresh()
410
+
411
+
412
+ class NullClaudeCodeAdapter:
413
+ """Null adapter for when Claude Code todos are not available.
414
+
415
+ Provides the same interface but does nothing, allowing graceful
416
+ degradation when Claude Code integration is not configured.
417
+ """
418
+
419
+ @property
420
+ def is_available(self) -> bool:
421
+ return False
422
+
423
+ async def initialize(self) -> None:
424
+ pass
425
+
426
+ def get_task(self, task_id: str) -> None:
427
+ return None
428
+
429
+ def list_tasks(
430
+ self,
431
+ status: ClaudeCodeTaskStatus | None = None,
432
+ session_id: str | None = None,
433
+ ) -> list:
434
+ return []
435
+
436
+ def get_ready_tasks(self) -> list:
437
+ return []
438
+
439
+ def get_current_task(self) -> None:
440
+ return None
441
+
442
+ def get_sessions(self) -> list:
443
+ return []
444
+
445
+ async def link_memory_to_task(self, *args, **kwargs) -> None:
446
+ pass
447
+
448
+ async def link_memories_to_task(self, *args, **kwargs) -> None:
449
+ pass
450
+
451
+ async def get_task_memories(self, task_id: str) -> list:
452
+ return []
453
+
454
+ async def on_task_completed(self, task_id: str) -> int:
455
+ return 0
456
+
457
+ async def sync(self) -> TaskSyncResult:
458
+ result = TaskSyncResult(source=TaskSource.CLAUDE_CODE)
459
+ result.warnings.append("Claude Code integration not available")
460
+ return result
461
+
462
+ async def get_unified_context(self, *args, **kwargs) -> None:
463
+ return None
464
+
465
+ async def get_context_for_injection(self, *args, **kwargs) -> str:
466
+ return ""
467
+
468
+ async def get_stats(self) -> dict:
469
+ return {"claude_code_available": False}
470
+
471
+ def refresh(self) -> int:
472
+ return 0
473
+
474
+
475
+ def create_claude_code_adapter(
476
+ engine: MemoryEngine,
477
+ todos_dir: Path | str | None = None,
478
+ **kwargs,
479
+ ) -> ClaudeCodeAdapter | NullClaudeCodeAdapter:
480
+ """Factory function to create the appropriate adapter.
481
+
482
+ Returns a NullClaudeCodeAdapter if Claude Code is not available.
483
+
484
+ Args:
485
+ engine: The Runtime Memory engine.
486
+ todos_dir: Explicit path to todos directory.
487
+ **kwargs: Additional arguments for ClaudeCodeAdapter.
488
+
489
+ Returns:
490
+ ClaudeCodeAdapter if available, NullClaudeCodeAdapter otherwise.
491
+ """
492
+ adapter = ClaudeCodeAdapter(engine, todos_dir, **kwargs)
493
+ if adapter.is_available:
494
+ return adapter
495
+ return NullClaudeCodeAdapter()