earl-bot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +40 -0
- data/CLAUDE.md +260 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +177 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/Rakefile +11 -0
- data/bin/README.md +21 -0
- data/bin/ci +49 -0
- data/bin/claude-context +155 -0
- data/bin/claude-usage +110 -0
- data/bin/coverage +221 -0
- data/bin/rubocop +10 -0
- data/bin/watch-ci +198 -0
- data/config/earl-claude-home/.claude/CLAUDE.md +10 -0
- data/config/earl-claude-home/.claude/settings.json +34 -0
- data/earl-bot.gemspec +42 -0
- data/exe/earl +51 -0
- data/exe/earl-install +129 -0
- data/exe/earl-permission-server +39 -0
- data/lib/earl/claude_session/stats.rb +76 -0
- data/lib/earl/claude_session.rb +468 -0
- data/lib/earl/command_executor/constants.rb +53 -0
- data/lib/earl/command_executor/heartbeat_display.rb +54 -0
- data/lib/earl/command_executor/lifecycle_handler.rb +61 -0
- data/lib/earl/command_executor/session_handler.rb +126 -0
- data/lib/earl/command_executor/spawn_handler.rb +99 -0
- data/lib/earl/command_executor/stats_formatter.rb +66 -0
- data/lib/earl/command_executor/usage_handler.rb +132 -0
- data/lib/earl/command_executor.rb +128 -0
- data/lib/earl/command_parser.rb +57 -0
- data/lib/earl/config.rb +94 -0
- data/lib/earl/cron_parser.rb +105 -0
- data/lib/earl/formatting.rb +14 -0
- data/lib/earl/heartbeat_config.rb +101 -0
- data/lib/earl/heartbeat_scheduler/config_reloading.rb +64 -0
- data/lib/earl/heartbeat_scheduler/execution.rb +105 -0
- data/lib/earl/heartbeat_scheduler/heartbeat_state.rb +41 -0
- data/lib/earl/heartbeat_scheduler/lifecycle.rb +75 -0
- data/lib/earl/heartbeat_scheduler.rb +131 -0
- data/lib/earl/logging.rb +12 -0
- data/lib/earl/mattermost/api_client.rb +85 -0
- data/lib/earl/mattermost.rb +261 -0
- data/lib/earl/mcp/approval_handler.rb +304 -0
- data/lib/earl/mcp/config.rb +62 -0
- data/lib/earl/mcp/github_pat_handler.rb +450 -0
- data/lib/earl/mcp/handler_base.rb +13 -0
- data/lib/earl/mcp/heartbeat_handler.rb +310 -0
- data/lib/earl/mcp/memory_handler.rb +89 -0
- data/lib/earl/mcp/server.rb +123 -0
- data/lib/earl/mcp/tmux_handler.rb +562 -0
- data/lib/earl/memory/prompt_builder.rb +40 -0
- data/lib/earl/memory/store.rb +125 -0
- data/lib/earl/message_queue.rb +56 -0
- data/lib/earl/permission_config.rb +22 -0
- data/lib/earl/question_handler/question_posting.rb +58 -0
- data/lib/earl/question_handler.rb +116 -0
- data/lib/earl/runner/idle_management.rb +44 -0
- data/lib/earl/runner/lifecycle.rb +73 -0
- data/lib/earl/runner/message_handling.rb +121 -0
- data/lib/earl/runner/reaction_handling.rb +42 -0
- data/lib/earl/runner/response_lifecycle.rb +96 -0
- data/lib/earl/runner/service_builder.rb +48 -0
- data/lib/earl/runner/startup.rb +73 -0
- data/lib/earl/runner/thread_context_builder.rb +43 -0
- data/lib/earl/runner.rb +70 -0
- data/lib/earl/safari_automation.rb +497 -0
- data/lib/earl/session_manager/persistence.rb +46 -0
- data/lib/earl/session_manager/session_creation.rb +108 -0
- data/lib/earl/session_manager.rb +92 -0
- data/lib/earl/session_store.rb +84 -0
- data/lib/earl/streaming_response.rb +219 -0
- data/lib/earl/tmux/parsing.rb +80 -0
- data/lib/earl/tmux/processes.rb +34 -0
- data/lib/earl/tmux/sessions.rb +41 -0
- data/lib/earl/tmux.rb +122 -0
- data/lib/earl/tmux_monitor/alert_dispatcher.rb +53 -0
- data/lib/earl/tmux_monitor/output_analyzer.rb +35 -0
- data/lib/earl/tmux_monitor/permission_forwarder.rb +80 -0
- data/lib/earl/tmux_monitor/question_forwarder.rb +124 -0
- data/lib/earl/tmux_monitor.rb +249 -0
- data/lib/earl/tmux_session_store.rb +133 -0
- data/lib/earl/tool_input_formatter.rb +44 -0
- data/lib/earl/version.rb +5 -0
- data/lib/earl.rb +87 -0
- data/lib/tasks/.keep +1 -0
- metadata +248 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3d920b59998a582669ec68fa575d136973e407d257948be9d2636ce4f96f3c12
|
|
4
|
+
data.tar.gz: 8791905132f6793c8524722e5c444cc6b69399dd838e2a64cc361d84f1e8bdb2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '08270a52ad3c2a47df4abd30ce4e116dbacc65a4cdec0631908c577456aa62963e09535c4549750da61faad654f81e0a6596caca37798570ccc33aac155dbb39'
|
|
7
|
+
data.tar.gz: b35cc27ffaef9b382cea42313f7bb237c250e52083c563d383abb2f06be0557a60c76e9c6cb6e3656805ac3fa543aae79ee77d6303532a980d513f0927d5d180
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby-4.0.1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-02-24
|
|
9
|
+
|
|
10
|
+
Initial public release. EARL has been in active personal use since June 2025,
|
|
11
|
+
running ~96 commits of development before this first tagged release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Core bot** — WebSocket connection to Mattermost, message routing, and Claude CLI spawning
|
|
16
|
+
- **Streaming responses** — first text chunk creates a post, subsequent chunks update via debounced PUT
|
|
17
|
+
- **Session persistence** — follow-up messages in same thread reuse the same Claude context; sessions survive restarts
|
|
18
|
+
- **Permission approval** — tool use gated by Mattermost emoji reactions via MCP sidecar server
|
|
19
|
+
- **Persistent memory** — markdown-based memory files (SOUL.md, USER.md, daily notes) injected into Claude sessions
|
|
20
|
+
- **Memory MCP tools** — `save_memory` and `search_memory` for Claude to manage its own memory
|
|
21
|
+
- **Heartbeat scheduler** — cron, interval, and one-shot (`run_at`) scheduled tasks that spawn Claude sessions
|
|
22
|
+
- **Heartbeat MCP tool** — `manage_heartbeat` for Claude to CRUD heartbeat schedules at runtime
|
|
23
|
+
- **Tmux session supervisor** — Mattermost as control plane for all running Claude sessions (EARL-managed and standalone)
|
|
24
|
+
- **Tmux MCP tool** — `manage_tmux_sessions` for Claude to list, capture, approve, spawn, and kill tmux sessions
|
|
25
|
+
- **GitHub PAT MCP tool** — Safari automation for creating fine-grained GitHub personal access tokens
|
|
26
|
+
- **Commands** — `!help`, `!stats`, `!stop`, `!kill`, `!compact`, `!cd`, `!permissions`, `!heartbeats`, `!usage`, `!context`, `!sessions`, `!session`, `!restart`, `!spawn`, `!update`, `!escape`
|
|
27
|
+
- **Dev/prod environments** — simultaneous dev and prod instances with separate config, bots, and channels
|
|
28
|
+
- **Claude HOME isolation** — EARL's Claude sessions use an isolated config directory
|
|
29
|
+
- **Thread context** — new sessions in existing threads get Mattermost transcript for context
|
|
30
|
+
- **Message queuing** — per-thread message queue for busy sessions
|
|
31
|
+
- **Graceful shutdown** — SIGINT/SIGTERM pauses sessions; SIGHUP restarts in-place
|
|
32
|
+
- **Install script** — `earl-install` sets up config dirs, clones prod repo, creates wrapper
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Converted from Rails application to standalone Ruby gem (`earl-bot`)
|
|
37
|
+
- Replaced Rails test infrastructure with plain Minitest
|
|
38
|
+
- Simplified Gemfile from 92 lines (Rails + ~30 gems) to gemspec + 1 runtime dependency
|
|
39
|
+
|
|
40
|
+
[0.1.0]: https://github.com/ericboehs/earl/releases/tag/v0.1.0
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
**EARL** (Eric's Automated Response Line) is a Ruby CLI bot that connects to Mattermost via WebSocket, listens for messages in configured channels, spawns Claude Code CLI sessions, and streams responses back as threaded replies.
|
|
8
|
+
|
|
9
|
+
This is a standalone Ruby gem (`earl-bot`) with one runtime dependency (`websocket-client-simple`).
|
|
10
|
+
|
|
11
|
+
Reference implementation: [claude-threads](https://github.com/anneschuth/claude-threads) (TypeScript/Bun).
|
|
12
|
+
|
|
13
|
+
## Environments
|
|
14
|
+
|
|
15
|
+
EARL supports dev and prod running simultaneously with separate config, bots, and channels.
|
|
16
|
+
|
|
17
|
+
| | Production | Development |
|
|
18
|
+
|--|-----------|-------------|
|
|
19
|
+
| **Config root** | `~/.config/earl/` | `~/.config/earl-dev/` |
|
|
20
|
+
| **Repo checkout** | `~/.local/share/earl/` (stable clone) | `~/Code/ericboehs/earl/` (working copy) |
|
|
21
|
+
| **Start command** | `earl` (from `~/bin/earl`) | `exe/earl` (from repo, direnv loads `.envrc`) |
|
|
22
|
+
| **Bot account** | `@earl` | `@earl-dev` |
|
|
23
|
+
| **Detection** | `EARL_ENV` unset or `production` | `EARL_ENV=development` via direnv |
|
|
24
|
+
|
|
25
|
+
Config root is derived from `EARL_ENV` via `Earl.config_root`. All file paths (sessions, memory, heartbeats, MCP configs, allowed tools) are relative to the config root.
|
|
26
|
+
|
|
27
|
+
## Running
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Development (from repo checkout, direnv sets EARL_ENV=development)
|
|
31
|
+
exe/earl
|
|
32
|
+
|
|
33
|
+
# Production (from ~/bin wrapper, uses ~/.local/share/earl/)
|
|
34
|
+
earl
|
|
35
|
+
|
|
36
|
+
# Restart a running instance (sends SIGHUP via PID file)
|
|
37
|
+
exe/earl restart # dev
|
|
38
|
+
earl restart # prod
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires env vars (see `<config_root>/env` or `.envrc`):
|
|
42
|
+
- `EARL_ENV` — Environment: `production` (default) or `development`
|
|
43
|
+
- `MATTERMOST_URL` — Mattermost server URL
|
|
44
|
+
- `MATTERMOST_BOT_TOKEN` — Bot authentication token
|
|
45
|
+
- `MATTERMOST_BOT_ID` — Bot user ID (to ignore own messages)
|
|
46
|
+
- `EARL_CHANNEL_ID` — Default channel to listen in
|
|
47
|
+
- `EARL_CHANNELS` — Multi-channel config (comma-separated `channel_id:/working/dir` pairs, e.g. `chan1:/path1,chan2:/path2`)
|
|
48
|
+
- `EARL_ALLOWED_USERS` — Comma-separated usernames allowed to interact
|
|
49
|
+
- `EARL_SKIP_PERMISSIONS` — Set to `true` to use `--dangerously-skip-permissions` instead of MCP approval
|
|
50
|
+
- `EARL_CLAUDE_HOME` — Custom HOME for Claude subprocesses (default: `<config_root>/claude-home`)
|
|
51
|
+
|
|
52
|
+
Optional config files (under `<config_root>/`):
|
|
53
|
+
- `heartbeats.yml` — Heartbeat schedule definitions
|
|
54
|
+
- `memory/` — Persistent memory files (SOUL.md, USER.md, daily notes)
|
|
55
|
+
- `earl.pid` — PID file for running instance (used by `earl restart`)
|
|
56
|
+
- `sessions.json` — Session persistence store
|
|
57
|
+
- `allowed_tools/` — Per-thread tool approval lists
|
|
58
|
+
- `tmux_sessions.json` — Tmux session metadata persistence
|
|
59
|
+
- `claude-home/` — Default working directory for Claude subprocesses (project-level CLAUDE.md lives here)
|
|
60
|
+
- `env` — Environment variables for launchd/wrapper (secrets, config)
|
|
61
|
+
- `logs/` — stdout/stderr logs when running via launchd
|
|
62
|
+
|
|
63
|
+
## Setup
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
exe/earl-install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
On first run this creates env files for both environments — fill in secrets and re-run. On subsequent runs it:
|
|
70
|
+
1. Creates config dirs for both dev (`~/.config/earl-dev/`) and prod (`~/.config/earl/`)
|
|
71
|
+
2. Copies default Claude project config to both `claude-home/` dirs
|
|
72
|
+
3. Clones the repo to `~/.local/share/earl/` (prod)
|
|
73
|
+
4. Creates `~/bin/earl` wrapper script (prod)
|
|
74
|
+
|
|
75
|
+
### Claude Project Directory
|
|
76
|
+
|
|
77
|
+
Claude subprocesses spawned by EARL use `<config_root>/claude-home/` as their default working directory (when no channel-specific working dir is configured). This lets EARL have its own project-level `CLAUDE.md` without polluting other repos. Claude uses the real `$HOME` for global config and credentials. Override with `EARL_CLAUDE_HOME` env var.
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
exe/
|
|
83
|
+
earl # Entry point
|
|
84
|
+
earl-install # Setup script: config dirs, prod clone, ~/bin/earl wrapper
|
|
85
|
+
earl-permission-server # MCP permission server (spawned by Claude CLI as subprocess)
|
|
86
|
+
bin/
|
|
87
|
+
ci # CI pipeline runner
|
|
88
|
+
claude-context # Context window usage helper (spawned by !context command)
|
|
89
|
+
claude-usage # Claude usage helper (spawned by !usage command)
|
|
90
|
+
coverage # Coverage report generator
|
|
91
|
+
~/bin/earl # Production wrapper (generated by exe/earl-install)
|
|
92
|
+
lib/
|
|
93
|
+
earl.rb # Module root, requires, shared logger, env/config_root
|
|
94
|
+
earl/
|
|
95
|
+
version.rb # Earl::VERSION
|
|
96
|
+
config.rb # ENV-based configuration
|
|
97
|
+
logging.rb # Shared logging mixin
|
|
98
|
+
formatting.rb # Shared number formatting helpers
|
|
99
|
+
permission_config.rb # Shared permission env builder
|
|
100
|
+
tool_input_formatter.rb # Shared tool display formatting
|
|
101
|
+
mattermost.rb # WebSocket + REST API client
|
|
102
|
+
mattermost/api_client.rb # HTTP client with retry logic
|
|
103
|
+
claude_session.rb # Single Claude CLI process wrapper
|
|
104
|
+
claude_session/
|
|
105
|
+
stats.rb # Usage statistics tracking (Struct)
|
|
106
|
+
session_manager.rb # Maps thread IDs -> Claude sessions
|
|
107
|
+
session_manager/
|
|
108
|
+
persistence.rb # Session pause/resume persistence
|
|
109
|
+
session_creation.rb # Session creation and resume logic
|
|
110
|
+
session_store.rb # Persists session metadata to disk
|
|
111
|
+
streaming_response.rb # Mattermost post lifecycle (create/update/debounce)
|
|
112
|
+
message_queue.rb # Per-thread message queuing for busy sessions
|
|
113
|
+
command_parser.rb # Parses !commands from message text
|
|
114
|
+
command_executor.rb # Executes !help, !stats, !stop, !kill, !escape, !compact, !cd, !permissions, !heartbeats, !usage, !context, !sessions, !session, !update, !restart, !spawn
|
|
115
|
+
command_executor/
|
|
116
|
+
constants.rb # Help table, dispatch map, script paths
|
|
117
|
+
lifecycle_handler.rb # !restart, !update handlers
|
|
118
|
+
heartbeat_display.rb # !heartbeats display formatting
|
|
119
|
+
session_handler.rb # !sessions, !session subcommand handlers
|
|
120
|
+
spawn_handler.rb # !spawn handler
|
|
121
|
+
stats_formatter.rb # !stats display formatting
|
|
122
|
+
usage_handler.rb # !usage, !context handlers
|
|
123
|
+
question_handler.rb # AskUserQuestion tool -> emoji reaction flow
|
|
124
|
+
question_handler/
|
|
125
|
+
question_posting.rb # Question post creation and cleanup
|
|
126
|
+
runner.rb # Main event loop, wires everything together
|
|
127
|
+
runner/
|
|
128
|
+
idle_management.rb # Idle session detection and cleanup
|
|
129
|
+
lifecycle.rb # Startup, shutdown, restart logic
|
|
130
|
+
message_handling.rb # Incoming message processing
|
|
131
|
+
reaction_handling.rb # Emoji reaction event processing
|
|
132
|
+
response_lifecycle.rb # Claude response streaming callbacks
|
|
133
|
+
service_builder.rb # Dependency construction
|
|
134
|
+
startup.rb # Channel resolution, initial logging
|
|
135
|
+
thread_context_builder.rb # Thread transcript for new sessions
|
|
136
|
+
cron_parser.rb # Minimal 5-field cron expression parser
|
|
137
|
+
heartbeat_config.rb # Loads heartbeat definitions from YAML
|
|
138
|
+
heartbeat_scheduler.rb # Runs heartbeat tasks on cron/interval/one-shot schedules; auto-reloads config
|
|
139
|
+
heartbeat_scheduler/
|
|
140
|
+
config_reloading.rb # Auto-reload config on file change
|
|
141
|
+
execution.rb # Heartbeat task execution
|
|
142
|
+
heartbeat_state.rb # Per-heartbeat mutable state (Data.define)
|
|
143
|
+
lifecycle.rb # Start/stop/pause/resume lifecycle
|
|
144
|
+
tmux.rb # Tmux shell wrapper (list sessions/panes, capture, send-keys, wait-for-text)
|
|
145
|
+
tmux/
|
|
146
|
+
parsing.rb # Tmux output parsing helpers
|
|
147
|
+
processes.rb # Process detection on TTYs
|
|
148
|
+
sessions.rb # Session/pane listing
|
|
149
|
+
tmux_session_store.rb # JSON persistence for tmux session metadata
|
|
150
|
+
tmux_monitor.rb # Background poller: detects questions/permissions in tmux panes, forwards via Mattermost reactions
|
|
151
|
+
tmux_monitor/
|
|
152
|
+
alert_dispatcher.rb # Mattermost alert posting
|
|
153
|
+
output_analyzer.rb # Pane output state detection
|
|
154
|
+
permission_forwarder.rb # Permission prompt forwarding
|
|
155
|
+
question_forwarder.rb # Question prompt forwarding
|
|
156
|
+
safari_automation.rb # Safari AppleScript automation for GitHub PAT creation
|
|
157
|
+
mcp/
|
|
158
|
+
config.rb # MCP server ENV-based config
|
|
159
|
+
handler_base.rb # Base class for MCP tool handlers
|
|
160
|
+
server.rb # JSON-RPC 2.0 MCP server over stdio
|
|
161
|
+
approval_handler.rb # Permission approval via Mattermost reactions
|
|
162
|
+
memory_handler.rb # save_memory / search_memory MCP tools
|
|
163
|
+
heartbeat_handler.rb # manage_heartbeat MCP tool (CRUD heartbeat schedules)
|
|
164
|
+
tmux_handler.rb # manage_tmux_sessions MCP tool (list, capture, approve, spawn, kill)
|
|
165
|
+
github_pat_handler.rb # GitHub PAT creation via Safari automation
|
|
166
|
+
memory/
|
|
167
|
+
store.rb # File I/O for persistent memory (markdown files)
|
|
168
|
+
prompt_builder.rb # Builds system prompt from memory store
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Message Flow
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
User posts in channel
|
|
175
|
+
-> Mattermost WebSocket 'posted' event
|
|
176
|
+
-> Runner checks allowlist
|
|
177
|
+
-> CommandParser checks for !commands
|
|
178
|
+
-> If command: CommandExecutor handles it (!help, !stats, !kill, !cd, !restart, !sessions, !session, !spawn, etc.)
|
|
179
|
+
-> If message: MessageQueue serializes per-thread
|
|
180
|
+
-> SessionManager gets/creates ClaudeSession for thread
|
|
181
|
+
-> Resumes from session store if available
|
|
182
|
+
-> Builds MCP config for permission approval
|
|
183
|
+
-> Injects memory context via --append-system-prompt
|
|
184
|
+
-> For new sessions in existing threads: fetches Mattermost thread transcript for context
|
|
185
|
+
-> session.send_message(text) writes JSON to Claude stdin
|
|
186
|
+
-> Claude stdout emits events (assistant, result, system)
|
|
187
|
+
-> on_text: StreamingResponse creates POST or debounced PUT
|
|
188
|
+
-> on_tool_use: StreamingResponse shows tool icon + detail
|
|
189
|
+
-> on_tool_use(AskUserQuestion): QuestionHandler posts options, waits for reaction
|
|
190
|
+
-> on_complete: final PUT with stats footer, process next queued message
|
|
191
|
+
-> User sees threaded reply in Mattermost
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Key Details
|
|
195
|
+
|
|
196
|
+
- **WebSocket events**: `data.post` is a nested JSON string requiring double-parse
|
|
197
|
+
- **Claude CLI**: spawned with `--input-format stream-json --output-format stream-json --verbose`
|
|
198
|
+
- **Permissions**: Default uses `--permission-prompt-tool mcp__earl__permission_prompt --mcp-config <path>` for interactive approval via Mattermost reactions. Set `EARL_SKIP_PERMISSIONS=true` for `--dangerously-skip-permissions`.
|
|
199
|
+
- **Streaming**: first text chunk creates a POST, subsequent chunks do PUT with 300ms debounce
|
|
200
|
+
- **Sessions**: follow-up messages in same thread reuse the same Claude process (same context window)
|
|
201
|
+
- **Session persistence**: sessions are saved to `~/.config/earl/sessions.json` and resumed on restart
|
|
202
|
+
- **Shutdown**: SIGINT/SIGTERM triggers graceful shutdown: stops background services, pauses all sessions, then exits.
|
|
203
|
+
- **Restart**: `!restart` (Mattermost), `SIGHUP` (signal), or `earl restart` (CLI). In prod, runs `git pull --ff-only` first (non-fatal on failure). Pauses sessions, then `Kernel.exec` replaces the process in-place. Sessions resume on boot. PID file at `<config_root>/earl.pid` enables CLI restart.
|
|
204
|
+
- **Memory**: Persistent facts stored as markdown in `~/.config/earl/memory/`. Injected into Claude sessions via `--append-system-prompt`. Claude can save/search via MCP tools.
|
|
205
|
+
- **Heartbeats**: Scheduled tasks (cron/interval/one-shot via `run_at`) that spawn Claude sessions, posting results to configured channels. One-off tasks (`once: true`) auto-disable after execution. Config auto-reloads on file change. Claude can manage schedules via the `manage_heartbeat` MCP tool.
|
|
206
|
+
- **Tmux MCP tool**: `manage_tmux_sessions` tool exposes tmux session control to spawned Claude sessions. Actions: list, capture, status, approve, deny, send_input, spawn (requires Mattermost confirmation), kill.
|
|
207
|
+
- **Tmux Session Supervisor**: Mattermost becomes a control plane for all running Claude sessions (both EARL-managed and standalone tmux-based). `!sessions` lists all tmux panes running Claude with per-pane status. Detection uses `list_all_panes` + `claude_on_tty?` (ps-based TTY check). `!session <name> approve/deny` remotely handles Claude CLI permission dialogs. `!session <name> status` shows AI-summarized state. `!spawn "prompt"` creates new Claude sessions in tmux. TmuxMonitor runs a background poller that detects questions and permission prompts in tmux panes and forwards them to Mattermost for reaction-based handling.
|
|
208
|
+
- **Thread context**: When a Claude session is first created for a thread that already has messages (e.g., from `!` commands and EARL replies), the Mattermost thread transcript (up to 20 posts) is prepended so Claude has context for follow-up messages.
|
|
209
|
+
|
|
210
|
+
## Testing with Mattermost MCP
|
|
211
|
+
|
|
212
|
+
A Mattermost MCP server can be configured in `.mcp.json` (gitignored) for integration testing while EARL is running.
|
|
213
|
+
|
|
214
|
+
**Available tools:** `mcp__mattermost__send_message`, `mcp__mattermost__get_channel_messages`, `mcp__mattermost__search_messages`, `mcp__mattermost__list_channels`, etc.
|
|
215
|
+
|
|
216
|
+
**Example workflow — send a message and check EARL's reply:**
|
|
217
|
+
```
|
|
218
|
+
# Send a message to EARL (starts a new thread)
|
|
219
|
+
mcp__mattermost__send_message(channel_id: "<your-channel-id>", message: "Hello EARL")
|
|
220
|
+
|
|
221
|
+
# Or reply in an existing thread
|
|
222
|
+
mcp__mattermost__send_message(channel_id: "<your-channel-id>", message: "!usage", reply_to: "<root_post_id>")
|
|
223
|
+
|
|
224
|
+
# Read recent messages to see EARL's response
|
|
225
|
+
mcp__mattermost__get_channel_messages(channel_id: "<your-channel-id>", limit: 5)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Development Commands
|
|
229
|
+
|
|
230
|
+
- `bin/ci` — Run full CI pipeline (7 steps: RuboCop, Reek, Bundler audit, Semgrep, Actionlint, Minitest, Coverage)
|
|
231
|
+
- `rubocop` — Ruby style checking
|
|
232
|
+
- `rubocop -A` — Auto-fix style violations
|
|
233
|
+
- `bundle exec rake test` — Run test suite
|
|
234
|
+
- `semgrep --config=r/ruby --metrics=off lib/` — Run Semgrep security scan manually
|
|
235
|
+
|
|
236
|
+
## Code Quality
|
|
237
|
+
|
|
238
|
+
This project uses **vanilla RuboCop, Reek, and Semgrep** with minimal global configuration. Do not:
|
|
239
|
+
|
|
240
|
+
- Add `# rubocop:disable` inline comments — fix the code instead
|
|
241
|
+
- Add `# :reek:` inline annotations — refactor to eliminate the smell
|
|
242
|
+
- Add per-class or per-method exclusions to `.rubocop.yml` or `.reek.yml`
|
|
243
|
+
- Raise thresholds or disable detectors to work around warnings
|
|
244
|
+
- Use `# nosemgrep` unless the finding is a verified false positive (e.g., `Open3` with array-form arguments)
|
|
245
|
+
|
|
246
|
+
Global Reek overrides (in `.reek.yml`):
|
|
247
|
+
- `UtilityFunction: public_methods_only: true` — private helpers may operate on other objects
|
|
248
|
+
- `TooManyStatements: max_statements: 10` — raised from default 5
|
|
249
|
+
- `TooManyMethods: max_methods: 20` — raised from default 15
|
|
250
|
+
|
|
251
|
+
If a linter flags something, refactor the code to satisfy it. Common Reek fixes:
|
|
252
|
+
- **FeatureEnvy**: Extract accessed fields into locals, use `values_at`, or move logic onto the data object
|
|
253
|
+
- **TooManyStatements**: Extract helper methods to stay under 10 statements
|
|
254
|
+
- **DuplicateMethodCall**: Extract repeated calls into a local variable
|
|
255
|
+
- **ControlParameter**: Replace with predicates, hash dispatch, or polymorphism
|
|
256
|
+
- **DataClump**: Bundle traveling parameters into Structs or Data.define objects
|
|
257
|
+
|
|
258
|
+
## Commit Messages
|
|
259
|
+
|
|
260
|
+
This project follows [Conventional Commits](https://www.conventionalcommits.org/) specification.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
earl-bot (0.1.0)
|
|
5
|
+
websocket-client-simple (~> 0.9)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
ast (2.4.3)
|
|
11
|
+
base64 (0.3.0)
|
|
12
|
+
bigdecimal (4.0.1)
|
|
13
|
+
bundler-audit (0.9.3)
|
|
14
|
+
bundler (>= 1.2.0)
|
|
15
|
+
thor (~> 1.0)
|
|
16
|
+
childprocess (5.1.0)
|
|
17
|
+
logger (~> 1.5)
|
|
18
|
+
concurrent-ruby (1.3.6)
|
|
19
|
+
docile (1.4.1)
|
|
20
|
+
dry-configurable (1.3.0)
|
|
21
|
+
dry-core (~> 1.1)
|
|
22
|
+
zeitwerk (~> 2.6)
|
|
23
|
+
dry-core (1.2.0)
|
|
24
|
+
concurrent-ruby (~> 1.0)
|
|
25
|
+
logger
|
|
26
|
+
zeitwerk (~> 2.6)
|
|
27
|
+
dry-inflector (1.3.1)
|
|
28
|
+
dry-initializer (3.2.0)
|
|
29
|
+
dry-logic (1.6.0)
|
|
30
|
+
bigdecimal
|
|
31
|
+
concurrent-ruby (~> 1.0)
|
|
32
|
+
dry-core (~> 1.1)
|
|
33
|
+
zeitwerk (~> 2.6)
|
|
34
|
+
dry-schema (1.15.0)
|
|
35
|
+
concurrent-ruby (~> 1.0)
|
|
36
|
+
dry-configurable (~> 1.0, >= 1.0.1)
|
|
37
|
+
dry-core (~> 1.1)
|
|
38
|
+
dry-initializer (~> 3.2)
|
|
39
|
+
dry-logic (~> 1.6)
|
|
40
|
+
dry-types (~> 1.8)
|
|
41
|
+
zeitwerk (~> 2.6)
|
|
42
|
+
dry-types (1.9.1)
|
|
43
|
+
bigdecimal (>= 3.0)
|
|
44
|
+
concurrent-ruby (~> 1.0)
|
|
45
|
+
dry-core (~> 1.0)
|
|
46
|
+
dry-inflector (~> 1.0)
|
|
47
|
+
dry-logic (~> 1.4)
|
|
48
|
+
zeitwerk (~> 2.6)
|
|
49
|
+
event_emitter (0.2.6)
|
|
50
|
+
iniparse (1.5.0)
|
|
51
|
+
json (2.18.1)
|
|
52
|
+
language_server-protocol (3.17.0.5)
|
|
53
|
+
lint_roller (1.1.0)
|
|
54
|
+
logger (1.7.0)
|
|
55
|
+
minitest (5.27.0)
|
|
56
|
+
mutex_m (0.3.0)
|
|
57
|
+
overcommit (0.68.0)
|
|
58
|
+
childprocess (>= 0.6.3, < 6)
|
|
59
|
+
iniparse (~> 1.4)
|
|
60
|
+
rexml (>= 3.3.9)
|
|
61
|
+
parallel (1.27.0)
|
|
62
|
+
parser (3.3.10.2)
|
|
63
|
+
ast (~> 2.4.1)
|
|
64
|
+
racc
|
|
65
|
+
prism (1.9.0)
|
|
66
|
+
racc (1.8.1)
|
|
67
|
+
rainbow (3.1.1)
|
|
68
|
+
rake (13.3.1)
|
|
69
|
+
reek (6.5.0)
|
|
70
|
+
dry-schema (~> 1.13)
|
|
71
|
+
logger (~> 1.6)
|
|
72
|
+
parser (~> 3.3.0)
|
|
73
|
+
rainbow (>= 2.0, < 4.0)
|
|
74
|
+
rexml (~> 3.1)
|
|
75
|
+
regexp_parser (2.11.3)
|
|
76
|
+
rexml (3.4.4)
|
|
77
|
+
rubocop (1.84.2)
|
|
78
|
+
json (~> 2.3)
|
|
79
|
+
language_server-protocol (~> 3.17.0.2)
|
|
80
|
+
lint_roller (~> 1.1.0)
|
|
81
|
+
parallel (~> 1.10)
|
|
82
|
+
parser (>= 3.3.0.2)
|
|
83
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
84
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
85
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
86
|
+
ruby-progressbar (~> 1.7)
|
|
87
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
88
|
+
rubocop-ast (1.49.0)
|
|
89
|
+
parser (>= 3.3.7.2)
|
|
90
|
+
prism (~> 1.7)
|
|
91
|
+
rubocop-rake (0.7.1)
|
|
92
|
+
lint_roller (~> 1.1)
|
|
93
|
+
rubocop (>= 1.72.1)
|
|
94
|
+
ruby-progressbar (1.13.0)
|
|
95
|
+
simplecov (0.22.0)
|
|
96
|
+
docile (~> 1.1)
|
|
97
|
+
simplecov-html (~> 0.11)
|
|
98
|
+
simplecov_json_formatter (~> 0.1)
|
|
99
|
+
simplecov-html (0.13.2)
|
|
100
|
+
simplecov_json_formatter (0.1.4)
|
|
101
|
+
thor (1.5.0)
|
|
102
|
+
unicode-display_width (3.2.0)
|
|
103
|
+
unicode-emoji (~> 4.1)
|
|
104
|
+
unicode-emoji (4.2.0)
|
|
105
|
+
websocket (1.2.11)
|
|
106
|
+
websocket-client-simple (0.9.0)
|
|
107
|
+
base64
|
|
108
|
+
event_emitter
|
|
109
|
+
mutex_m
|
|
110
|
+
websocket
|
|
111
|
+
zeitwerk (2.7.5)
|
|
112
|
+
|
|
113
|
+
PLATFORMS
|
|
114
|
+
arm64-darwin-25
|
|
115
|
+
ruby
|
|
116
|
+
|
|
117
|
+
DEPENDENCIES
|
|
118
|
+
bundler-audit (~> 0.9)
|
|
119
|
+
earl-bot!
|
|
120
|
+
minitest (~> 5.0)
|
|
121
|
+
overcommit
|
|
122
|
+
rake (~> 13.0)
|
|
123
|
+
reek (~> 6.0)
|
|
124
|
+
rubocop (~> 1.0)
|
|
125
|
+
rubocop-rake (~> 0.7)
|
|
126
|
+
simplecov (~> 0.22)
|
|
127
|
+
|
|
128
|
+
CHECKSUMS
|
|
129
|
+
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
130
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
131
|
+
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
|
132
|
+
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
|
|
133
|
+
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
|
|
134
|
+
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
135
|
+
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
136
|
+
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
|
|
137
|
+
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
|
|
138
|
+
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
|
|
139
|
+
dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3
|
|
140
|
+
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
|
|
141
|
+
dry-schema (1.15.0) sha256=0f2a34adba4206bd6d46ec1b6b7691b402e198eecaff1d8349a7d48a77d82cd2
|
|
142
|
+
dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
|
|
143
|
+
earl-bot (0.1.0)
|
|
144
|
+
event_emitter (0.2.6) sha256=c72697bd5cce9d36594be1972c17f1c9a573236f44303a4d1d548080364e1391
|
|
145
|
+
iniparse (1.5.0) sha256=36a165e98d8a250b7631c4a7f9afba32af78f089970cd6446a39771189c761f1
|
|
146
|
+
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
|
|
147
|
+
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
148
|
+
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
149
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
150
|
+
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
|
|
151
|
+
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
|
|
152
|
+
overcommit (0.68.0) sha256=bfbcd26388e024e10a3d720f03077bc9389fe7c3beac360263b6c77926bcc8d0
|
|
153
|
+
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
|
154
|
+
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
|
|
155
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
156
|
+
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
157
|
+
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
158
|
+
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
|
159
|
+
reek (6.5.0) sha256=d26d3a492773b2bbc228888067a21afe33ac07954a17dbd64cdeae42c4c69be1
|
|
160
|
+
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
|
161
|
+
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
162
|
+
rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
|
|
163
|
+
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
|
|
164
|
+
rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
|
|
165
|
+
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
166
|
+
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
|
167
|
+
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
|
168
|
+
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
|
169
|
+
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
|
170
|
+
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
171
|
+
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
172
|
+
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
|
|
173
|
+
websocket-client-simple (0.9.0) sha256=f9a37c5e4922b35a711e21e6d73ed1e25892efa47d183203ab2f5beb4e563109
|
|
174
|
+
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
|
|
175
|
+
|
|
176
|
+
BUNDLED WITH
|
|
177
|
+
4.0.3
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Eric Boehs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# EARL
|
|
2
|
+
|
|
3
|
+
**Eric's Automated Response Line** — a Ruby CLI bot that connects to Mattermost via WebSocket, listens for messages in configured channels, spawns Claude Code CLI sessions, and streams responses back as threaded replies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install earl-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "earl-bot"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Configure environment
|
|
21
|
+
cp .envrc.example .envrc # or use ~/.config/earl/env
|
|
22
|
+
# Fill in MATTERMOST_URL, MATTERMOST_BOT_TOKEN, etc.
|
|
23
|
+
|
|
24
|
+
# Run directly
|
|
25
|
+
earl
|
|
26
|
+
|
|
27
|
+
# Or install dev + prod environments
|
|
28
|
+
earl-install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Running as a Service
|
|
32
|
+
|
|
33
|
+
EARL can run as a persistent service for automatic startup and crash recovery.
|
|
34
|
+
|
|
35
|
+
**Prerequisite:** [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) must be installed and authenticated.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
earl-install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
On first run this creates `~/.config/earl/env` — fill in your secrets and re-run. On subsequent runs it sets up config dirs, clones the prod repo, and creates a wrapper script.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Streaming responses** — first text chunk creates a post, subsequent chunks update via debounced PUT
|
|
46
|
+
- **Session persistence** — follow-up messages in the same thread reuse the same Claude context window; sessions survive restarts
|
|
47
|
+
- **Permission approval** — tool use is gated by Mattermost emoji reactions (or skip with `EARL_SKIP_PERMISSIONS=true`)
|
|
48
|
+
- **Memory** — persistent facts stored as markdown, injected into Claude sessions and manageable via MCP tools
|
|
49
|
+
- **Heartbeats** — scheduled tasks (cron/interval/one-shot) that spawn Claude sessions on a schedule
|
|
50
|
+
- **Tmux supervision** — Mattermost becomes a control plane for all running Claude sessions (EARL-managed and standalone)
|
|
51
|
+
- **Claude HOME isolation** — EARL's Claude sessions use an isolated config directory, separate from the user's personal `~/.claude/`
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
### Required Environment Variables
|
|
56
|
+
|
|
57
|
+
| Variable | Description |
|
|
58
|
+
|----------|-------------|
|
|
59
|
+
| `MATTERMOST_URL` | Mattermost server URL |
|
|
60
|
+
| `MATTERMOST_BOT_TOKEN` | Bot authentication token |
|
|
61
|
+
| `MATTERMOST_BOT_ID` | Bot user ID (to ignore own messages) |
|
|
62
|
+
| `EARL_CHANNEL_ID` | Default channel to listen in |
|
|
63
|
+
| `EARL_ALLOWED_USERS` | Comma-separated usernames allowed to interact |
|
|
64
|
+
|
|
65
|
+
### Optional
|
|
66
|
+
|
|
67
|
+
| Variable | Description |
|
|
68
|
+
|----------|-------------|
|
|
69
|
+
| `EARL_MODEL` | Override Claude model (e.g., `sonnet`, `opus`, `haiku`) |
|
|
70
|
+
| `EARL_CHANNELS` | Multi-channel config (`channel_id:/working/dir` pairs) |
|
|
71
|
+
| `EARL_SKIP_PERMISSIONS` | Set to `true` to skip permission prompts |
|
|
72
|
+
| `EARL_CLAUDE_HOME` | Custom HOME for Claude subprocesses (default: `~/.config/earl/claude-home`) |
|
|
73
|
+
| `EARL_DEBUG` | Enable debug logging |
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
Users can send commands in Mattermost messages:
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `!help` | Show available commands |
|
|
82
|
+
| `!stats` | Show session statistics |
|
|
83
|
+
| `!stop` | Stop current response |
|
|
84
|
+
| `!kill` | Kill Claude session for this thread |
|
|
85
|
+
| `!compact` | Compact the session context |
|
|
86
|
+
| `!cd <path>` | Change working directory |
|
|
87
|
+
| `!sessions` | List all Claude sessions (EARL + tmux) |
|
|
88
|
+
| `!session <name> status/approve/deny` | Manage tmux sessions |
|
|
89
|
+
| `!spawn "prompt"` | Spawn a new Claude session in tmux |
|
|
90
|
+
| `!usage` | Show Claude usage |
|
|
91
|
+
| `!context` | Show context window usage |
|
|
92
|
+
| `!heartbeats` | List scheduled heartbeats |
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bin/ci # Full CI pipeline (rubocop + reek + tests + coverage)
|
|
98
|
+
rubocop -A # Auto-fix style violations
|
|
99
|
+
bundle exec rake test # Run test suite
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
See [CLAUDE.md](CLAUDE.md) for detailed architecture documentation.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
This project is licensed under the [MIT License](LICENSE).
|
data/Rakefile
ADDED
data/bin/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Developer Tools
|
|
2
|
+
|
|
3
|
+
This directory contains scripts to help developers and AI assistants maintain code quality and run tests efficiently.
|
|
4
|
+
|
|
5
|
+
## Key Scripts
|
|
6
|
+
|
|
7
|
+
### `ci`
|
|
8
|
+
Comprehensive CI pipeline that runs all checks and tests in sequence:
|
|
9
|
+
RuboCop, Reek, Bundler audit, Semgrep, Actionlint, Minitest, and coverage reporting.
|
|
10
|
+
|
|
11
|
+
### `coverage`
|
|
12
|
+
Generates test coverage reports to help identify untested code.
|
|
13
|
+
|
|
14
|
+
### `watch-ci`
|
|
15
|
+
Monitors GitHub Actions CI status for the current branch, exiting on pass or fail.
|
|
16
|
+
|
|
17
|
+
### `rubocop`
|
|
18
|
+
Wrapper that explicitly sets the config path for consistent behavior.
|
|
19
|
+
|
|
20
|
+
### `claude-context` / `claude-usage`
|
|
21
|
+
Helpers spawned by `!context` and `!usage` bot commands.
|