
# Context Service API - Plugin Extensibility

## Overview

The `ContextService` provides automatic context loading when users type trigger keywords. This system is fully extensible, allowing plugins to:

1. **Register custom trigger keywords** - Auto-load context when users type specific words
2. **Register custom context loaders** - Load context from different sources (files, APIs, databases, etc.)
3. **Hook into the initialization process** - Know when `ContextService` is ready for registrations

## How It Works

When the app starts:
1. `ContextService` initializes with default triggers from config
2. `ContextService` fires `CONTEXT_SERVICE_READY` event
3. Plugins hook into this event and register their triggers/loaders
4. When user types a message, all registered triggers are scanned
5. Matching contexts are loaded using registered loaders
6. Context is injected as a system message before LLM processing

## Plugin API

### 1. Hook into ContextService Initialization

```python
from kollabor.plugins import BasePlugin
from kollabor.events.models import EventType

class MyPlugin(BasePlugin):
    async def initialize(self, args, event_bus, config, ...):
        # Register hook for CONTEXT_SERVICE_READY event
        await event_bus.register_hook(
            event_type=EventType.CONTEXT_SERVICE_READY,
            callback=self._on_context_service_ready,
            priority=100,
        )
    
    async def _on_context_service_ready(self, data: dict, event):
        """Called when ContextService is ready."""
        context_service = data.get("service")
        if not context_service:
            return
        
        # Register your triggers and loaders here
        context_service.register_trigger("github", "myplugin:github")
        context_service.register_loader("myplugin", self._load_my_context)
```

### 2. Register Trigger Keywords

```python
context_service.register_trigger(
    keyword="github",  # Word/phrase that triggers this context
    context_id="myplugin:github"  # Unique ID (used by loaders)
)
```

**Example triggers:**
- "github" → Load GitHub repo context
- "database" → Load database schema context
- "api" → Load API documentation
- "project" → Load project-specific context

### 3. Register Context Loaders

```python
async def _load_my_context(self, context_id: str) -> str:
    """Custom loader for plugin-specific contexts."""
    
    # Parse context_id (e.g., "myplugin:github")
    if ":" in context_id:
        context_name = context_id.split(":", 1)[1]
    else:
        context_name = context_id
    
    # Load from your data source
    content = await self._fetch_from_api(context_name)
    
    return f"## Context: {context_name}\n\n{content}"

# Register the loader
context_service.register_loader(
    context_type="myplugin",  # Matches context_id prefix
    loader_func=self._load_my_context  # Async function
)
```

**Loader types:**
- **Built-in:** `bookmarks` (loads from BOOKMARKS.md)
- **Custom:** Any type you register (e.g., `api`, `file`, `database`)

## Configuration

### Default Triggers (config.json)

```json
{
  "context_triggers": {
    "roots": "book:the-roots",
    "kollabor": "project:kollabor-cli",
    "webceive": "business:webceive"
  }
}
```

### Bookmarks Path

```json
{
  "context": {
    "bookmarks_path": "/path/to/BOOKMARKS.md"
  }
}
```

## Examples

### Example 1: File-Based Loader

```python
async def _load_from_file(self, context_id: str) -> str:
    """Load context from plugin data files."""
    from pathlib import Path
    
    # Extract filename from context_id
    filename = context_id.split(":", 1)[1] + ".md"
    
    # Load from plugin data directory
    file_path = Path(__file__).parent / "data" / filename
    if file_path.exists():
        return file_path.read_text()
    
    return None

context_service.register_loader("file", self._load_from_file)
```

### Example 2: API-Based Loader

```python
async def _load_from_api(self, context_id: str) -> str:
    """Load context from external API."""
    import aiohttp
    
    context_name = context_id.split(":", 1)[1]
    url = f"https://api.example.com/contexts/{context_name}"
    
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            if response.status == 200:
                data = await response.json()
                return f"## API Context: {context_name}\n\n{data['content']}"
    
    return None

context_service.register_loader("api", self._load_from_api)
```

### Example 3: Database Loader

```python
async def _load_from_database(self, context_id: str) -> str:
    """Load context from database."""
    # Query your database for context
    result = await db.query(
        "SELECT content FROM contexts WHERE id = ?",
        (context_id,)
    )
    
    if result:
        return result[0]['content']
    
    return None

context_service.register_loader("database", self._load_from_database)
```

## Event Flow

```
1. App starts
   ↓
2. ContextService initializes
   ↓
3. Fires CONTEXT_SERVICE_READY event
   ↓
4. Plugins register triggers/loaders
   ↓
5. User types message
   ↓
6. scan_for_triggers() scans input
   ↓
7. For each match:
   - load_context() called
   - Loader selected by context type
   - Content loaded
   - Injected as system message
```

## Best Practices

### 1. Use unique context IDs
```python
# Good: plugin_name:specific_context
"github" → "myplugin:github"

# Bad: generic names
"github" → "github"  # Might conflict
```

### 2. Handle errors gracefully
```python
async def _load_context(self, context_id: str) -> str:
    try:
        content = await self._fetch_source(context_id)
        return content
    except Exception as e:
        logger.error(f"Failed to load context {context_id}: {e}")
        return None  # Let service handle missing context
```

### 3. Log registrations
```python
context_service.register_trigger("github", "myplugin:github")
logger.info(f"Registered trigger: github → myplugin:github")
```

### 4. Provide fallback content
```python
async def _load_context(self, context_id: str) -> str:
    content = await self._fetch_source(context_id)
    
    if not content:
        # Fallback message
        return f"## Context: {context_id}\n\nContext not found."
    
    return content
```

## Built-in Loaders

### Bookmarks Loader

Loads context sections from `BOOKMARKS.md` file.

**Trigger:** `context_id` like `book:the-roots`
**Source:** `~/.kollabor-cli/BOOKMARKS.md` (configurable via `context.bookmarks_path`)
**Format:** Markdown sections with `###` headers

**Example BOOKMARKS.md:**
```markdown
### Book: The Roots

Book about grief and finding strength.

14 chapters on:
- Child loss
- Healing journey
- Finding hope

Location: /Users/malmazan/workspace/the-roots/
```

## Testing

Write tmux tests to verify your plugin's context triggers work:

```json
{
  "name": "my_plugin_context",
  "description": "Test custom context triggers",
  "steps": [
    {"action": "start_app"},
    {"action": "type", "text": "Tell me about github"},
    {"action": "send_keys", "keys": "Enter"},
    {"action": "sleep", "seconds": 15},
    {"action": "capture"},
    {"action": "assert_contains", "pattern": "github|repo"}
  ]
}
```

## Summary

- **Register triggers** via `context_service.register_trigger()`
- **Register loaders** via `context_service.register_loader()`
- **Hook into init** via `CONTEXT_SERVICE_READY` event
- **Load from anywhere:** files, APIs, databases, in-memory
- **Test** with tmux JSON tests

See `plugins/example_context_plugin.py` for a complete working example.
