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.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +40 -0
  4. data/CLAUDE.md +260 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +177 -0
  7. data/LICENSE +21 -0
  8. data/README.md +106 -0
  9. data/Rakefile +11 -0
  10. data/bin/README.md +21 -0
  11. data/bin/ci +49 -0
  12. data/bin/claude-context +155 -0
  13. data/bin/claude-usage +110 -0
  14. data/bin/coverage +221 -0
  15. data/bin/rubocop +10 -0
  16. data/bin/watch-ci +198 -0
  17. data/config/earl-claude-home/.claude/CLAUDE.md +10 -0
  18. data/config/earl-claude-home/.claude/settings.json +34 -0
  19. data/earl-bot.gemspec +42 -0
  20. data/exe/earl +51 -0
  21. data/exe/earl-install +129 -0
  22. data/exe/earl-permission-server +39 -0
  23. data/lib/earl/claude_session/stats.rb +76 -0
  24. data/lib/earl/claude_session.rb +468 -0
  25. data/lib/earl/command_executor/constants.rb +53 -0
  26. data/lib/earl/command_executor/heartbeat_display.rb +54 -0
  27. data/lib/earl/command_executor/lifecycle_handler.rb +61 -0
  28. data/lib/earl/command_executor/session_handler.rb +126 -0
  29. data/lib/earl/command_executor/spawn_handler.rb +99 -0
  30. data/lib/earl/command_executor/stats_formatter.rb +66 -0
  31. data/lib/earl/command_executor/usage_handler.rb +132 -0
  32. data/lib/earl/command_executor.rb +128 -0
  33. data/lib/earl/command_parser.rb +57 -0
  34. data/lib/earl/config.rb +94 -0
  35. data/lib/earl/cron_parser.rb +105 -0
  36. data/lib/earl/formatting.rb +14 -0
  37. data/lib/earl/heartbeat_config.rb +101 -0
  38. data/lib/earl/heartbeat_scheduler/config_reloading.rb +64 -0
  39. data/lib/earl/heartbeat_scheduler/execution.rb +105 -0
  40. data/lib/earl/heartbeat_scheduler/heartbeat_state.rb +41 -0
  41. data/lib/earl/heartbeat_scheduler/lifecycle.rb +75 -0
  42. data/lib/earl/heartbeat_scheduler.rb +131 -0
  43. data/lib/earl/logging.rb +12 -0
  44. data/lib/earl/mattermost/api_client.rb +85 -0
  45. data/lib/earl/mattermost.rb +261 -0
  46. data/lib/earl/mcp/approval_handler.rb +304 -0
  47. data/lib/earl/mcp/config.rb +62 -0
  48. data/lib/earl/mcp/github_pat_handler.rb +450 -0
  49. data/lib/earl/mcp/handler_base.rb +13 -0
  50. data/lib/earl/mcp/heartbeat_handler.rb +310 -0
  51. data/lib/earl/mcp/memory_handler.rb +89 -0
  52. data/lib/earl/mcp/server.rb +123 -0
  53. data/lib/earl/mcp/tmux_handler.rb +562 -0
  54. data/lib/earl/memory/prompt_builder.rb +40 -0
  55. data/lib/earl/memory/store.rb +125 -0
  56. data/lib/earl/message_queue.rb +56 -0
  57. data/lib/earl/permission_config.rb +22 -0
  58. data/lib/earl/question_handler/question_posting.rb +58 -0
  59. data/lib/earl/question_handler.rb +116 -0
  60. data/lib/earl/runner/idle_management.rb +44 -0
  61. data/lib/earl/runner/lifecycle.rb +73 -0
  62. data/lib/earl/runner/message_handling.rb +121 -0
  63. data/lib/earl/runner/reaction_handling.rb +42 -0
  64. data/lib/earl/runner/response_lifecycle.rb +96 -0
  65. data/lib/earl/runner/service_builder.rb +48 -0
  66. data/lib/earl/runner/startup.rb +73 -0
  67. data/lib/earl/runner/thread_context_builder.rb +43 -0
  68. data/lib/earl/runner.rb +70 -0
  69. data/lib/earl/safari_automation.rb +497 -0
  70. data/lib/earl/session_manager/persistence.rb +46 -0
  71. data/lib/earl/session_manager/session_creation.rb +108 -0
  72. data/lib/earl/session_manager.rb +92 -0
  73. data/lib/earl/session_store.rb +84 -0
  74. data/lib/earl/streaming_response.rb +219 -0
  75. data/lib/earl/tmux/parsing.rb +80 -0
  76. data/lib/earl/tmux/processes.rb +34 -0
  77. data/lib/earl/tmux/sessions.rb +41 -0
  78. data/lib/earl/tmux.rb +122 -0
  79. data/lib/earl/tmux_monitor/alert_dispatcher.rb +53 -0
  80. data/lib/earl/tmux_monitor/output_analyzer.rb +35 -0
  81. data/lib/earl/tmux_monitor/permission_forwarder.rb +80 -0
  82. data/lib/earl/tmux_monitor/question_forwarder.rb +124 -0
  83. data/lib/earl/tmux_monitor.rb +249 -0
  84. data/lib/earl/tmux_session_store.rb +133 -0
  85. data/lib/earl/tool_input_formatter.rb +44 -0
  86. data/lib/earl/version.rb +5 -0
  87. data/lib/earl.rb +87 -0
  88. data/lib/tasks/.keep +1 -0
  89. 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem "overcommit", require: false
9
+ end
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task default: :test
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.