Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ SLACK_APP_TOKEN=xapp-your-app-token
# -------------------------------------------
ANTHROPIC_API_KEY=sk-ant-your-api-key
# CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token
# -- OR leave ANTHROPIC_API_KEY unset so ANTHROPIC_AUTH_TOKEN takes precedence for Ollama routing
# ANTHROPIC_AUTH_TOKEN=ollama
# ANTHROPIC_BASE_URL=http://localhost:11434
# CLAUDE_MODEL=glm-4.7:cloud

# -------------------------------------------
# Optional: Bender settings
Expand Down
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,18 @@ LOG_LEVEL="info" # Logging level (default: info)
- `chat:write` — Post messages and thread replies
- `channels:history` — Read messages in public channels
- `groups:history` — Read messages in private channels (if needed)
4. Subscribe to these **Events**:
- `app_mention` — Trigger on @mentions
- `message.channels` — Listen for thread replies
5. Install the app to your workspace and copy the Bot User OAuth Token (`xoxb-...`)
4. Configure **Event Subscriptions** (required — Socket Mode connects but delivers no events until this is enabled):
- Open the **Event Subscriptions** page and toggle **Enable Events** to **On**.
- Expand **Subscribe to bot events** and add:
- `app_mention` — Trigger on @mentions
- `message.channels` — Listen for thread replies in public channels
- `message.groups` — Listen for thread replies in private channels (if needed)
- Click **Save Changes**.

> **Note:** Having the matching OAuth scopes is *not* sufficient — you must enable Event Subscriptions and subscribe to the bot events above, or no events will reach the app.
5. Install (or **reinstall**) the app to your workspace and copy the Bot User OAuth Token (`xoxb-...`)

> **Note:** If you change scopes or event subscriptions after the first install, you must reinstall the app for the changes to take effect.

### Workspace Directory

Expand All @@ -116,13 +124,18 @@ workspace/ # Example agent configuration
### Running Bender

```bash
# Using the module
python -m bender
# Copy and fill in your environment variables first
cp .env.example .env

# Or with environment variables inline
SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... ANTHROPIC_API_KEY=sk-ant-... python -m bender
# Run with uv (loads .env into the process environment)
uv run --env-file .env python -m bender

# Or, if the venv is activated and vars are exported in your shell
python -m bender
```

> **Note:** Bender does not auto-load `.env` — the variables must be present in the process environment. `uv run --env-file .env` handles this for you; otherwise export them in your shell first.

Bender starts both the Slack Socket Mode handler and the FastAPI HTTP server concurrently.

### Slack Interaction
Expand Down
9 changes: 8 additions & 1 deletion src/bender/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import asyncio
import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path

logger = logging.getLogger(__name__)

# Default timeout for Claude Code invocations (5 minutes)
DEFAULT_TIMEOUT_SECONDS = 300
DEFAULT_MODEL_NAME = os.getenv("CLAUDE_MODEL", None)


@dataclass
Expand All @@ -31,6 +33,7 @@ async def invoke_claude(
session_id: str | None = None,
resume: bool = False,
timeout: int = DEFAULT_TIMEOUT_SECONDS,
model_name: str | None = DEFAULT_MODEL_NAME,
) -> ClaudeResponse:
"""Invoke Claude Code CLI in headless mode via subprocess.

Expand All @@ -49,6 +52,8 @@ async def invoke_claude(
"""
cmd = ["claude", "--print", "--output-format", "json"]

if model_name:
cmd.extend(["--model", model_name])
if resume and session_id:
cmd.extend(["--resume", session_id])
elif session_id:
Expand Down Expand Up @@ -89,7 +94,9 @@ async def invoke_claude(
if process.returncode != 0:
error_msg = stderr.decode().strip() if stderr else "Unknown error"
logger.error("Claude Code failed (exit=%d): %s", process.returncode, error_msg)
raise ClaudeCodeError(f"Claude Code exited with code {process.returncode}: {error_msg}")
raise ClaudeCodeError(
f"Claude Code exited with code {process.returncode}: {error_msg}"
)

return _parse_response(stdout.decode(), session_id or "")

Expand Down
5 changes: 4 additions & 1 deletion src/bender/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Settings(BaseSettings):
# Required: Claude Code authentication (at least one)
anthropic_api_key: str | None = None
claude_code_oauth_token: str | None = None
anthropic_base_url: str | None = None

# Optional
bender_workspace: Path = Path.cwd()
Expand All @@ -29,7 +30,9 @@ class Settings(BaseSettings):

def validate_auth(self) -> None:
"""Ensure at least one Claude Code authentication method is configured."""
if not self.anthropic_api_key and not self.claude_code_oauth_token:
if self.anthropic_base_url:
logging.info(f"Using ANTHROPIC_BASE_URL: {self.anthropic_base_url}")
elif not self.anthropic_api_key and not self.claude_code_oauth_token:
raise ValueError(
"At least one authentication method is required: "
"ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN"
Expand Down