Skip to content
Merged
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
15 changes: 12 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Follow these patterns consistently (aligned with agent-session-analytics):
| LaunchAgent | `com.evansenter.agent-event-bus.plist` |
| systemd service | `agent-event-bus.service` |

**Environment variables**: `AGENT_EVENT_BUS_*` prefix (e.g., `_DB`, `_URL`, `_AUTH_DISABLED`, `_ICON`, `_TESTING`)
**Environment variables**: `AGENT_EVENT_BUS_*` prefix (e.g., `_DB`, `_LOG`, `_ERR`, `_URL`, `_AUTH_DISABLED`, `_ICON`, `_TESTING`)

---

Expand Down Expand Up @@ -149,12 +149,21 @@ CLI and MCP expose the same functionality:
## Operations

```bash
# Watch live activity
tail -f ~/.claude/contrib/agent-event-bus/agent-event-bus.log
# Watch live activity (auto-detects local vs remote bus from MCP config)
make logs

# Force a remote tail (overrides auto-detect)
make logs BUS_HOST=your-server.tailnet.ts.net

# Override database path
AGENT_EVENT_BUS_DB=/path/to/db.sqlite agent-event-bus

# Override log/error file paths — the install scripts substitute these into
# the launchd plist / systemd unit, so they must be in the environment of
# `make install-server` itself. Prefix on the make invocation (or `export`
# them first) — bare shell assignments below will NOT apply at install time.
AGENT_EVENT_BUS_LOG=/path/to/custom.log AGENT_EVENT_BUS_ERR=/path/to/custom.err make install-server

# Dev mode console logging
DEV_MODE=1 agent-event-bus

Expand Down
36 changes: 32 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
.PHONY: check fmt lint test clean install-server install-client uninstall dev venv restart logs

# Canonical paths (override with matching AGENT_EVENT_BUS_* env vars)
LOG_FILE := $(or $(AGENT_EVENT_BUS_LOG),$(HOME)/.claude/contrib/agent-event-bus/agent-event-bus.log)
ERR_FILE := $(or $(AGENT_EVENT_BUS_ERR),$(HOME)/.claude/contrib/agent-event-bus/agent-event-bus.err)

# Run all quality gates (format check, lint, tests)
check: fmt lint test

Expand Down Expand Up @@ -124,7 +128,7 @@ restart:
if launchctl list | grep -q "com.evansenter.agent-event-bus"; then \
echo "Service restarted successfully"; \
else \
echo "Error: Service failed to start. Check ~/.claude/contrib/agent-event-bus/agent-event-bus.err"; \
echo "Error: Service failed to start. Check $(ERR_FILE)"; \
exit 1; \
fi; \
else \
Expand All @@ -138,11 +142,35 @@ restart:
if systemctl --user is-active agent-event-bus &>/dev/null; then \
echo "Service restarted successfully"; \
else \
echo "Error: Service failed to start. Check ~/.claude/contrib/agent-event-bus/agent-event-bus.err"; \
echo "Error: Service failed to start. Check $(ERR_FILE)"; \
exit 1; \
fi; \
fi

# Tail the event bus log
# Tail the event bus log (auto-detects local vs remote bus)
# Override with BUS_HOST=<tailscale-host> to force a remote tail.
# Auto-detect parses `claude mcp list` text output (agent-event-bus: URL ...);
# detection silently falls through to local if that format ever changes.
# Remote tail path is hardcoded to the canonical default: we can't know the
# remote bus's AGENT_EVENT_BUS_LOG from here, so a remote override is not
# honored by `make logs`. Run the tail directly over SSH in that case.
logs:
@tail -f ~/.claude/contrib/agent-event-bus/agent-event-bus.log
@HOST="$(BUS_HOST)"; \
if [ -z "$$HOST" ]; then \
CLAUDE_CMD=$$(command -v claude || echo "$$HOME/.local/bin/claude"); \
if [ -x "$$CLAUDE_CMD" ]; then \
URL=$$("$$CLAUDE_CMD" mcp list 2>/dev/null | awk '/^agent-event-bus:/ {print $$2}'); \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The awk parser depends on the current claude mcp list text format (colon-terminated name in field 1, URL in field 2). If that output ever changes (extra prefix, different separator, extra decoration), detection silently fails and falls through to the local tail, which reproduces the exact bug this PR is fixing. A short code comment noting the format dependency would help future debugging, or using claude mcp get agent-event-bus with structured output (if supported) would be a more stable contract.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the format-dependency note into the target's header comment block (Makefile:146-148). Kept the text parser since claude mcp list has no structured output flag today. Resolved in c770c0f.

fi; \
if [ -z "$$URL" ]; then \
URL="$$AGENT_EVENT_BUS_URL"; \
fi; \
if echo "$$URL" | grep -qE '^https?://'; then \
HOST=$$(echo "$$URL" | sed -E 's|https?://||; s|/.*||; s|:[0-9]+$$||; s|^\[||; s|\]$$||'); \
fi; \
fi; \
if [ -z "$$HOST" ] || [ "$$HOST" = "localhost" ] || [ "$$HOST" = "127.0.0.1" ] || [ "$$HOST" = "::1" ] || [ "$$HOST" = "0.0.0.0" ]; then \
tail -f $(LOG_FILE); \
else \
echo "Tailing remote bus at $$HOST (Ctrl-C to exit)..."; \
ssh -t -- "$$HOST" 'tail -f ~/.claude/contrib/agent-event-bus/agent-event-bus.log'; \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The log path is now repeated on lines 163 and 166 (and appears in the restart error message earlier in the file). Extracting a Make variable at the top of the Makefile would keep the three references in sync. Minor DRY nit.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded scope per user direction — see PR comment above. TL;DR: introduced AGENT_EVENT_BUS_LOG / AGENT_EVENT_BUS_ERR env vars (mirroring existing AGENT_EVENT_BUS_DB pattern), wired end-to-end through Python, Makefile, install scripts, and launchd/systemd template substitution. c770c0f.

fi
6 changes: 4 additions & 2 deletions scripts/agent-event-bus.service
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ Type=simple
WorkingDirectory=__PROJECT_DIR__
Environment=PYTHONPATH=__PROJECT_DIR__/src
Environment=AGENT_EVENT_BUS_ICON=__PROJECT_DIR__/assets/icon.png
Environment=AGENT_EVENT_BUS_LOG=__LOG_FILE__
Environment=AGENT_EVENT_BUS_ERR=__ERR_FILE__
ExecStart=__VENV_PYTHON__ -m agent_event_bus.server
Restart=always
RestartSec=5
StandardOutput=append:__HOME__/.claude/contrib/agent-event-bus/agent-event-bus.log
StandardError=append:__HOME__/.claude/contrib/agent-event-bus/agent-event-bus.err
StandardOutput=append:__LOG_FILE__
StandardError=append:__ERR_FILE__

[Install]
WantedBy=default.target
6 changes: 5 additions & 1 deletion scripts/com.evansenter.agent-event-bus.plist
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<string>__PROJECT_DIR__/src</string>
<key>AGENT_EVENT_BUS_ICON</key>
<string>__PROJECT_DIR__/assets/icon.png</string>
<key>AGENT_EVENT_BUS_LOG</key>
<string>__LOG_FILE__</string>
<key>AGENT_EVENT_BUS_ERR</key>
<string>__ERR_FILE__</string>
<!-- Multi-machine: Set HOST to 0.0.0.0 to accept remote connections -->
<!-- <key>HOST</key> -->
<!-- <string>0.0.0.0</string> -->
Expand All @@ -39,7 +43,7 @@
<string>__HOME__/.claude/contrib/agent-event-bus/agent-event-bus.stdout</string>

<key>StandardErrorPath</key>
<string>__HOME__/.claude/contrib/agent-event-bus/agent-event-bus.err</string>
<string>__ERR_FILE__</string>

<key>ProcessType</key>
<string>Background</string>
Expand Down
12 changes: 9 additions & 3 deletions scripts/install-launchagent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ PLIST_TEMPLATE="$SCRIPT_DIR/com.evansenter.agent-event-bus.plist"
PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.agent-event-bus.plist"
LABEL="com.evansenter.agent-event-bus"

# Resolve paths (respect env var overrides, fall back to canonical defaults)
LOG_FILE="${AGENT_EVENT_BUS_LOG:-$HOME/.claude/contrib/agent-event-bus/agent-event-bus.log}"
ERR_FILE="${AGENT_EVENT_BUS_ERR:-$HOME/.claude/contrib/agent-event-bus/agent-event-bus.err}"

# Check venv exists
if [[ ! -f "$VENV_PYTHON" ]]; then
echo "Error: Virtual environment not found at $PROJECT_DIR/.venv"
Expand All @@ -32,6 +36,8 @@ echo "Installing LaunchAgent..."
sed -e "s|__VENV_PYTHON__|$VENV_PYTHON|g" \
-e "s|__PROJECT_DIR__|$PROJECT_DIR|g" \
-e "s|__HOME__|$HOME|g" \
-e "s|__LOG_FILE__|$LOG_FILE|g" \
-e "s|__ERR_FILE__|$ERR_FILE|g" \
"$PLIST_TEMPLATE" > "$PLIST_DEST"

# Load the service
Expand All @@ -43,8 +49,8 @@ sleep 1
if launchctl list | grep -q "$LABEL"; then
echo ""
echo "Agent Event Bus installed and running!"
echo " Logs: ~/.claude/contrib/agent-event-bus/agent-event-bus.log"
echo " Errors: ~/.claude/contrib/agent-event-bus/agent-event-bus.err"
echo " Logs: $LOG_FILE"
echo " Errors: $ERR_FILE"
echo ""

# Also install CLI for use in hooks/scripts
Expand All @@ -54,7 +60,7 @@ if launchctl list | grep -q "$LABEL"; then
echo "To uninstall: $SCRIPT_DIR/uninstall-launchagent.sh"
osascript -e 'display notification "LaunchAgent installed and running" with title "Agent Event Bus"' 2>/dev/null
else
echo "Error: Service failed to start. Check ~/.claude/contrib/agent-event-bus/agent-event-bus.err"
echo "Error: Service failed to start. Check $ERR_FILE"
osascript -e 'display notification "Failed to start - check logs" with title "Agent Event Bus" sound name "Basso"' 2>/dev/null
exit 1
fi
12 changes: 9 additions & 3 deletions scripts/install-systemd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ SERVICE_DIR="$HOME/.config/systemd/user"
SERVICE_DEST="$SERVICE_DIR/agent-event-bus.service"
SERVICE_NAME="agent-event-bus"

# Resolve paths (respect env var overrides, fall back to canonical defaults)
LOG_FILE="${AGENT_EVENT_BUS_LOG:-$HOME/.claude/contrib/agent-event-bus/agent-event-bus.log}"
ERR_FILE="${AGENT_EVENT_BUS_ERR:-$HOME/.claude/contrib/agent-event-bus/agent-event-bus.err}"

# Check venv exists
if [[ ! -f "$VENV_PYTHON" ]]; then
echo "Error: Virtual environment not found at $PROJECT_DIR/.venv"
Expand All @@ -33,6 +37,8 @@ echo "Installing systemd service..."
sed -e "s|__VENV_PYTHON__|$VENV_PYTHON|g" \
-e "s|__PROJECT_DIR__|$PROJECT_DIR|g" \
-e "s|__HOME__|$HOME|g" \
-e "s|__LOG_FILE__|$LOG_FILE|g" \
-e "s|__ERR_FILE__|$ERR_FILE|g" \
"$SERVICE_TEMPLATE" > "$SERVICE_DEST"

# Reload systemd and start service
Expand All @@ -45,8 +51,8 @@ sleep 1
if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then
echo ""
echo "Agent Event Bus installed and running!"
echo " Logs: ~/.claude/contrib/agent-event-bus/agent-event-bus.log"
echo " Errors: ~/.claude/contrib/agent-event-bus/agent-event-bus.err"
echo " Logs: $LOG_FILE"
echo " Errors: $ERR_FILE"
echo " Status: systemctl --user status $SERVICE_NAME"
echo ""

Expand All @@ -58,6 +64,6 @@ if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then
else
echo "Error: Service failed to start. Check logs:"
echo " journalctl --user -u $SERVICE_NAME"
echo " ~/.claude/contrib/agent-event-bus/agent-event-bus.err"
echo " $ERR_FILE"
exit 1
fi
7 changes: 4 additions & 3 deletions src/agent_event_bus/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@
from agent_event_bus.storage import Event, Session, SQLiteStorage, Webhook

# Configure logging
# Always log to ~/.claude/contrib/agent-event-bus/agent-event-bus.log for tail -f access
# In dev mode, also log to console
# Default log path: ~/.claude/contrib/agent-event-bus/agent-event-bus.log
# Override with AGENT_EVENT_BUS_LOG env var (matches AGENT_EVENT_BUS_DB pattern)
# Skip file logging during tests to avoid polluting production logs
LOG_FILE = Path.home() / ".claude" / "contrib" / "agent-event-bus" / "agent-event-bus.log"
_DEFAULT_LOG_FILE = Path.home() / ".claude" / "contrib" / "agent-event-bus" / "agent-event-bus.log"
LOG_FILE = Path(os.environ.get("AGENT_EVENT_BUS_LOG", str(_DEFAULT_LOG_FILE)))

logger = logging.getLogger("agent-event-bus")
logger.setLevel(logging.DEBUG if os.environ.get("DEV_MODE") else logging.INFO)
Expand Down
82 changes: 82 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import logging
import os
import socket
import subprocess
import sys
from datetime import datetime

import pytest
Expand Down Expand Up @@ -1262,3 +1264,83 @@ def test_publish_event_no_warning_on_valid_channel(self, caplog):
publish_event("test", "payload", channel="machine:localhost")

assert "Invalid" not in caplog.text


class TestMakeLogsHostDetection:
"""Tests for the host-detection pipeline in the Makefile's `logs` target.

The grep-then-sed pipeline is at Makefile:166-168. These tests duplicate it
via shell subprocess so regressions in the URL → HOST extraction are caught.
Keep this in sync if the Makefile pipeline changes.
"""

@staticmethod
def _resolve_host(url: str) -> str:
"""Run the Makefile's detection pipeline against a single URL."""
script = (
'URL="$1"; HOST=""; '
"if echo \"$URL\" | grep -qE '^https?://'; then "
'HOST=$(echo "$URL" | '
"sed -E 's|https?://||; s|/.*||; s|:[0-9]+$||; s|^\\[||; s|\\]$||'); "
"fi; "
'printf "%s" "$HOST"'
)
result = subprocess.run(
["bash", "-c", script, "_", url],
capture_output=True,
text=True,
check=True,
)
return result.stdout

@pytest.mark.parametrize(
"url,expected",
[
(
"https://mac-mini.tailac7b3c.ts.net/agent-event-bus/mcp",
"mac-mini.tailac7b3c.ts.net",
),
("http://host.example.com:8080/mcp", "host.example.com"),
("http://127.0.0.1:8080/mcp", "127.0.0.1"),
("http://[::1]:8080/mcp", "::1"),
("https://localhost/mcp", "localhost"),
("stdio://something", ""), # non-http scheme → HOST unset
("", ""), # empty URL → HOST unset
("https://-oProxyCommand=id/mcp", "-oProxyCommand=id"), # mitigated by `ssh --`
],
)
def test_host_extraction(self, url, expected):
assert self._resolve_host(url) == expected


class TestLogFileEnvVar:
"""Tests for AGENT_EVENT_BUS_LOG env var override."""

def _resolved_log_file(self, env: dict) -> str:
"""Resolve server.LOG_FILE in a subprocess with the given env.

Subprocess required because LOG_FILE is set at module import time,
and reloading the server module in-process would disrupt other tests.
"""
result = subprocess.run(
[sys.executable, "-c", "from agent_event_bus import server; print(server.LOG_FILE)"],
env=env,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, f"stderr: {result.stderr}"
return result.stdout.strip()

def test_log_file_respects_env_var(self, tmp_path):
"""LOG_FILE honors AGENT_EVENT_BUS_LOG when set."""
custom = tmp_path / "custom.log"
env = {**os.environ, "AGENT_EVENT_BUS_LOG": str(custom), "AGENT_EVENT_BUS_TESTING": "1"}
assert self._resolved_log_file(env) == str(custom)

def test_log_file_falls_back_to_default(self):
"""LOG_FILE uses the canonical default when AGENT_EVENT_BUS_LOG is unset."""
env = {k: v for k, v in os.environ.items() if k != "AGENT_EVENT_BUS_LOG"}
env["AGENT_EVENT_BUS_TESTING"] = "1"
expected = os.path.expanduser("~/.claude/contrib/agent-event-bus/agent-event-bus.log")
assert self._resolved_log_file(env) == expected
Loading