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,339 @@
1
+ """Claude Code todos parser for reading task data from ~/.claude/todos/.
2
+
3
+ This module handles:
4
+ - Discovery of ~/.claude/todos/ directory
5
+ - Parsing JSON array task files
6
+ - Building task index with session/agent relationships
7
+ - Graceful handling of malformed data
8
+ - Support for CLAUDE_CODE_TASK_LIST_ID environment variable
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ from runtime_memory.tasks.models import ClaudeCodeTask, ClaudeCodeTaskStatus
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Iterator
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ClaudeCodeDirectoryNotFoundError(Exception):
28
+ """Raised when ~/.claude/todos/ directory cannot be found."""
29
+
30
+ pass
31
+
32
+
33
+ class ClaudeCodeParser:
34
+ """Parses Claude Code task data from ~/.claude/todos/ directory.
35
+
36
+ Supports:
37
+ - JSON array files (each file is a session's task list)
38
+ - CLAUDE_CODE_TASK_LIST_ID environment variable for filtering
39
+ - Caching of parsed tasks
40
+
41
+ File naming convention:
42
+ {session-uuid}-agent-{agent-uuid}.json
43
+
44
+ Example:
45
+ >>> parser = ClaudeCodeParser()
46
+ >>> tasks = parser.list_tasks()
47
+ >>> ready = parser.get_ready_tasks()
48
+ """
49
+
50
+ # Default location for Claude Code todos
51
+ DEFAULT_TODOS_DIR = Path.home() / ".claude" / "todos"
52
+
53
+ def __init__(
54
+ self,
55
+ todos_dir: Path | str | None = None,
56
+ task_list_id: str | None = None,
57
+ ):
58
+ """Initialize the parser.
59
+
60
+ Args:
61
+ todos_dir: Explicit path to todos directory.
62
+ If None, uses ~/.claude/todos/.
63
+ task_list_id: Filter to specific task list ID (session/agent).
64
+ If None, checks CLAUDE_CODE_TASK_LIST_ID env var.
65
+ """
66
+ self._todos_dir: Path | None = None
67
+ self._explicit_dir = Path(todos_dir) if todos_dir else None
68
+ self._task_list_id = task_list_id or os.environ.get("CLAUDE_CODE_TASK_LIST_ID")
69
+ self._task_cache: dict[str, ClaudeCodeTask] = {}
70
+ self._cache_valid = False
71
+
72
+ @property
73
+ def todos_dir(self) -> Path | None:
74
+ """Get the todos directory path, discovering if needed."""
75
+ if self._todos_dir is None:
76
+ self._todos_dir = self._discover_todos_dir()
77
+ return self._todos_dir
78
+
79
+ def _discover_todos_dir(self) -> Path | None:
80
+ """Discover the todos directory.
81
+
82
+ Search order:
83
+ 1. Explicit path provided to constructor
84
+ 2. $CLAUDE_CODE_TODOS_DIR environment variable
85
+ 3. Default ~/.claude/todos/
86
+
87
+ Returns:
88
+ Path to todos directory or None if not found.
89
+ """
90
+ # 1. Explicit path
91
+ if self._explicit_dir:
92
+ if self._explicit_dir.is_dir():
93
+ logger.debug(f"Using explicit todos dir: {self._explicit_dir}")
94
+ return self._explicit_dir
95
+ logger.warning(f"Explicit todos dir not found: {self._explicit_dir}")
96
+ return None
97
+
98
+ # 2. Environment variable
99
+ env_dir = os.environ.get("CLAUDE_CODE_TODOS_DIR")
100
+ if env_dir:
101
+ env_path = Path(env_dir)
102
+ if env_path.is_dir():
103
+ logger.debug(f"Using $CLAUDE_CODE_TODOS_DIR: {env_path}")
104
+ return env_path
105
+ logger.warning(f"$CLAUDE_CODE_TODOS_DIR not found: {env_path}")
106
+
107
+ # 3. Default location
108
+ default_path = self.DEFAULT_TODOS_DIR
109
+ if default_path.is_dir():
110
+ logger.debug(f"Using default todos dir: {default_path}")
111
+ return default_path
112
+
113
+ logger.debug("No Claude Code todos directory found")
114
+ return None
115
+
116
+ def is_available(self) -> bool:
117
+ """Check if Claude Code todos are available (directory exists)."""
118
+ return self.todos_dir is not None
119
+
120
+ def invalidate_cache(self) -> None:
121
+ """Invalidate the task cache, forcing a re-parse on next access."""
122
+ self._task_cache.clear()
123
+ self._cache_valid = False
124
+
125
+ def _ensure_cache(self) -> None:
126
+ """Ensure the task cache is populated."""
127
+ if self._cache_valid:
128
+ return
129
+
130
+ self._task_cache.clear()
131
+
132
+ if not self.todos_dir:
133
+ return
134
+
135
+ # Parse all JSON files in todos dir
136
+ for task in self._parse_all_files():
137
+ self._task_cache[task.id] = task
138
+
139
+ self._cache_valid = True
140
+ logger.debug(f"Cached {len(self._task_cache)} Claude Code tasks")
141
+
142
+ def _parse_all_files(self) -> Iterator[ClaudeCodeTask]:
143
+ """Parse all JSON files in the todos directory.
144
+
145
+ Yields:
146
+ ClaudeCodeTask objects for each valid task entry.
147
+ """
148
+ if not self.todos_dir:
149
+ return
150
+
151
+ # Find all .json files
152
+ json_files = list(self.todos_dir.glob("*.json"))
153
+ logger.debug(f"Found {len(json_files)} JSON files in todos dir")
154
+
155
+ for json_file in json_files:
156
+ # Filter by task list ID if specified
157
+ if self._task_list_id and self._task_list_id not in json_file.name:
158
+ continue
159
+
160
+ yield from self._parse_json_file(json_file)
161
+
162
+ def _parse_json_file(self, file_path: Path) -> Iterator[ClaudeCodeTask]:
163
+ """Parse a single JSON file containing tasks.
164
+
165
+ Args:
166
+ file_path: Path to the JSON file.
167
+
168
+ Yields:
169
+ ClaudeCodeTask objects for each valid task.
170
+ """
171
+ try:
172
+ # Extract session and agent IDs from filename
173
+ # Format: {session-uuid}-agent-{agent-uuid}.json
174
+ filename = file_path.stem # Remove .json
175
+ session_id = ""
176
+ agent_id = ""
177
+
178
+ if "-agent-" in filename:
179
+ parts = filename.split("-agent-")
180
+ session_id = parts[0]
181
+ agent_id = parts[1] if len(parts) > 1 else ""
182
+ else:
183
+ session_id = filename
184
+
185
+ with open(file_path, encoding="utf-8") as f:
186
+ data = json.load(f)
187
+
188
+ # Handle empty files or empty arrays
189
+ if not data:
190
+ return
191
+
192
+ # File should be a JSON array
193
+ if not isinstance(data, list):
194
+ logger.warning(f"Expected JSON array in {file_path}, got {type(data)}")
195
+ return
196
+
197
+ for index, task_data in enumerate(data):
198
+ try:
199
+ if not isinstance(task_data, dict):
200
+ logger.warning(
201
+ f"Expected dict at index {index} in {file_path}"
202
+ )
203
+ continue
204
+
205
+ task = ClaudeCodeTask.from_dict(
206
+ task_data,
207
+ session_id=session_id,
208
+ agent_id=agent_id,
209
+ index=index,
210
+ file_path=str(file_path),
211
+ )
212
+ yield task
213
+
214
+ except (KeyError, ValueError) as e:
215
+ logger.warning(
216
+ f"Invalid task data at {file_path}[{index}]: {e}"
217
+ )
218
+
219
+ except json.JSONDecodeError as e:
220
+ logger.warning(f"Malformed JSON in {file_path}: {e}")
221
+ except OSError as e:
222
+ logger.error(f"Failed to read {file_path}: {e}")
223
+
224
+ def get_task(self, task_id: str) -> ClaudeCodeTask | None:
225
+ """Get a task by ID.
226
+
227
+ Args:
228
+ task_id: The task ID (e.g., "cc-abc12345-0").
229
+
230
+ Returns:
231
+ The task or None if not found.
232
+ """
233
+ self._ensure_cache()
234
+ return self._task_cache.get(task_id)
235
+
236
+ def list_tasks(
237
+ self,
238
+ status: ClaudeCodeTaskStatus | None = None,
239
+ session_id: str | None = None,
240
+ ) -> list[ClaudeCodeTask]:
241
+ """List all tasks, optionally filtered.
242
+
243
+ Args:
244
+ status: Filter by status (e.g., COMPLETED).
245
+ session_id: Filter by session ID.
246
+
247
+ Returns:
248
+ List of matching tasks.
249
+ """
250
+ self._ensure_cache()
251
+
252
+ tasks = list(self._task_cache.values())
253
+
254
+ if status is not None:
255
+ tasks = [t for t in tasks if t.status == status]
256
+
257
+ if session_id is not None:
258
+ tasks = [t for t in tasks if t.session_id == session_id]
259
+
260
+ return tasks
261
+
262
+ def get_ready_tasks(self) -> list[ClaudeCodeTask]:
263
+ """Get tasks that are ready to work on (pending status).
264
+
265
+ Returns:
266
+ List of ready tasks.
267
+ """
268
+ return self.list_tasks(status=ClaudeCodeTaskStatus.PENDING)
269
+
270
+ def get_in_progress_tasks(self) -> list[ClaudeCodeTask]:
271
+ """Get tasks currently being worked on.
272
+
273
+ Returns:
274
+ List of in-progress tasks.
275
+ """
276
+ return self.list_tasks(status=ClaudeCodeTaskStatus.IN_PROGRESS)
277
+
278
+ def get_completed_tasks(self) -> list[ClaudeCodeTask]:
279
+ """Get tasks that have been completed.
280
+
281
+ Returns:
282
+ List of completed tasks.
283
+ """
284
+ return self.list_tasks(status=ClaudeCodeTaskStatus.COMPLETED)
285
+
286
+ def get_tasks_by_session(self, session_id: str) -> list[ClaudeCodeTask]:
287
+ """Get all tasks for a specific session.
288
+
289
+ Args:
290
+ session_id: The Claude session ID.
291
+
292
+ Returns:
293
+ List of tasks for that session.
294
+ """
295
+ return self.list_tasks(session_id=session_id)
296
+
297
+ def get_sessions(self) -> list[str]:
298
+ """Get list of unique session IDs.
299
+
300
+ Returns:
301
+ List of session IDs that have tasks.
302
+ """
303
+ self._ensure_cache()
304
+ sessions = set()
305
+ for task in self._task_cache.values():
306
+ if task.session_id:
307
+ sessions.add(task.session_id)
308
+ return sorted(sessions)
309
+
310
+ def refresh(self) -> int:
311
+ """Refresh the task cache from disk.
312
+
313
+ Returns:
314
+ Number of tasks loaded.
315
+ """
316
+ self.invalidate_cache()
317
+ self._ensure_cache()
318
+ return len(self._task_cache)
319
+
320
+ def get_stats(self) -> dict:
321
+ """Get statistics about Claude Code tasks.
322
+
323
+ Returns:
324
+ Dict with task statistics.
325
+ """
326
+ self._ensure_cache()
327
+
328
+ tasks = list(self._task_cache.values())
329
+ by_status = {}
330
+ for task in tasks:
331
+ status = task.status.value
332
+ by_status[status] = by_status.get(status, 0) + 1
333
+
334
+ return {
335
+ "total_tasks": len(tasks),
336
+ "by_status": by_status,
337
+ "sessions": len(self.get_sessions()),
338
+ "todos_dir": str(self.todos_dir) if self.todos_dir else None,
339
+ }