feat: openclaw-style secrets (env.vars + \) and per-task model routing
- Replace python-dotenv with config.json env.vars block + \ substitution - Add models section for per-task model routing (heartbeat, subagent, default) - Heartbeat/subagent tasks can use different models/providers than main chat - Remove python-dotenv from dependencies - Update all docs to reflect new config approach - Reorganize docs into project/ and research/ subdirectories
This commit is contained in:
979
docs/project/features-guide.md
Normal file
979
docs/project/features-guide.md
Normal file
@@ -0,0 +1,979 @@
|
||||
# Aetheel Features Guide
|
||||
|
||||
Complete reference for all Aetheel features, how to use them, and how to test them.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Dual AI Runtimes](#1-dual-ai-runtimes)
|
||||
2. [Tool Enablement & MCP Servers](#2-tool-enablement--mcp-servers)
|
||||
3. [Multi-Channel Adapters](#3-multi-channel-adapters)
|
||||
4. [WebChat Browser Interface](#4-webchat-browser-interface)
|
||||
5. [Persistent Memory System](#5-persistent-memory-system)
|
||||
6. [Skills System](#6-skills-system)
|
||||
7. [Scheduler & Action Tags](#7-scheduler--action-tags)
|
||||
8. [Heartbeat / Proactive System](#8-heartbeat--proactive-system)
|
||||
9. [Subagents & Agent-to-Agent Communication](#9-subagents--agent-to-agent-communication)
|
||||
10. [Self-Modification](#10-self-modification)
|
||||
11. [Lifecycle Hooks](#11-lifecycle-hooks)
|
||||
12. [Webhooks (External Event Receiver)](#12-webhooks-external-event-receiver)
|
||||
13. [CLI Interface](#13-cli-interface)
|
||||
14. [Configuration](#14-configuration)
|
||||
15. [Running Tests](#15-running-tests)
|
||||
13. [Running Tests](#13-running-tests)
|
||||
|
||||
---
|
||||
|
||||
## 1. Dual AI Runtimes
|
||||
|
||||
Aetheel supports two AI backends that share the same `AgentResponse` interface. Switch between them with a single flag.
|
||||
|
||||
### OpenCode (default)
|
||||
|
||||
Uses the [OpenCode](https://opencode.ai) CLI. Supports CLI mode (subprocess per request) and SDK mode (persistent server connection).
|
||||
|
||||
```bash
|
||||
# CLI mode (default)
|
||||
python main.py
|
||||
|
||||
# SDK mode (requires `opencode serve` running)
|
||||
python main.py --sdk
|
||||
|
||||
# Custom model
|
||||
python main.py --model anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
### OpenCode Advanced Features
|
||||
|
||||
Aetheel exposes several OpenCode CLI features via chat commands and config:
|
||||
|
||||
| Feature | Chat Command | Config Key |
|
||||
|---------|-------------|------------|
|
||||
| Agent selection | `agents` (list), config to set | `runtime.agent` |
|
||||
| Attach to server | — | `runtime.attach` |
|
||||
| Model discovery | `models`, `models <provider>` | — |
|
||||
| Usage stats | `stats`, `stats <days>` | — |
|
||||
| File attachments | Passed from chat adapters | — |
|
||||
| Session forking | Internal (subagent branching) | — |
|
||||
| Session titles | Auto-set from first message | — |
|
||||
|
||||
Setting `runtime.attach` to a running `opencode serve` URL (e.g. `"http://localhost:4096"`) makes CLI mode attach to that server instead of spawning a fresh process per request. This avoids MCP server cold boot times and is significantly faster.
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": {
|
||||
"agent": "researcher",
|
||||
"attach": "http://localhost:4096"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code
|
||||
|
||||
Uses the [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI with native `--system-prompt` support.
|
||||
|
||||
```bash
|
||||
python main.py --claude
|
||||
python main.py --claude --model claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
### Key differences
|
||||
|
||||
| Feature | OpenCode | Claude Code |
|
||||
|---------|----------|-------------|
|
||||
| System prompt | XML-injected into message | Native `--system-prompt` flag |
|
||||
| Session continuity | `--continue --session <id>` | `--continue --session-id <id>` |
|
||||
| Tool access | Enabled by default | Controlled via `--allowedTools` |
|
||||
| Output format | JSONL events | JSON result object |
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Echo mode (no AI runtime needed)
|
||||
python main.py --test
|
||||
|
||||
# Verify runtime detection
|
||||
python cli.py doctor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Tool Enablement & MCP Servers
|
||||
|
||||
### Tool access
|
||||
|
||||
Claude Code runtime now has tools enabled by default. The `allowed_tools` list controls which tools the agent can use:
|
||||
|
||||
```
|
||||
Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch,
|
||||
Task, TaskOutput, TaskStop, Skill, TeamCreate, TeamDelete, SendMessage
|
||||
```
|
||||
|
||||
To disable tools (pure conversation mode), set in `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"claude": {
|
||||
"no_tools": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To customize the tool list:
|
||||
|
||||
```json
|
||||
{
|
||||
"claude": {
|
||||
"no_tools": false,
|
||||
"allowed_tools": ["Bash", "Read", "Write", "WebSearch"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP server configuration
|
||||
|
||||
Add external tool servers via config. Aetheel writes the appropriate config file (`.mcp.json` for Claude, `opencode.json` for OpenCode) to the workspace before launching the runtime.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"my-tool": {
|
||||
"command": "uvx",
|
||||
"args": ["my-mcp-server@latest"],
|
||||
"env": { "API_KEY": "..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Run MCP config writer tests
|
||||
cd Aetheel
|
||||
python -m pytest tests/test_mcp_config.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Multi-Channel Adapters
|
||||
|
||||
Aetheel connects to messaging platforms via adapters. Each adapter converts platform events into a channel-agnostic `IncomingMessage` and routes responses back.
|
||||
|
||||
### Slack (default)
|
||||
|
||||
Requires `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` in `config.json` → `env.vars`. Starts automatically when tokens are present.
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Telegram
|
||||
|
||||
```bash
|
||||
# Set TELEGRAM_BOT_TOKEN in config.json env.vars first
|
||||
python main.py --telegram
|
||||
```
|
||||
|
||||
### Discord
|
||||
|
||||
```bash
|
||||
# Set DISCORD_BOT_TOKEN in config.json env.vars first
|
||||
python main.py --discord
|
||||
```
|
||||
|
||||
### Multiple adapters
|
||||
|
||||
```bash
|
||||
python main.py --telegram --discord --webchat
|
||||
```
|
||||
|
||||
When multiple adapters run, all but the last start in background threads. The last one runs blocking.
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_base_adapter.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. WebChat Browser Interface
|
||||
|
||||
A browser-based chat UI served via aiohttp with WebSocket support. No Slack/Discord/Telegram needed.
|
||||
|
||||
### Starting WebChat
|
||||
|
||||
```bash
|
||||
# Via CLI flag
|
||||
python main.py --webchat
|
||||
|
||||
# Or enable in config permanently
|
||||
# Set "webchat": {"enabled": true} in ~/.aetheel/config.json
|
||||
```
|
||||
|
||||
Then open `http://127.0.0.1:8080` in your browser.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"webchat": {
|
||||
"enabled": false,
|
||||
"port": 8080,
|
||||
"host": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- Dark-themed chat UI at `GET /`
|
||||
- WebSocket endpoint at `/ws` for real-time messaging
|
||||
- Per-connection session isolation (each browser tab gets its own conversation)
|
||||
- Auto-reconnect on disconnect (3 second delay)
|
||||
- Basic markdown rendering (bold, code blocks, bullet points)
|
||||
- Connection status indicator (green/red/gray)
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Browser <--> WebSocket (/ws) <--> WebChatAdapter <--> ai_handler <--> Runtime
|
||||
|
|
||||
GET / serves static/chat.html
|
||||
```
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Start with echo handler + webchat
|
||||
python main.py --test --webchat
|
||||
|
||||
# Open http://127.0.0.1:8080 and send a message
|
||||
# You should see the echo response with message metadata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Persistent Memory System
|
||||
|
||||
Aetheel maintains persistent memory across sessions using local embeddings and SQLite.
|
||||
|
||||
### Identity files
|
||||
|
||||
Located in `~/.aetheel/workspace/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SOUL.md` | Personality, values, communication style |
|
||||
| `USER.md` | User preferences, context, background |
|
||||
| `MEMORY.md` | Long-term notes, facts to remember |
|
||||
|
||||
Edit these files directly. Changes are picked up automatically via file watching.
|
||||
|
||||
### How it works
|
||||
|
||||
1. All `.md` files in the workspace are chunked and embedded using `fastembed` (BAAI/bge-small-en-v1.5, 384-dim, runs locally)
|
||||
2. Chunks are stored in SQLite with FTS5 full-text search
|
||||
3. On each message, Aetheel searches memory using hybrid scoring (0.7 vector + 0.3 BM25)
|
||||
4. Relevant results are injected into the system prompt as context
|
||||
5. Conversations are logged to `daily/YYYY-MM-DD.md`
|
||||
|
||||
### CLI memory commands
|
||||
|
||||
```bash
|
||||
# Search memory
|
||||
python cli.py memory search "python projects"
|
||||
|
||||
# Force re-index
|
||||
python cli.py memory sync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Skills System
|
||||
|
||||
Skills are markdown files that teach the agent how to handle specific types of requests. They're loaded at startup and injected into the system prompt when trigger words match.
|
||||
|
||||
### Creating a skill
|
||||
|
||||
Create `~/.aetheel/workspace/skills/<name>/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: weather
|
||||
description: Check weather for any city
|
||||
triggers: [weather, forecast, temperature, rain]
|
||||
---
|
||||
|
||||
# Weather Skill
|
||||
|
||||
When the user asks about weather, use web search to find current conditions...
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
1. Skills are discovered from `~/.aetheel/workspace/skills/*/SKILL.md`
|
||||
2. YAML frontmatter defines `name`, `description`, and `triggers`
|
||||
3. When a message contains a trigger word, the skill's body is injected into the system prompt
|
||||
4. A summary of all available skills is always included
|
||||
|
||||
### Hot reload
|
||||
|
||||
Send `reload` or `/reload` in chat to reload skills without restarting.
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_skills.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Scheduler & Action Tags
|
||||
|
||||
APScheduler-based system with SQLite persistence for one-shot and recurring jobs.
|
||||
|
||||
### Action tags
|
||||
|
||||
The AI can include action tags in its responses. The system strips them from the visible reply and executes the action.
|
||||
|
||||
| Tag | Effect |
|
||||
|-----|--------|
|
||||
| `[ACTION:remind\|5\|Drink water!]` | Sends "Drink water!" to the channel in 5 minutes |
|
||||
| `[ACTION:cron\|0 9 * * *\|Good morning!]` | Sends "Good morning!" every day at 9 AM |
|
||||
| `[ACTION:spawn\|Research Python 3.14]` | Spawns a background subagent for the task |
|
||||
|
||||
### Managing jobs
|
||||
|
||||
In chat:
|
||||
- `/cron list` — list all scheduled jobs
|
||||
- `/cron remove <id>` — remove a job
|
||||
|
||||
Via CLI:
|
||||
```bash
|
||||
python cli.py cron list
|
||||
python cli.py cron remove <job_id>
|
||||
```
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Scheduler tests require apscheduler installed
|
||||
python -m pytest tests/test_scheduler.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Heartbeat / Proactive System
|
||||
|
||||
The heartbeat system runs periodic tasks automatically by parsing a user-editable `HEARTBEAT.md` file.
|
||||
|
||||
### How it works
|
||||
|
||||
1. At startup, `HeartbeatRunner` reads `~/.aetheel/workspace/HEARTBEAT.md`
|
||||
2. Each `## ` heading defines a schedule (natural language)
|
||||
3. Bullet points under each heading are task prompts
|
||||
4. Tasks are registered as cron jobs with the Scheduler
|
||||
5. When a task fires, it creates a synthetic message routed through `ai_handler`
|
||||
|
||||
### HEARTBEAT.md format
|
||||
|
||||
```markdown
|
||||
# Heartbeat Tasks
|
||||
|
||||
## Every 30 minutes
|
||||
- Check if any scheduled reminders need attention
|
||||
- Review recent session logs for anything worth remembering
|
||||
|
||||
## Every morning (9:00 AM)
|
||||
- Summarize yesterday's conversations
|
||||
- Check for any pending follow-ups in MEMORY.md
|
||||
|
||||
## Every evening (6:00 PM)
|
||||
- Update MEMORY.md with today's key learnings
|
||||
```
|
||||
|
||||
### Supported schedule patterns
|
||||
|
||||
| Pattern | Cron equivalent |
|
||||
|---------|----------------|
|
||||
| `Every 30 minutes` | `*/30 * * * *` |
|
||||
| `Every hour` | `0 * * * *` |
|
||||
| `Every 2 hours` | `0 */2 * * *` |
|
||||
| `Every morning (9:00 AM)` | `0 9 * * *` |
|
||||
| `Every evening (6:00 PM)` | `0 18 * * *` |
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"default_channel": "slack",
|
||||
"default_channel_id": "C123456",
|
||||
"silent": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `enabled` to `false` to disable heartbeat entirely. If `HEARTBEAT.md` doesn't exist, a default one is created automatically.
|
||||
|
||||
### Model routing for heartbeat
|
||||
|
||||
Heartbeat tasks can use a cheaper/local model to save costs. Configure in the `models` section:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"heartbeat": {
|
||||
"engine": "opencode",
|
||||
"model": "ollama/llama3.2",
|
||||
"provider": "ollama"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When set, heartbeat jobs use a dedicated runtime instance with the specified model instead of the global default. Regular chat messages are unaffected.
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Verify heartbeat parsing works
|
||||
python -c "
|
||||
from heartbeat.heartbeat import HeartbeatRunner
|
||||
print(HeartbeatRunner._parse_schedule_header('Every 30 minutes')) # */30 * * * *
|
||||
print(HeartbeatRunner._parse_schedule_header('Every morning (9:00 AM)')) # 0 9 * * *
|
||||
print(HeartbeatRunner._parse_schedule_header('Every evening (6:00 PM)')) # 0 18 * * *
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Subagents & Agent-to-Agent Communication
|
||||
|
||||
### Subagent spawning
|
||||
|
||||
The AI can spawn background tasks that run in separate threads with their own runtime instances.
|
||||
|
||||
```
|
||||
[ACTION:spawn|Research the latest Python 3.14 features and summarize them]
|
||||
```
|
||||
|
||||
The subagent runs asynchronously and sends results back to the originating channel when done.
|
||||
|
||||
### Managing subagents
|
||||
|
||||
In chat:
|
||||
- `/subagents` — list active subagent tasks with IDs and status
|
||||
|
||||
### SubagentBus
|
||||
|
||||
A thread-safe pub/sub message bus for inter-subagent communication:
|
||||
|
||||
```python
|
||||
from agent.subagent import SubagentManager
|
||||
|
||||
mgr = SubagentManager(runtime_factory=..., send_fn=...)
|
||||
|
||||
# Subscribe to a channel
|
||||
mgr.bus.subscribe("results", lambda msg, sender: print(f"{sender}: {msg}"))
|
||||
|
||||
# Publish from a subagent
|
||||
mgr.bus.publish("results", "Task complete!", "subagent-abc123")
|
||||
```
|
||||
|
||||
### Claude Code team tools
|
||||
|
||||
When using Claude Code runtime, the agent has access to team coordination tools:
|
||||
- `TeamCreate`, `TeamDelete` — create/delete agent teams
|
||||
- `SendMessage` — send messages between agents in a team
|
||||
- `Task`, `TaskOutput`, `TaskStop` — spawn and manage subagent tasks
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_subagent_bus.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Self-Modification
|
||||
|
||||
The AI agent knows it can modify its own files. The system prompt tells it about:
|
||||
|
||||
- `~/.aetheel/config.json` — edit configuration
|
||||
- `~/.aetheel/workspace/skills/<name>/SKILL.md` — create new skills
|
||||
- `~/.aetheel/workspace/SOUL.md` — update personality
|
||||
- `~/.aetheel/workspace/USER.md` — update user profile
|
||||
- `~/.aetheel/workspace/MEMORY.md` — update long-term memory
|
||||
- `~/.aetheel/workspace/HEARTBEAT.md` — modify periodic tasks
|
||||
|
||||
### Hot reload
|
||||
|
||||
After the agent edits config or skills, send `reload` or `/reload` in chat to apply changes without restarting:
|
||||
|
||||
```
|
||||
You: /reload
|
||||
Aetheel: 🔄 Config and skills reloaded.
|
||||
```
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Verify the system prompt contains self-modification instructions
|
||||
python -c "
|
||||
from agent.opencode_runtime import build_aetheel_system_prompt
|
||||
prompt = build_aetheel_system_prompt()
|
||||
assert 'Self-Modification' in prompt
|
||||
assert 'config.json' in prompt
|
||||
assert '/reload' in prompt
|
||||
print('Self-modification prompt sections present ✅')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Lifecycle Hooks
|
||||
|
||||
Event-driven lifecycle hooks inspired by OpenClaw's internal hook system. Hooks fire on gateway/agent lifecycle events and let you run custom Python code at those moments.
|
||||
|
||||
### Supported events
|
||||
|
||||
| Event | When it fires |
|
||||
|---|---|
|
||||
| `gateway:startup` | Gateway process starts (after adapters connect) |
|
||||
| `gateway:shutdown` | Gateway process is shutting down |
|
||||
| `command:reload` | User sends `/reload` |
|
||||
| `command:new` | User starts a fresh session |
|
||||
| `agent:bootstrap` | Before workspace files are injected into context |
|
||||
| `agent:response` | After the agent produces a response |
|
||||
|
||||
### Creating a hook
|
||||
|
||||
Create a directory in `~/.aetheel/workspace/hooks/<name>/` with two files:
|
||||
|
||||
`HOOK.md` — metadata in YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: session-logger
|
||||
description: Log session starts to a file
|
||||
events: [gateway:startup, command:reload]
|
||||
enabled: true
|
||||
---
|
||||
# Session Logger Hook
|
||||
|
||||
Logs gateway lifecycle events for debugging.
|
||||
```
|
||||
|
||||
`handler.py` — Python handler with a `handle(event)` function:
|
||||
|
||||
```python
|
||||
def handle(event):
|
||||
"""Called when a matching event fires."""
|
||||
print(f"Hook fired: {event.event_key}")
|
||||
# Push messages back to the user
|
||||
event.messages.append("Hook executed!")
|
||||
```
|
||||
|
||||
### Hook discovery locations
|
||||
|
||||
Hooks are discovered from two directories (in order):
|
||||
|
||||
1. `~/.aetheel/workspace/hooks/<name>/HOOK.md` — workspace hooks (per-project)
|
||||
2. `~/.aetheel/hooks/<name>/HOOK.md` — managed hooks (shared across workspaces)
|
||||
|
||||
### Programmatic hooks
|
||||
|
||||
You can also register hooks in Python code:
|
||||
|
||||
```python
|
||||
from hooks import HookManager, HookEvent
|
||||
|
||||
mgr = HookManager(workspace_dir="~/.aetheel/workspace")
|
||||
mgr.register("gateway:startup", lambda e: print("Gateway started!"))
|
||||
mgr.trigger(HookEvent(type="gateway", action="startup"))
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `enabled` to `false` to disable all hook discovery and execution.
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_hooks.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Webhooks (External Event Receiver)
|
||||
|
||||
HTTP endpoints that accept POST requests from external systems (GitHub, Jira, email services, custom scripts) and route them through the AI handler as synthetic messages. Inspired by OpenClaw's `/hooks/*` gateway endpoints.
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Endpoint | Method | Auth | Description |
|
||||
|---|---|---|---|
|
||||
| `/hooks/health` | GET | No | Health check |
|
||||
| `/hooks/wake` | POST | Yes | Wake the agent with a text prompt |
|
||||
| `/hooks/agent` | POST | Yes | Send a message to a specific agent session |
|
||||
|
||||
### Enabling webhooks
|
||||
|
||||
```json
|
||||
{
|
||||
"webhooks": {
|
||||
"enabled": true,
|
||||
"port": 8090,
|
||||
"host": "127.0.0.1",
|
||||
"token": "your-secret-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The webhook server starts automatically when `webhooks.enabled` is `true`.
|
||||
|
||||
### POST /hooks/wake
|
||||
|
||||
Wake the agent with a text prompt. The agent processes it and returns the response.
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/hooks/wake \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "Check my email for urgent items"}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"response": "I checked your inbox and found 2 urgent items..."
|
||||
}
|
||||
```
|
||||
|
||||
Optionally deliver the response to a messaging channel:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/hooks/wake \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"text": "Summarize today'\''s calendar",
|
||||
"channel": "slack",
|
||||
"channel_id": "C123456"
|
||||
}'
|
||||
```
|
||||
|
||||
### POST /hooks/agent
|
||||
|
||||
Send a message to a specific agent session with channel delivery:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/hooks/agent \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "New GitHub issue: Fix login bug #42",
|
||||
"channel": "slack",
|
||||
"channel_id": "C123456",
|
||||
"sender": "GitHub"
|
||||
}'
|
||||
```
|
||||
|
||||
### Use cases
|
||||
|
||||
- GitHub webhook → POST to `/hooks/agent` → agent triages the issue
|
||||
- Email service → POST to `/hooks/wake` → agent summarizes new emails
|
||||
- Cron script → POST to `/hooks/wake` → agent runs a daily report
|
||||
- IoT sensor → POST to `/hooks/agent` → agent alerts on anomalies
|
||||
|
||||
### Authentication
|
||||
|
||||
All POST endpoints require a bearer token. Pass it via:
|
||||
- `Authorization: Bearer <token>` header
|
||||
- `?token=<token>` query parameter (fallback)
|
||||
|
||||
If no token is configured (`"token": ""`), endpoints are open (dev mode only).
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_webhooks.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. CLI Interface
|
||||
|
||||
Aetheel includes a Click-based CLI with subcommands for all major operations.
|
||||
|
||||
### Installation
|
||||
|
||||
After installing with `pip install -e .` or `uv sync`, the `aetheel` command is available:
|
||||
|
||||
```bash
|
||||
aetheel # Same as `aetheel start`
|
||||
aetheel start # Start with default adapters
|
||||
aetheel --help # Show all commands
|
||||
```
|
||||
|
||||
Or run directly:
|
||||
|
||||
```bash
|
||||
python cli.py start --discord --webchat
|
||||
python cli.py chat "What is Python?"
|
||||
python cli.py doctor
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `aetheel` / `aetheel start` | Start with configured adapters |
|
||||
| `aetheel start --discord` | Start with Discord adapter |
|
||||
| `aetheel start --telegram` | Start with Telegram adapter |
|
||||
| `aetheel start --webchat` | Start with WebChat adapter |
|
||||
| `aetheel start --claude` | Use Claude Code runtime |
|
||||
| `aetheel start --test` | Echo handler (no AI) |
|
||||
| `aetheel chat "message"` | One-shot AI query (prints to stdout) |
|
||||
| `aetheel status` | Show runtime status |
|
||||
| `aetheel doctor` | Run diagnostics (check runtimes, tokens, workspace) |
|
||||
| `aetheel config show` | Print current config.json |
|
||||
| `aetheel config edit` | Open config in $EDITOR |
|
||||
| `aetheel config init` | Reset config to defaults |
|
||||
| `aetheel cron list` | List scheduled jobs |
|
||||
| `aetheel cron remove <id>` | Remove a scheduled job |
|
||||
| `aetheel memory search "query"` | Search memory |
|
||||
| `aetheel memory sync` | Force memory re-index |
|
||||
|
||||
### How to test
|
||||
|
||||
```bash
|
||||
# Verify CLI structure
|
||||
python cli.py --help
|
||||
python cli.py start --help
|
||||
python cli.py config --help
|
||||
python cli.py cron --help
|
||||
python cli.py memory --help
|
||||
|
||||
# Run diagnostics
|
||||
python cli.py doctor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Configuration
|
||||
|
||||
All configuration lives in `~/.aetheel/config.json`, including secrets (in the `env.vars` block).
|
||||
|
||||
### Config hierarchy (highest priority wins)
|
||||
|
||||
1. CLI arguments (`--model`, `--claude`, etc.)
|
||||
2. Process environment variables
|
||||
3. `env.vars` block in config.json
|
||||
4. `${VAR}` substitution in config values
|
||||
5. `~/.aetheel/config.json` static values
|
||||
6. Dataclass defaults
|
||||
|
||||
### Full config.json example
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-...",
|
||||
"SLACK_APP_TOKEN": "xapp-...",
|
||||
"TELEGRAM_BOT_TOKEN": "",
|
||||
"DISCORD_BOT_TOKEN": "",
|
||||
"ANTHROPIC_API_KEY": ""
|
||||
}
|
||||
},
|
||||
"log_level": "INFO",
|
||||
"runtime": {
|
||||
"mode": "cli",
|
||||
"model": null,
|
||||
"timeout_seconds": 120,
|
||||
"server_url": "http://localhost:4096",
|
||||
"format": "json",
|
||||
"agent": null,
|
||||
"attach": null
|
||||
},
|
||||
"claude": {
|
||||
"model": null,
|
||||
"timeout_seconds": 120,
|
||||
"max_turns": 3,
|
||||
"no_tools": false,
|
||||
"allowed_tools": [
|
||||
"Bash", "Read", "Write", "Edit", "Glob", "Grep",
|
||||
"WebSearch", "WebFetch",
|
||||
"Task", "TaskOutput", "TaskStop", "Skill",
|
||||
"TeamCreate", "TeamDelete", "SendMessage"
|
||||
]
|
||||
},
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"bot_token": "${SLACK_BOT_TOKEN}",
|
||||
"app_token": "${SLACK_APP_TOKEN}"
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"bot_token": "${TELEGRAM_BOT_TOKEN}"
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"bot_token": "${DISCORD_BOT_TOKEN}",
|
||||
"listen_channels": []
|
||||
},
|
||||
"memory": {
|
||||
"workspace": "~/.aetheel/workspace",
|
||||
"db_path": "~/.aetheel/memory.db"
|
||||
},
|
||||
"scheduler": {
|
||||
"db_path": "~/.aetheel/scheduler.db"
|
||||
},
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"default_channel": "slack",
|
||||
"default_channel_id": "",
|
||||
"silent": false
|
||||
},
|
||||
"webchat": {
|
||||
"enabled": false,
|
||||
"port": 8080,
|
||||
"host": "127.0.0.1"
|
||||
},
|
||||
"mcp": {
|
||||
"servers": {}
|
||||
},
|
||||
"models": {
|
||||
"heartbeat": null,
|
||||
"subagent": null,
|
||||
"default": null
|
||||
},
|
||||
"hooks": {
|
||||
"enabled": true
|
||||
},
|
||||
"webhooks": {
|
||||
"enabled": false,
|
||||
"port": 8090,
|
||||
"host": "127.0.0.1",
|
||||
"token": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Process environment variable overrides
|
||||
|
||||
Process env vars still override everything. Useful for CI, Docker, or systemd:
|
||||
|
||||
```bash
|
||||
OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
|
||||
CLAUDE_MODEL=claude-sonnet-4-20250514
|
||||
LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Running Tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
cd Aetheel
|
||||
pip install -e ".[test]"
|
||||
# or
|
||||
uv sync --extra test
|
||||
```
|
||||
|
||||
### Run all tests
|
||||
|
||||
```bash
|
||||
python -m pytest tests/ -v --ignore=tests/test_scheduler.py
|
||||
```
|
||||
|
||||
> Note: `test_scheduler.py` requires `apscheduler` installed. If you have it, run the full suite:
|
||||
> ```bash
|
||||
> python -m pytest tests/ -v
|
||||
> ```
|
||||
|
||||
### Run specific test files
|
||||
|
||||
```bash
|
||||
# Base adapter tests
|
||||
python -m pytest tests/test_base_adapter.py -v
|
||||
|
||||
# Skills system tests
|
||||
python -m pytest tests/test_skills.py -v
|
||||
|
||||
# MCP config writer tests
|
||||
python -m pytest tests/test_mcp_config.py -v
|
||||
|
||||
# SubagentBus pub/sub tests
|
||||
python -m pytest tests/test_subagent_bus.py -v
|
||||
|
||||
# Scheduler tests (requires apscheduler)
|
||||
python -m pytest tests/test_scheduler.py -v
|
||||
|
||||
# Hook system tests
|
||||
python -m pytest tests/test_hooks.py -v
|
||||
|
||||
# Webhook receiver tests
|
||||
python -m pytest tests/test_webhooks.py -v
|
||||
```
|
||||
|
||||
### Test summary
|
||||
|
||||
| Test file | What it covers | Count |
|
||||
|-----------|---------------|-------|
|
||||
| `test_base_adapter.py` | BaseAdapter dispatch, handler registration, error handling | 9 |
|
||||
| `test_skills.py` | Skill loading, trigger matching, frontmatter parsing, context building | 21 |
|
||||
| `test_mcp_config.py` | MCP config writer (Claude/OpenCode formats, round-trip, edge cases) | 8 |
|
||||
| `test_subagent_bus.py` | SubagentBus subscribe/publish, isolation, error resilience, thread safety | 10 + 2 |
|
||||
| `test_hooks.py` | Hook discovery, trigger, programmatic hooks, error resilience, messages | 14 |
|
||||
| `test_webhooks.py` | Webhook endpoints (wake, agent, health), auth, channel delivery | 10 |
|
||||
| `test_scheduler.py` | Scheduler one-shot/cron jobs, persistence, removal | varies |
|
||||
|
||||
### Quick smoke tests
|
||||
|
||||
```bash
|
||||
# Verify config loads correctly
|
||||
python -c "from config import load_config; c = load_config(); print(f'Tools enabled: {not c.claude.no_tools}, Tools: {len(c.claude.allowed_tools)}')"
|
||||
|
||||
# Verify system prompt has new sections
|
||||
python -c "
|
||||
from agent.opencode_runtime import build_aetheel_system_prompt
|
||||
p = build_aetheel_system_prompt()
|
||||
for section in ['Your Tools', 'Self-Modification', 'Subagents & Teams']:
|
||||
assert section in p, f'Missing: {section}'
|
||||
print(f' ✅ {section}')
|
||||
"
|
||||
|
||||
# Verify heartbeat parser
|
||||
python -c "
|
||||
from heartbeat.heartbeat import HeartbeatRunner
|
||||
tests = [('Every 30 minutes', '*/30 * * * *'), ('Every morning (9:00 AM)', '0 9 * * *'), ('Every evening (6:00 PM)', '0 18 * * *')]
|
||||
for header, expected in tests:
|
||||
result = HeartbeatRunner._parse_schedule_header(header)
|
||||
assert result == expected, f'{header}: got {result}, expected {expected}'
|
||||
print(f' ✅ {header} -> {result}')
|
||||
"
|
||||
|
||||
# Verify CLI commands exist
|
||||
python cli.py --help
|
||||
```
|
||||
Reference in New Issue
Block a user