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:
Steve Yegge
2025-10-17 16:28:29 -07:00
parent b8bcffba1d
commit b40de9bc41
8 changed files with 992 additions and 22 deletions

View File

@@ -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,
)