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,415 @@
1
+ """Beads CLI bridge for interacting with the bd command.
2
+
3
+ This module provides a wrapper around the Beads CLI (`bd` command),
4
+ offering an alternative to direct file parsing. Useful when:
5
+ - You want real-time task status (CLI may have fresher data)
6
+ - You need to leverage bd's dependency resolution
7
+ - File parsing is insufficient for complex operations
8
+
9
+ Falls back to file parsing if bd command is unavailable.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import json
16
+ import logging
17
+ import shutil
18
+ from dataclasses import dataclass
19
+ from datetime import UTC, datetime
20
+ from typing import TYPE_CHECKING
21
+
22
+ from runtime_memory.tasks.models import BeadsTask, BeadsTaskStatus
23
+
24
+ if TYPE_CHECKING:
25
+ pass
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Cache duration for CLI results (seconds)
30
+ CLI_CACHE_DURATION = 30
31
+
32
+ # Command timeout (seconds)
33
+ CLI_TIMEOUT = 5
34
+
35
+
36
+ @dataclass
37
+ class CLIResult:
38
+ """Result from a CLI command execution."""
39
+
40
+ success: bool
41
+ stdout: str
42
+ stderr: str
43
+ return_code: int
44
+
45
+
46
+ class BeadsCLI:
47
+ """Wrapper for the Beads CLI (bd command).
48
+
49
+ Provides methods to interact with Beads via its command-line interface.
50
+ Results are cached for performance.
51
+
52
+ Example:
53
+ >>> cli = BeadsCLI()
54
+ >>> if cli.is_available():
55
+ ... tasks = await cli.ready()
56
+ ... for task in tasks:
57
+ ... print(task.title)
58
+ """
59
+
60
+ def __init__(self, timeout: float = CLI_TIMEOUT) -> None:
61
+ """Initialize the CLI bridge.
62
+
63
+ Args:
64
+ timeout: Command timeout in seconds.
65
+ """
66
+ self._timeout = timeout
67
+ self._bd_path: str | None = None
68
+ self._available: bool | None = None
69
+ self._version: str | None = None
70
+
71
+ # Cache
72
+ self._cache: dict[str, tuple[datetime, any]] = {}
73
+ self._cache_duration = CLI_CACHE_DURATION
74
+
75
+ def is_available(self) -> bool:
76
+ """Check if the bd command is available.
77
+
78
+ Returns:
79
+ True if bd command exists and is executable.
80
+ """
81
+ if self._available is None:
82
+ self._bd_path = shutil.which("bd")
83
+ self._available = self._bd_path is not None
84
+ if self._available:
85
+ logger.debug(f"Found bd at: {self._bd_path}")
86
+ else:
87
+ logger.debug("bd command not found in PATH")
88
+ return self._available
89
+
90
+ async def get_version(self) -> str | None:
91
+ """Get the Beads version.
92
+
93
+ Returns:
94
+ Version string or None if unavailable.
95
+ """
96
+ if self._version is not None:
97
+ return self._version
98
+
99
+ if not self.is_available():
100
+ return None
101
+
102
+ result = await self._run_command(["bd", "--version"])
103
+ if result.success:
104
+ self._version = result.stdout.strip()
105
+ return self._version
106
+ return None
107
+
108
+ async def ready(self) -> list[BeadsTask]:
109
+ """Get tasks that are ready to work on.
110
+
111
+ Calls `bd ready --json` to get unblocked tasks.
112
+
113
+ Returns:
114
+ List of ready BeadsTask objects.
115
+ """
116
+ return await self._cached_command("ready", ["bd", "ready", "--json"])
117
+
118
+ async def show(self, task_id: str) -> BeadsTask | None:
119
+ """Get details for a specific task.
120
+
121
+ Calls `bd show <id> --json`.
122
+
123
+ Args:
124
+ task_id: The task ID to look up.
125
+
126
+ Returns:
127
+ BeadsTask or None if not found.
128
+ """
129
+ cache_key = f"show:{task_id}"
130
+ cached = self._get_cached(cache_key)
131
+ if cached is not None:
132
+ return cached
133
+
134
+ if not self.is_available():
135
+ return None
136
+
137
+ result = await self._run_command(["bd", "show", task_id, "--json"])
138
+ if not result.success:
139
+ return None
140
+
141
+ try:
142
+ data = json.loads(result.stdout)
143
+ task = self._parse_task(data)
144
+ self._set_cached(cache_key, task)
145
+ return task
146
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
147
+ logger.warning(f"Failed to parse bd show output: {e}")
148
+ return None
149
+
150
+ async def list_all(self) -> list[BeadsTask]:
151
+ """Get all tasks.
152
+
153
+ Calls `bd export --json` to get complete task list.
154
+
155
+ Returns:
156
+ List of all BeadsTask objects.
157
+ """
158
+ return await self._cached_command("list_all", ["bd", "export", "--json"])
159
+
160
+ async def get_current(self) -> BeadsTask | None:
161
+ """Get the currently active task.
162
+
163
+ Attempts to detect the current task from:
164
+ 1. Tasks with in_progress status
165
+ 2. Git branch naming convention (if applicable)
166
+
167
+ Returns:
168
+ Current task or None.
169
+ """
170
+ if not self.is_available():
171
+ return None
172
+
173
+ # Get all in-progress tasks
174
+ all_tasks = await self.list_all()
175
+ in_progress = [
176
+ t for t in all_tasks if t.status == BeadsTaskStatus.IN_PROGRESS
177
+ ]
178
+
179
+ if in_progress:
180
+ return in_progress[0]
181
+
182
+ return None
183
+
184
+ async def _cached_command(
185
+ self,
186
+ cache_key: str,
187
+ command: list[str],
188
+ ) -> list[BeadsTask]:
189
+ """Run a command with caching.
190
+
191
+ Args:
192
+ cache_key: Key for cache lookup.
193
+ command: Command to execute.
194
+
195
+ Returns:
196
+ List of parsed tasks.
197
+ """
198
+ cached = self._get_cached(cache_key)
199
+ if cached is not None:
200
+ return cached
201
+
202
+ if not self.is_available():
203
+ return []
204
+
205
+ result = await self._run_command(command)
206
+ if not result.success:
207
+ logger.warning(f"Command failed: {' '.join(command)}")
208
+ return []
209
+
210
+ tasks = self._parse_tasks_output(result.stdout)
211
+ self._set_cached(cache_key, tasks)
212
+ return tasks
213
+
214
+ async def _run_command(self, command: list[str]) -> CLIResult:
215
+ """Run a shell command asynchronously.
216
+
217
+ Args:
218
+ command: Command and arguments to execute.
219
+
220
+ Returns:
221
+ CLIResult with output and status.
222
+ """
223
+ try:
224
+ process = await asyncio.create_subprocess_exec(
225
+ *command,
226
+ stdout=asyncio.subprocess.PIPE,
227
+ stderr=asyncio.subprocess.PIPE,
228
+ )
229
+
230
+ try:
231
+ stdout, stderr = await asyncio.wait_for(
232
+ process.communicate(),
233
+ timeout=self._timeout,
234
+ )
235
+ except asyncio.TimeoutError:
236
+ process.kill()
237
+ await process.wait()
238
+ return CLIResult(
239
+ success=False,
240
+ stdout="",
241
+ stderr=f"Command timed out after {self._timeout}s",
242
+ return_code=-1,
243
+ )
244
+
245
+ return CLIResult(
246
+ success=process.returncode == 0,
247
+ stdout=stdout.decode("utf-8", errors="replace"),
248
+ stderr=stderr.decode("utf-8", errors="replace"),
249
+ return_code=process.returncode or 0,
250
+ )
251
+
252
+ except FileNotFoundError:
253
+ return CLIResult(
254
+ success=False,
255
+ stdout="",
256
+ stderr="Command not found",
257
+ return_code=-1,
258
+ )
259
+ except Exception as e:
260
+ return CLIResult(
261
+ success=False,
262
+ stdout="",
263
+ stderr=str(e),
264
+ return_code=-1,
265
+ )
266
+
267
+ def _parse_tasks_output(self, output: str) -> list[BeadsTask]:
268
+ """Parse JSON output containing multiple tasks.
269
+
270
+ Handles both JSON array and JSONL formats.
271
+
272
+ Args:
273
+ output: Raw command output.
274
+
275
+ Returns:
276
+ List of parsed tasks.
277
+ """
278
+ output = output.strip()
279
+ if not output:
280
+ return []
281
+
282
+ tasks = []
283
+
284
+ # Try JSON array first
285
+ try:
286
+ data = json.loads(output)
287
+ if isinstance(data, list):
288
+ for item in data:
289
+ try:
290
+ tasks.append(self._parse_task(item))
291
+ except (KeyError, ValueError) as e:
292
+ logger.warning(f"Failed to parse task: {e}")
293
+ return tasks
294
+ elif isinstance(data, dict):
295
+ # Single task wrapped in object
296
+ if "tasks" in data:
297
+ for item in data["tasks"]:
298
+ try:
299
+ tasks.append(self._parse_task(item))
300
+ except (KeyError, ValueError) as e:
301
+ logger.warning(f"Failed to parse task: {e}")
302
+ return tasks
303
+ else:
304
+ return [self._parse_task(data)]
305
+ except json.JSONDecodeError:
306
+ pass
307
+
308
+ # Try JSONL (one JSON object per line)
309
+ for line in output.split("\n"):
310
+ line = line.strip()
311
+ if not line:
312
+ continue
313
+ try:
314
+ data = json.loads(line)
315
+ tasks.append(self._parse_task(data))
316
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
317
+ logger.debug(f"Failed to parse line: {e}")
318
+
319
+ return tasks
320
+
321
+ def _parse_task(self, data: dict) -> BeadsTask:
322
+ """Parse a single task from JSON data.
323
+
324
+ Args:
325
+ data: Task data dictionary.
326
+
327
+ Returns:
328
+ BeadsTask object.
329
+
330
+ Raises:
331
+ KeyError: If required fields are missing.
332
+ ValueError: If data is invalid.
333
+ """
334
+ # Map common field names from Beads output
335
+ task_id = data.get("id") or data.get("task_id") or data.get("ID")
336
+ if not task_id:
337
+ raise KeyError("Task ID not found")
338
+
339
+ title = data.get("title") or data.get("name") or data.get("summary") or ""
340
+
341
+ # Map status
342
+ status_str = data.get("status", "pending").lower()
343
+ status_map = {
344
+ "pending": BeadsTaskStatus.PENDING,
345
+ "todo": BeadsTaskStatus.PENDING,
346
+ "ready": BeadsTaskStatus.PENDING,
347
+ "in_progress": BeadsTaskStatus.IN_PROGRESS,
348
+ "in-progress": BeadsTaskStatus.IN_PROGRESS,
349
+ "active": BeadsTaskStatus.IN_PROGRESS,
350
+ "working": BeadsTaskStatus.IN_PROGRESS,
351
+ "done": BeadsTaskStatus.DONE,
352
+ "completed": BeadsTaskStatus.DONE,
353
+ "finished": BeadsTaskStatus.DONE,
354
+ "blocked": BeadsTaskStatus.BLOCKED,
355
+ "waiting": BeadsTaskStatus.BLOCKED,
356
+ "cancelled": BeadsTaskStatus.CANCELLED,
357
+ "canceled": BeadsTaskStatus.CANCELLED,
358
+ "abandoned": BeadsTaskStatus.CANCELLED,
359
+ }
360
+ status = status_map.get(status_str, BeadsTaskStatus.PENDING)
361
+
362
+ return BeadsTask(
363
+ id=task_id,
364
+ title=title,
365
+ description=data.get("description", ""),
366
+ status=status,
367
+ parent_id=data.get("parent_id") or data.get("parent"),
368
+ dependencies=data.get("dependencies", []) or data.get("blocked_by", []),
369
+ tags=data.get("tags", []) or data.get("labels", []),
370
+ metadata=data.get("metadata", {}),
371
+ )
372
+
373
+ def _get_cached(self, key: str) -> any:
374
+ """Get a cached value if still valid.
375
+
376
+ Args:
377
+ key: Cache key.
378
+
379
+ Returns:
380
+ Cached value or None if expired/missing.
381
+ """
382
+ if key not in self._cache:
383
+ return None
384
+
385
+ cached_time, value = self._cache[key]
386
+ age = (datetime.now(UTC) - cached_time).total_seconds()
387
+
388
+ if age > self._cache_duration:
389
+ del self._cache[key]
390
+ return None
391
+
392
+ return value
393
+
394
+ def _set_cached(self, key: str, value: any) -> None:
395
+ """Set a cached value.
396
+
397
+ Args:
398
+ key: Cache key.
399
+ value: Value to cache.
400
+ """
401
+ self._cache[key] = (datetime.now(UTC), value)
402
+
403
+ def invalidate_cache(self) -> None:
404
+ """Clear all cached values."""
405
+ self._cache.clear()
406
+
407
+
408
+ # Convenience function
409
+ def get_beads_cli() -> BeadsCLI:
410
+ """Get a BeadsCLI instance.
411
+
412
+ Returns:
413
+ BeadsCLI instance.
414
+ """
415
+ return BeadsCLI()