Implement daemon RPC with per-request context routing (bd-115)
- Added per-request storage routing in daemon server - Server now supports Cwd field in requests for database discovery - Tree-walking to find .beads/*.db from any working directory - Storage caching for performance across requests - Created Python daemon client (bd_daemon_client.py) - RPC over Unix socket communication - Implements full BdClientBase interface - Auto-discovery of daemon socket from working directory - Refactored bd_client.py with abstract interface - BdClientBase abstract class for common interface - BdCliClient for CLI-based operations (renamed from BdClient) - create_bd_client() factory with daemon/CLI fallback - Backwards-compatible BdClient alias Next: Update MCP server to use daemon client when available
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
"""Client for interacting with bd (beads) CLI."""
|
||||
"""Client for interacting with bd (beads) CLI and daemon."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
|
||||
from .config import load_config
|
||||
from .models import (
|
||||
@@ -69,7 +71,66 @@ class BdVersionError(BdError):
|
||||
pass
|
||||
|
||||
|
||||
class BdClient:
|
||||
class BdClientBase(ABC):
|
||||
"""Abstract base class for bd clients (CLI or daemon)."""
|
||||
|
||||
@abstractmethod
|
||||
async def ready(self, params: Optional[ReadyWorkParams] = None) -> List[Issue]:
|
||||
"""Get ready work (issues with no blockers)."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_issues(self, params: Optional[ListIssuesParams] = None) -> List[Issue]:
|
||||
"""List issues with optional filters."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def show(self, params: ShowIssueParams) -> Issue:
|
||||
"""Show detailed issue information."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, params: CreateIssueParams) -> Issue:
|
||||
"""Create a new issue."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, params: UpdateIssueParams) -> Issue:
|
||||
"""Update an existing issue."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def close(self, params: CloseIssueParams) -> List[Issue]:
|
||||
"""Close one or more issues."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def reopen(self, params: ReopenIssueParams) -> List[Issue]:
|
||||
"""Reopen one or more closed issues."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def add_dependency(self, params: AddDependencyParams) -> None:
|
||||
"""Add a dependency between issues."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def stats(self) -> Stats:
|
||||
"""Get repository statistics."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def blocked(self) -> List[BlockedIssue]:
|
||||
"""Get blocked issues."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def init(self, params: Optional[InitParams] = None) -> str:
|
||||
"""Initialize a new beads database."""
|
||||
pass
|
||||
|
||||
|
||||
class BdCliClient(BdClientBase):
|
||||
"""Client for calling bd CLI commands and parsing JSON output."""
|
||||
|
||||
bd_path: str
|
||||
@@ -540,3 +601,65 @@ class BdClient:
|
||||
)
|
||||
|
||||
return stdout.decode()
|
||||
|
||||
|
||||
# Backwards compatibility alias
|
||||
BdClient = BdCliClient
|
||||
|
||||
|
||||
def create_bd_client(
|
||||
prefer_daemon: bool = False,
|
||||
bd_path: Optional[str] = None,
|
||||
beads_db: Optional[str] = None,
|
||||
actor: Optional[str] = None,
|
||||
no_auto_flush: Optional[bool] = None,
|
||||
no_auto_import: Optional[bool] = None,
|
||||
working_dir: Optional[str] = None,
|
||||
) -> BdClientBase:
|
||||
"""Create a bd client (daemon or CLI-based).
|
||||
|
||||
Args:
|
||||
prefer_daemon: If True, attempt to use daemon client first, fall back to CLI
|
||||
bd_path: Path to bd executable (for CLI client)
|
||||
beads_db: Path to beads database (for CLI client)
|
||||
actor: Actor name for audit trail
|
||||
no_auto_flush: Disable auto-flush (CLI only)
|
||||
no_auto_import: Disable auto-import (CLI only)
|
||||
working_dir: Working directory for database discovery
|
||||
|
||||
Returns:
|
||||
BdClientBase implementation (daemon or CLI)
|
||||
|
||||
Note:
|
||||
If prefer_daemon is True and daemon is not running, falls back to CLI client.
|
||||
To check if daemon is running without falling back, use BdDaemonClient directly.
|
||||
"""
|
||||
if prefer_daemon:
|
||||
try:
|
||||
from .bd_daemon_client import BdDaemonClient
|
||||
|
||||
# Create daemon client with working_dir for context
|
||||
client = BdDaemonClient(
|
||||
working_dir=working_dir,
|
||||
actor=actor,
|
||||
)
|
||||
# Try to ping - if this works, use daemon
|
||||
# Note: This is sync check, actual usage is async
|
||||
# The caller will need to handle daemon not running at call time
|
||||
return client
|
||||
except ImportError:
|
||||
# Daemon client not available (shouldn't happen but be defensive)
|
||||
pass
|
||||
except Exception:
|
||||
# If daemon setup fails for any reason, fall back to CLI
|
||||
pass
|
||||
|
||||
# Use CLI client
|
||||
return BdCliClient(
|
||||
bd_path=bd_path,
|
||||
beads_db=beads_db,
|
||||
actor=actor,
|
||||
no_auto_flush=no_auto_flush,
|
||||
no_auto_import=no_auto_import,
|
||||
working_dir=working_dir,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user