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:
21
docs/project/additions.txt
Normal file
21
docs/project/additions.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
# completed
|
||||
config instead of env
|
||||
openclaw-style secrets in config.json (env.vars + ${VAR} substitution, removed python-dotenv dependency)
|
||||
per-task model routing (models.heartbeat, models.subagent, models.default — different models for different task types)
|
||||
edit its own files and config as well as add skills
|
||||
start command for all instead of flags use config
|
||||
customize opencode/claudecode setup like llms and providers during setup, agent creation/modify for claudecode and opencode
|
||||
install script starts server and adds the aetheel command
|
||||
llm usage stats
|
||||
logo
|
||||
discord advanced features (reply threading, history context, ack reactions, typing indicators, reaction handling, slash commands, interactive components, exec approvals)
|
||||
opencode advanced features (agent selection, attach mode, file attachments, session fork/title, models listing, stats, agents listing)
|
||||
|
||||
|
||||
# Not complete
|
||||
agent to agent and agent orchestration
|
||||
better UI
|
||||
human in the loop
|
||||
security
|
||||
browse plugins and skills from claude marketplace or opencode
|
||||
self modification docs
|
||||
222
docs/project/commands.md
Normal file
222
docs/project/commands.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Aetheel Commands Reference
|
||||
|
||||
All commands work across every adapter (Slack, Discord, Telegram, WebChat).
|
||||
|
||||
## Chat Commands
|
||||
|
||||
Type these as regular messages in any channel or DM. No `/` prefix needed — just send the word as a message. The `/` prefix also works in DMs and on platforms that don't intercept it (Discord, Telegram, WebChat).
|
||||
|
||||
> In Slack channels, Slack intercepts `/` as a native slash command and blocks it. Use the prefix-free form instead (e.g. `engine claude` not `/engine claude`). In Slack DMs with the bot, both forms work.
|
||||
|
||||
### General
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `status` | Show bot status, engine, model, model routes, sessions |
|
||||
| `help` | Show all available commands |
|
||||
| `time` | Current server time |
|
||||
| `sessions` | Active session count + cleanup stale |
|
||||
| `reload` | Reload config.json and skills |
|
||||
| `subagents` | List active background tasks |
|
||||
|
||||
### Runtime Switching (live, no restart needed)
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `engine` | Show current engine (opencode/claude) |
|
||||
| `engine opencode` | Switch to OpenCode runtime |
|
||||
| `engine claude` | Switch to Claude Code runtime |
|
||||
| `model` | Show current model |
|
||||
| `model <name>` | Switch model (e.g. `model anthropic/claude-sonnet-4-20250514`) |
|
||||
| `provider` | Show current provider (OpenCode only) |
|
||||
| `provider <name>` | Switch provider (e.g. `provider anthropic`, `provider openai`) |
|
||||
| `usage` | Show LLM usage stats, costs, and rate limit history |
|
||||
| `models` | List all available models from configured providers (OpenCode only) |
|
||||
| `models <provider>` | List models for a specific provider (e.g. `models anthropic`) |
|
||||
| `stats` | Show OpenCode token usage and cost stats (all time) |
|
||||
| `stats <days>` | Show stats for the last N days (e.g. `stats 7`) |
|
||||
| `agents` | List available OpenCode agents |
|
||||
|
||||
Engine, model, and provider changes take effect immediately and are persisted to `config.json` so they survive restarts.
|
||||
|
||||
#### Examples
|
||||
|
||||
```
|
||||
engine claude
|
||||
model claude-sonnet-4-20250514
|
||||
|
||||
engine opencode
|
||||
model anthropic/claude-sonnet-4-20250514
|
||||
provider anthropic
|
||||
|
||||
model openai/gpt-4o
|
||||
provider openai
|
||||
|
||||
model google/gemini-2.5-pro
|
||||
provider google
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `config` | Show config summary (engine, model, adapters) |
|
||||
| `config show` | Dump full config.json |
|
||||
| `config set <key> <value>` | Edit a config value (dotted notation) |
|
||||
|
||||
#### Config Set Examples
|
||||
|
||||
```
|
||||
config set runtime.timeout_seconds 300
|
||||
config set claude.max_turns 5
|
||||
config set webchat.enabled true
|
||||
config set discord.enabled true
|
||||
config set webhooks.token my-secret-token
|
||||
config set heartbeat.silent true
|
||||
```
|
||||
|
||||
After `config set`, run `reload` to apply changes that don't auto-apply (adapter changes require a restart).
|
||||
|
||||
### Scheduler
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `cron list` | List all scheduled jobs |
|
||||
| `cron remove <id>` | Remove a scheduled job by ID |
|
||||
|
||||
### MCP Servers
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `mcp list` | List configured MCP servers |
|
||||
| `mcp add <name> <command> [args]` | Add a new MCP server |
|
||||
| `mcp remove <name>` | Remove an MCP server |
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
mcp add brave-search uvx brave-search-mcp@latest
|
||||
mcp add filesystem npx @anthropic-ai/mcp-filesystem /home/user/projects
|
||||
mcp add github uvx github-mcp-server
|
||||
mcp remove brave-search
|
||||
```
|
||||
|
||||
### Skills
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `skill list` | List all loaded skills |
|
||||
| `skill show <name>` | View a skill's content |
|
||||
| `skill create <name>` | Create a new skill template |
|
||||
| `skill rename <old> <new>` | Rename a skill |
|
||||
| `skill remove <name>` | Remove a skill |
|
||||
| `skill reload` | Reload all skills |
|
||||
|
||||
You can also manage skills via natural language:
|
||||
|
||||
| Natural Language | What It Does |
|
||||
|---|---|
|
||||
| "Create a skill for checking weather" | Creates a skill with auto-derived name and triggers |
|
||||
| "Rename my-old-skill to weather" | Renames the skill directory and updates SKILL.md |
|
||||
|
||||
These are handled locally (no AI call needed), so they work even if the AI runtime is down.
|
||||
|
||||
### AI Chat
|
||||
|
||||
Any message that isn't a command is sent to the AI. The AI can also trigger actions by including tags in its response:
|
||||
|
||||
| AI Action Tag | What It Does |
|
||||
|---|---|
|
||||
| `[ACTION:remind\|<minutes>\|<message>]` | Schedule a one-shot reminder |
|
||||
| `[ACTION:cron\|<cron_expr>\|<prompt>]` | Schedule a recurring cron job |
|
||||
| `[ACTION:spawn\|<task>]` | Spawn a background subagent |
|
||||
|
||||
---
|
||||
|
||||
## Terminal Commands
|
||||
|
||||
After installation, the `aetheel` command is available in your shell.
|
||||
|
||||
### Service Management
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `aetheel start` | Start Aetheel in the foreground |
|
||||
| `aetheel stop` | Stop the background service |
|
||||
| `aetheel restart` | Restart the background service |
|
||||
| `aetheel status` | Show install and service status |
|
||||
| `aetheel logs` | Tail the live log file |
|
||||
|
||||
### Setup & Maintenance
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `aetheel setup` | Re-run the interactive setup wizard |
|
||||
| `aetheel update` | Pull latest code + update dependencies |
|
||||
| `aetheel doctor` | Run diagnostics (config, tokens, runtimes) |
|
||||
| `aetheel config` | Open config.json in your `$EDITOR` |
|
||||
|
||||
### Pass-Through
|
||||
|
||||
Any other arguments are passed directly to `main.py`:
|
||||
|
||||
```bash
|
||||
aetheel --test # Echo mode, no AI
|
||||
aetheel --claude # Override: use Claude engine
|
||||
aetheel --model gpt-4o # Override: specific model
|
||||
aetheel --log DEBUG # Override: debug logging
|
||||
```
|
||||
|
||||
These flags are optional overrides. All settings come from `~/.aetheel/config.json` by default.
|
||||
|
||||
---
|
||||
|
||||
## Config File Reference
|
||||
|
||||
All features are controlled by `~/.aetheel/config.json`. No flags required.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"runtime": {
|
||||
"engine": "opencode", // "opencode" or "claude"
|
||||
"mode": "cli", // "cli" or "sdk"
|
||||
"model": null, // e.g. "anthropic/claude-sonnet-4-20250514"
|
||||
"provider": null, // e.g. "anthropic", "openai", "google"
|
||||
"timeout_seconds": 120,
|
||||
"agent": null, // OpenCode agent name (from `agents` command)
|
||||
"attach": null // Attach to running server URL for faster CLI mode
|
||||
},
|
||||
"claude": {
|
||||
"model": null, // e.g. "claude-sonnet-4-20250514"
|
||||
"max_turns": 3,
|
||||
"no_tools": false
|
||||
},
|
||||
"slack": { "enabled": true },
|
||||
"telegram": { "enabled": false },
|
||||
"discord": { "enabled": false, "listen_channels": [] },
|
||||
"webchat": { "enabled": false, "port": 8080 },
|
||||
"webhooks": { "enabled": false, "port": 8090, "token": "" },
|
||||
"heartbeat": { "enabled": true },
|
||||
"models": { "heartbeat": null, "subagent": null, "default": null },
|
||||
"hooks": { "enabled": true }
|
||||
}
|
||||
```
|
||||
|
||||
Adapters auto-enable when their token is set in `config.json` → `env.vars`, even if `enabled` is `false` in config.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Failover & Rate Limit Handling
|
||||
|
||||
When a rate limit or quota error is detected from the active engine:
|
||||
|
||||
1. Aetheel notifies you in the channel with the error details
|
||||
2. Automatically attempts to failover to the other engine (claude → opencode or vice versa)
|
||||
3. If failover succeeds, the response is delivered and the active engine is switched
|
||||
4. If failover also fails, both errors are reported
|
||||
|
||||
Rate limit detection covers: HTTP 429, "rate limit", "quota exceeded", "too many requests", "usage limit", "credit balance", "overloaded", and similar patterns from both Claude Code and OpenCode.
|
||||
|
||||
Use `usage` to see cumulative stats including rate limit hits and failover count.
|
||||
|
||||
Note: Claude Code's `--output-format json` returns `cost_usd` per request. OpenCode does not currently expose per-request cost in its CLI output, so cost tracking is only available for the Claude engine.
|
||||
445
docs/project/configuration.md
Normal file
445
docs/project/configuration.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# Configuration Guide
|
||||
|
||||
> How Aetheel loads its settings — config file, secrets, and CLI overrides.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Config File](#config-file)
|
||||
3. [Secrets (env.vars)](#secrets)
|
||||
4. [CLI Overrides](#cli-overrides)
|
||||
5. [Priority Order](#priority-order)
|
||||
6. [Reference](#reference)
|
||||
7. [Examples](#examples)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aetheel uses a single JSON config file for everything — settings and secrets:
|
||||
|
||||
| File | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| `config.json` | `~/.aetheel/config.json` | All settings, secrets (via `env.vars` block), and `${VAR}` references |
|
||||
|
||||
Secrets (tokens, API keys) go in the `env.vars` block inside config.json. They can be referenced elsewhere in the config using `${VAR}` syntax. Process environment variables still override everything.
|
||||
|
||||
On first run, Aetheel auto-creates `~/.aetheel/config.json` with sensible defaults.
|
||||
|
||||
---
|
||||
|
||||
## Config File
|
||||
|
||||
Located at `~/.aetheel/config.json`. Created automatically on first run.
|
||||
|
||||
### Full Default Config
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "",
|
||||
"SLACK_APP_TOKEN": "",
|
||||
"TELEGRAM_BOT_TOKEN": "",
|
||||
"DISCORD_BOT_TOKEN": "",
|
||||
"ANTHROPIC_API_KEY": "",
|
||||
"OPENCODE_SERVER_PASSWORD": ""
|
||||
}
|
||||
},
|
||||
"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": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Section: `runtime`
|
||||
|
||||
Controls the OpenCode AI runtime (default).
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `mode` | string | `"cli"` | `"cli"` (subprocess) or `"sdk"` (opencode serve API) |
|
||||
| `model` | string\|null | `null` | Model ID, e.g. `"anthropic/claude-sonnet-4-20250514"`. Null uses OpenCode's default. |
|
||||
| `timeout_seconds` | int | `120` | Max seconds to wait for a response |
|
||||
| `server_url` | string | `"http://localhost:4096"` | OpenCode server URL (SDK mode only) |
|
||||
| `format` | string | `"json"` | CLI output format: `"json"` (structured) or `"default"` (plain text) |
|
||||
| `workspace` | string\|null | `null` | Working directory for OpenCode. Null uses current directory. |
|
||||
| `provider` | string\|null | `null` | Provider override, e.g. `"anthropic"`, `"openai"` |
|
||||
| `agent` | string\|null | `null` | OpenCode agent name. Use `agents` command to list available agents. |
|
||||
| `attach` | string\|null | `null` | URL of a running `opencode serve` instance to attach to in CLI mode, avoiding MCP cold boot per request. |
|
||||
|
||||
### Section: `claude`
|
||||
|
||||
Controls the Claude Code runtime (used with `--claude` flag).
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `model` | string\|null | `null` | Model ID, e.g. `"claude-sonnet-4-20250514"` |
|
||||
| `timeout_seconds` | int | `120` | Max seconds to wait for a response |
|
||||
| `max_turns` | int | `3` | Max agentic tool-use turns per request |
|
||||
| `no_tools` | bool | `true` | Disable tools for pure conversation mode |
|
||||
|
||||
### Section: `discord`
|
||||
|
||||
Discord-specific settings (non-secret).
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `listen_channels` | list[string] | `[]` | Channel IDs where the bot responds to all messages (no @mention needed) |
|
||||
|
||||
### Section: `memory`
|
||||
|
||||
Memory system paths.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `workspace` | string | `"~/.aetheel/workspace"` | Directory for identity files (SOUL.md, USER.md, MEMORY.md) and skills |
|
||||
| `db_path` | string | `"~/.aetheel/memory.db"` | SQLite database for embeddings and search index |
|
||||
|
||||
### Section: `scheduler`
|
||||
|
||||
Scheduler storage.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `db_path` | string | `"~/.aetheel/scheduler.db"` | SQLite database for persisted scheduled jobs |
|
||||
|
||||
### Section: `models`
|
||||
|
||||
Per-task model routing. Each task type can use a different model, provider, and engine. Omit or set to `null` to use the global `runtime` config.
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"heartbeat": {
|
||||
"engine": "opencode",
|
||||
"model": "ollama/llama3.2",
|
||||
"provider": "ollama"
|
||||
},
|
||||
"subagent": {
|
||||
"model": "minimax/minimax-m1",
|
||||
"provider": "minimax"
|
||||
},
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `heartbeat` | object\|null | `null` | Model override for heartbeat periodic tasks |
|
||||
| `subagent` | object\|null | `null` | Model override for background subagent tasks |
|
||||
| `default` | object\|null | `null` | Fallback model override for all other tasks |
|
||||
|
||||
Each route object supports:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `engine` | string\|null | `null` | `"opencode"` or `"claude"` — null inherits global |
|
||||
| `model` | string\|null | `null` | Model ID (e.g. `"ollama/llama3.2"`) — null inherits global |
|
||||
| `provider` | string\|null | `null` | Provider name (e.g. `"ollama"`, `"minimax"`) — null inherits global |
|
||||
| `timeout_seconds` | int\|null | `null` | Request timeout — null inherits global |
|
||||
|
||||
### Top-level
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `log_level` | string | `"INFO"` | `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"` |
|
||||
|
||||
---
|
||||
|
||||
## Secrets
|
||||
|
||||
Secrets live in the `env.vars` block inside `config.json`. Values defined here are injected into the process environment (if not already set), and can be referenced anywhere in the config using `${VAR}` syntax.
|
||||
|
||||
### Example config.json with secrets
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
|
||||
"SLACK_APP_TOKEN": "xapp-your-app-token",
|
||||
"DISCORD_BOT_TOKEN": "your-discord-token",
|
||||
"ANTHROPIC_API_KEY": "sk-ant-..."
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"bot_token": "${SLACK_BOT_TOKEN}",
|
||||
"app_token": "${SLACK_APP_TOKEN}"
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"bot_token": "${DISCORD_BOT_TOKEN}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How `${VAR}` substitution works
|
||||
|
||||
- `${VAR}` → resolved from process env (including `env.vars`)
|
||||
- `$${VAR}` → literal `${VAR}` (escape sequence)
|
||||
- Missing vars log a warning and keep the literal `${VAR}` string
|
||||
|
||||
### Required (at least one adapter)
|
||||
|
||||
| Variable | Format | Description |
|
||||
|----------|--------|-------------|
|
||||
| `SLACK_BOT_TOKEN` | `xoxb-...` | Slack bot OAuth token |
|
||||
| `SLACK_APP_TOKEN` | `xapp-...` | Slack app-level token (Socket Mode) |
|
||||
|
||||
### Optional
|
||||
|
||||
| Variable | Format | Description |
|
||||
|----------|--------|-------------|
|
||||
| `TELEGRAM_BOT_TOKEN` | string | Telegram bot token from @BotFather |
|
||||
| `DISCORD_BOT_TOKEN` | string | Discord bot token from Developer Portal |
|
||||
| `OPENCODE_SERVER_PASSWORD` | string | Password for `opencode serve` (SDK mode) |
|
||||
| `ANTHROPIC_API_KEY` | `sk-ant-...` | Anthropic API key (Claude Code runtime) |
|
||||
|
||||
All of these can be set in `env.vars` in config.json, as process environment variables, or both (process env wins).
|
||||
|
||||
### Environment Variable Overrides
|
||||
|
||||
Any config.json setting can also be overridden via environment variables. These take priority over the config file:
|
||||
|
||||
| Env Variable | Overrides |
|
||||
|-------------|-----------|
|
||||
| `LOG_LEVEL` | `log_level` |
|
||||
| `OPENCODE_MODE` | `runtime.mode` |
|
||||
| `OPENCODE_MODEL` | `runtime.model` |
|
||||
| `OPENCODE_TIMEOUT` | `runtime.timeout_seconds` |
|
||||
| `OPENCODE_SERVER_URL` | `runtime.server_url` |
|
||||
| `OPENCODE_PROVIDER` | `runtime.provider` |
|
||||
| `OPENCODE_WORKSPACE` | `runtime.workspace` |
|
||||
| `OPENCODE_AGENT` | `runtime.agent` |
|
||||
| `OPENCODE_ATTACH` | `runtime.attach` |
|
||||
| `CLAUDE_MODEL` | `claude.model` |
|
||||
| `CLAUDE_TIMEOUT` | `claude.timeout_seconds` |
|
||||
| `CLAUDE_MAX_TURNS` | `claude.max_turns` |
|
||||
| `CLAUDE_NO_TOOLS` | `claude.no_tools` |
|
||||
| `DISCORD_LISTEN_CHANNELS` | `discord.listen_channels` (comma-separated) |
|
||||
| `AETHEEL_WORKSPACE` | `memory.workspace` |
|
||||
| `AETHEEL_MEMORY_DB` | `memory.db_path` |
|
||||
|
||||
---
|
||||
|
||||
## CLI Overrides
|
||||
|
||||
CLI arguments have the highest priority and override both config.json and environment variables.
|
||||
|
||||
```bash
|
||||
python main.py [options]
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--model <id>` | Override model for both runtimes |
|
||||
| `--claude` | Use Claude Code runtime instead of OpenCode |
|
||||
| `--cli` | Force CLI mode (OpenCode) |
|
||||
| `--sdk` | Force SDK mode (OpenCode) |
|
||||
| `--telegram` | Enable Telegram adapter |
|
||||
| `--discord` | Enable Discord adapter |
|
||||
| `--test` | Use echo handler (no AI) |
|
||||
| `--log <level>` | Override log level |
|
||||
|
||||
---
|
||||
|
||||
## Priority Order
|
||||
|
||||
When the same setting is defined in multiple places, the highest priority wins:
|
||||
|
||||
```
|
||||
CLI arguments > Process env vars > env.vars block > ${VAR} substitution > config.json values > Defaults
|
||||
```
|
||||
|
||||
For example, if `config.json` sets `runtime.model` to `"anthropic/claude-sonnet-4-20250514"` but you run `python main.py --model openai/gpt-5.1`, the CLI argument wins.
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
### File Locations
|
||||
|
||||
| File | Path | Git-tracked |
|
||||
|------|------|-------------|
|
||||
| Config + Secrets | `~/.aetheel/config.json` | No |
|
||||
| Memory DB | `~/.aetheel/memory.db` | No |
|
||||
| Session DB | `~/.aetheel/sessions.db` | No |
|
||||
| Scheduler DB | `~/.aetheel/scheduler.db` | No |
|
||||
| Identity files | `~/.aetheel/workspace/SOUL.md` etc. | No |
|
||||
| Session logs | `~/.aetheel/workspace/daily/` | No |
|
||||
|
||||
### Data Directory Structure
|
||||
|
||||
```
|
||||
~/.aetheel/
|
||||
├── config.json # Main configuration
|
||||
├── memory.db # Embeddings + search index
|
||||
├── sessions.db # Persistent session mappings
|
||||
├── scheduler.db # Scheduled jobs
|
||||
└── workspace/
|
||||
├── SOUL.md # Bot personality
|
||||
├── USER.md # User profile
|
||||
├── MEMORY.md # Long-term memory
|
||||
├── skills/ # Skill definitions
|
||||
│ └── <name>/
|
||||
│ └── SKILL.md
|
||||
└── daily/ # Session logs
|
||||
└── YYYY-MM-DD.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal Setup (Slack + OpenCode CLI)
|
||||
|
||||
`~/.aetheel/config.json`:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-token",
|
||||
"SLACK_APP_TOKEN": "xapp-your-token"
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"bot_token": "${SLACK_BOT_TOKEN}",
|
||||
"app_token": "${SLACK_APP_TOKEN}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
No other changes needed — defaults work.
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Custom Model + SDK Mode
|
||||
|
||||
`~/.aetheel/config.json`:
|
||||
```json
|
||||
{
|
||||
"runtime": {
|
||||
"mode": "sdk",
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"server_url": "http://localhost:4096"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Start OpenCode server first, then Aetheel:
|
||||
```bash
|
||||
opencode serve --port 4096
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Discord with Listen Channels
|
||||
|
||||
`~/.aetheel/config.json`:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"DISCORD_BOT_TOKEN": "your-discord-token"
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"bot_token": "${DISCORD_BOT_TOKEN}",
|
||||
"listen_channels": ["1234567890123456"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
python main.py --discord
|
||||
```
|
||||
|
||||
### Multi-Channel (Slack + Discord + Telegram)
|
||||
|
||||
`~/.aetheel/config.json`:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-token",
|
||||
"SLACK_APP_TOKEN": "xapp-your-token",
|
||||
"DISCORD_BOT_TOKEN": "your-discord-token",
|
||||
"TELEGRAM_BOT_TOKEN": "your-telegram-token"
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"bot_token": "${SLACK_BOT_TOKEN}",
|
||||
"app_token": "${SLACK_APP_TOKEN}"
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"bot_token": "${DISCORD_BOT_TOKEN}"
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"bot_token": "${TELEGRAM_BOT_TOKEN}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
python main.py --discord --telegram
|
||||
```
|
||||
|
||||
### Claude Code Runtime
|
||||
|
||||
`~/.aetheel/config.json`:
|
||||
```json
|
||||
{
|
||||
"claude": {
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"max_turns": 5,
|
||||
"no_tools": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
python main.py --claude
|
||||
```
|
||||
326
docs/project/discord-features.md
Normal file
326
docs/project/discord-features.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# Discord Advanced Features
|
||||
|
||||
All Discord features are config-driven via `~/.aetheel/config.json` under the `discord` key. No code changes needed.
|
||||
|
||||
---
|
||||
|
||||
## Default Config
|
||||
|
||||
```json
|
||||
{
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"listen_channels": [],
|
||||
"reply_to_mode": "first",
|
||||
"history_enabled": true,
|
||||
"history_limit": 20,
|
||||
"channel_overrides": {},
|
||||
"ack_reaction": "👀",
|
||||
"typing_indicator": true,
|
||||
"reaction_mode": "own",
|
||||
"exec_approvals": false,
|
||||
"exec_approval_tools": ["Bash", "Write", "Edit"],
|
||||
"slash_commands": true,
|
||||
"components_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reply Threading
|
||||
|
||||
Controls whether the bot replies to the user's message using Discord's native reply feature (the quoted message above the response).
|
||||
|
||||
```json
|
||||
"reply_to_mode": "first"
|
||||
```
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `"off"` | Plain messages, no reply reference |
|
||||
| `"first"` | First chunk of the response replies to the user's message |
|
||||
| `"all"` | Every chunk replies to the user's message |
|
||||
|
||||
If the original message gets deleted before the bot responds, it falls back to a plain message automatically.
|
||||
|
||||
---
|
||||
|
||||
## Channel History Context
|
||||
|
||||
Injects recent channel messages into the AI's system prompt so it has conversational context beyond the current message.
|
||||
|
||||
```json
|
||||
"history_enabled": true,
|
||||
"history_limit": 20
|
||||
```
|
||||
|
||||
- `history_enabled` — global toggle, default `true`
|
||||
- `history_limit` — number of recent messages to fetch, default `20`
|
||||
- History is only fetched for guild channels, not DMs (DMs already have session continuity)
|
||||
- Messages are formatted as `[username]: content` and injected under a "Recent Channel History" section in the system prompt
|
||||
- The bot's own messages appear as `[assistant]`
|
||||
|
||||
### Per-Channel Overrides
|
||||
|
||||
You can enable, disable, or change the limit per channel:
|
||||
|
||||
```json
|
||||
"channel_overrides": {
|
||||
"1234567890": {
|
||||
"history_enabled": true,
|
||||
"history_limit": 50
|
||||
},
|
||||
"9876543210": {
|
||||
"history_enabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keys are Discord channel IDs (strings). Any field you omit falls back to the global default.
|
||||
|
||||
Use cases:
|
||||
- Disable history in a high-traffic channel to save context window
|
||||
- Increase the limit in a project channel where long context matters
|
||||
- Disable entirely for a channel where privacy is a concern
|
||||
|
||||
---
|
||||
|
||||
## Ack Reactions
|
||||
|
||||
Adds a reaction emoji to the user's message while the bot is processing, then removes it when the response is sent. Gives immediate visual feedback that the bot received the message.
|
||||
|
||||
```json
|
||||
"ack_reaction": "👀"
|
||||
```
|
||||
|
||||
- Set to any valid emoji: `"👀"`, `"⏳"`, `"🤔"`, etc.
|
||||
- Set to `""` (empty string) to disable
|
||||
- The reaction is removed automatically after the response is sent
|
||||
- If the bot lacks Add Reactions permission in a channel, it silently skips
|
||||
|
||||
---
|
||||
|
||||
## Typing Indicator
|
||||
|
||||
Shows the "Aetheel is typing..." indicator in the channel while the AI processes the message.
|
||||
|
||||
```json
|
||||
"typing_indicator": true
|
||||
```
|
||||
|
||||
- `true` — typing indicator shown during processing (default)
|
||||
- `false` — no typing indicator
|
||||
|
||||
The indicator stays active for the entire duration of the AI call. Combined with ack reactions, users get two layers of feedback: the reaction appears instantly, and the typing indicator persists until the response arrives.
|
||||
|
||||
---
|
||||
|
||||
## Reaction Handling
|
||||
|
||||
Controls whether the bot processes emoji reactions as messages to the AI.
|
||||
|
||||
```json
|
||||
"reaction_mode": "own"
|
||||
```
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `"off"` | Reactions are ignored entirely |
|
||||
| `"own"` | Only reactions on the bot's own messages are processed |
|
||||
| `"all"` | Reactions on any message in the channel are processed |
|
||||
|
||||
When a reaction is processed, it's sent to the AI as:
|
||||
```
|
||||
[Reaction: 👍 on message: <original message text>]
|
||||
```
|
||||
|
||||
The AI can then respond contextually — for example, a 👎 on a suggestion could prompt the bot to offer alternatives.
|
||||
|
||||
Bot reactions and reactions from other bots are always ignored.
|
||||
|
||||
---
|
||||
|
||||
## Slash Commands
|
||||
|
||||
Registers native Discord slash commands that appear in the `/` menu.
|
||||
|
||||
```json
|
||||
"slash_commands": true
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/ask <message>` | Ask Aetheel a question. Shows "thinking..." while processing. |
|
||||
| `/status` | Check bot status (same as typing `status` in chat) |
|
||||
| `/help` | Show help (same as typing `help` in chat) |
|
||||
|
||||
Commands are synced with Discord on bot startup. First sync can take up to an hour to propagate globally — guild-level commands appear faster.
|
||||
|
||||
Set to `false` to disable slash command registration entirely.
|
||||
|
||||
### Bot Permissions
|
||||
|
||||
For slash commands to work, the bot must be invited with the `applications.commands` OAuth2 scope in addition to `bot`. If you originally invited without it, re-invite using:
|
||||
|
||||
OAuth2 → URL Generator → Scopes: `bot`, `applications.commands`
|
||||
|
||||
---
|
||||
|
||||
## Interactive Components
|
||||
|
||||
Enables the bot to send messages with buttons and select menus.
|
||||
|
||||
```json
|
||||
"components_enabled": true
|
||||
```
|
||||
|
||||
Components are used internally by:
|
||||
- Exec approval prompts (approve/deny buttons)
|
||||
- Any future interactive features
|
||||
|
||||
The adapter exposes `send_components_message()` for programmatic use:
|
||||
|
||||
```python
|
||||
adapter.send_components_message(
|
||||
channel_id="123456789",
|
||||
text="Choose an option:",
|
||||
buttons=[
|
||||
{"label": "Option A", "style": "primary", "custom_id": "opt_a"},
|
||||
{"label": "Option B", "style": "secondary", "custom_id": "opt_b"},
|
||||
{"label": "Delete", "style": "danger", "custom_id": "delete"},
|
||||
],
|
||||
select_options=[
|
||||
{"label": "Python", "value": "python", "description": "Snake language"},
|
||||
{"label": "TypeScript", "value": "ts", "description": "JS but typed"},
|
||||
],
|
||||
callback=my_callback_fn,
|
||||
)
|
||||
```
|
||||
|
||||
Button styles: `primary` (blurple), `secondary` (gray), `success` (green), `danger` (red).
|
||||
|
||||
Set to `false` to disable — approval prompts and interactive messages fall back to plain text.
|
||||
|
||||
---
|
||||
|
||||
## Exec Approvals
|
||||
|
||||
Adds a human-in-the-loop confirmation step for dangerous AI tool use. When the AI tries to use a gated tool, a button prompt appears in the channel asking the user to approve or deny.
|
||||
|
||||
```json
|
||||
"exec_approvals": false,
|
||||
"exec_approval_tools": ["Bash", "Write", "Edit"]
|
||||
```
|
||||
|
||||
- `exec_approvals` — master toggle, default `false`
|
||||
- `exec_approval_tools` — list of tool names that require approval
|
||||
|
||||
### How It Works
|
||||
|
||||
1. AI decides to use a gated tool (e.g. `Bash`)
|
||||
2. Bot sends an embed with approve/deny buttons:
|
||||
```
|
||||
⚠️ Exec Approval Required
|
||||
Tool: Bash
|
||||
Action: <description of what the AI wants to do>
|
||||
[✅ Approve] [❌ Deny]
|
||||
```
|
||||
3. Only the user who sent the original message can click the buttons
|
||||
4. If approved, the tool executes normally
|
||||
5. If denied or timed out (2 minutes), the action is blocked
|
||||
|
||||
### Customizing Gated Tools
|
||||
|
||||
Add or remove tools from the approval list:
|
||||
|
||||
```json
|
||||
"exec_approval_tools": ["Bash", "Write", "Edit", "WebFetch"]
|
||||
```
|
||||
|
||||
Tools not in this list are auto-approved. Set the list to `[]` to approve everything (while keeping the feature enabled for future use).
|
||||
|
||||
---
|
||||
|
||||
## Listen Channels
|
||||
|
||||
Channels where the bot responds to all messages without requiring an @mention.
|
||||
|
||||
```json
|
||||
"listen_channels": ["1234567890", "9876543210"]
|
||||
```
|
||||
|
||||
In all other guild channels, the bot only responds when @mentioned. DMs always respond to all messages regardless of this setting.
|
||||
|
||||
You can also set this via environment variable:
|
||||
```bash
|
||||
DISCORD_LISTEN_CHANNELS=1234567890,9876543210
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required Bot Permissions
|
||||
|
||||
For all features to work, invite the bot with these permissions:
|
||||
|
||||
| Permission | Required For |
|
||||
|------------|-------------|
|
||||
| Send Messages | Responding to users |
|
||||
| Read Message History | History context injection |
|
||||
| View Channels | Seeing channels |
|
||||
| Add Reactions | Ack reactions |
|
||||
| Use External Emojis | Custom ack reaction emojis |
|
||||
| Embed Links | Exec approval prompts |
|
||||
|
||||
OAuth2 scopes: `bot`, `applications.commands`
|
||||
|
||||
Privileged intents (in Developer Portal → Bot):
|
||||
- Message Content Intent (required)
|
||||
- Server Members Intent (recommended)
|
||||
|
||||
---
|
||||
|
||||
## Example Configs
|
||||
|
||||
### Minimal (just the basics)
|
||||
```json
|
||||
{
|
||||
"discord": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
Uses all defaults: reply threading on first message, history on, ack 👀, typing on, reactions on own messages, slash commands on.
|
||||
|
||||
### Privacy-focused
|
||||
```json
|
||||
{
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"history_enabled": false,
|
||||
"ack_reaction": "",
|
||||
"reaction_mode": "off",
|
||||
"slash_commands": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Full control with approvals
|
||||
```json
|
||||
{
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"reply_to_mode": "all",
|
||||
"history_limit": 50,
|
||||
"ack_reaction": "⏳",
|
||||
"exec_approvals": true,
|
||||
"exec_approval_tools": ["Bash", "Write", "Edit", "WebFetch"],
|
||||
"channel_overrides": {
|
||||
"123456789": { "history_limit": 100 },
|
||||
"987654321": { "history_enabled": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
317
docs/project/discord-setup.md
Normal file
317
docs/project/discord-setup.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Discord Bot Setup Guide
|
||||
|
||||
> Complete guide to creating a Discord bot and connecting it to Aetheel.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Create a Discord Application](#step-1-create-a-discord-application)
|
||||
3. [Create the Bot](#step-2-create-the-bot)
|
||||
4. [Enable Privileged Intents](#step-3-enable-privileged-intents)
|
||||
5. [Invite the Bot to Your Server](#step-4-invite-the-bot-to-your-server)
|
||||
6. [Configure Aetheel](#step-5-configure-aetheel)
|
||||
7. [Run and Test](#step-6-run-and-test)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
9. [Architecture Reference](#architecture-reference)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aetheel connects to Discord using the **Gateway API** via `discord.py`, which means:
|
||||
- ✅ **No public URL needed** — works behind firewalls and NAT
|
||||
- ✅ **No webhook setup** — Discord pushes events via WebSocket
|
||||
- ✅ **Real-time** — instant message delivery
|
||||
- ✅ **DMs + @mentions** — responds in both
|
||||
|
||||
### What You'll Need
|
||||
|
||||
| Item | Description |
|
||||
|------|-------------|
|
||||
| **Discord Account** | With access to a server where you have Manage Server permissions |
|
||||
| **Bot Token** | From the Discord Developer Portal |
|
||||
| **Python 3.14+** | Runtime for the Aetheel service |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create a Discord Application
|
||||
|
||||
1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications)
|
||||
2. Click **"New Application"**
|
||||
3. Enter a name: `Aetheel` (or any name you prefer)
|
||||
4. Accept the Terms of Service
|
||||
5. Click **"Create"**
|
||||
|
||||
You'll be taken to your application's **General Information** page.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create the Bot
|
||||
|
||||
1. Navigate to **Bot** in the left sidebar
|
||||
2. The bot user is created automatically with your application
|
||||
3. Click **"Reset Token"** to generate a new bot token
|
||||
4. **⚠️ Copy the token now!** You won't be able to see it again.
|
||||
- Save it somewhere safe — this is your `DISCORD_BOT_TOKEN`
|
||||
|
||||
### Optional: Bot Settings
|
||||
|
||||
On the same Bot page, you can configure:
|
||||
|
||||
| Setting | Recommended | Why |
|
||||
|---------|-------------|-----|
|
||||
| **Public Bot** | OFF | Only you can invite it to servers |
|
||||
| **Requires OAuth2 Code Grant** | OFF | Simpler invite flow |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Enable Privileged Intents
|
||||
|
||||
Still on the **Bot** page, scroll down to **Privileged Gateway Intents**.
|
||||
|
||||
Enable the following:
|
||||
|
||||
| Intent | Required | Purpose |
|
||||
|--------|----------|---------|
|
||||
| **Message Content Intent** | ✅ Yes | Read the text content of messages |
|
||||
| **Server Members Intent** | Optional | Resolve member display names |
|
||||
| **Presence Intent** | No | Not needed |
|
||||
|
||||
> **Important:** The Message Content Intent is required. Without it, the bot will receive empty message content for guild messages.
|
||||
|
||||
Click **"Save Changes"**.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Invite the Bot to Your Server
|
||||
|
||||
1. Navigate to **OAuth2** → **URL Generator** in the left sidebar
|
||||
2. Under **Scopes**, check: `bot`
|
||||
3. Under **Bot Permissions**, check:
|
||||
|
||||
| Permission | Purpose |
|
||||
|------------|---------|
|
||||
| **Send Messages** | Reply to users |
|
||||
| **Read Message History** | Context for conversations |
|
||||
| **View Channels** | See channels the bot is in |
|
||||
|
||||
4. Copy the **Generated URL** at the bottom
|
||||
5. Open the URL in your browser
|
||||
6. Select the server you want to add the bot to
|
||||
7. Click **"Authorize"**
|
||||
|
||||
The bot should now appear in your server's member list (offline until you start Aetheel).
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Configure Aetheel
|
||||
|
||||
### Option A: Using config.json (recommended)
|
||||
|
||||
Edit `~/.aetheel/config.json` and add your token to the `env.vars` block:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"DISCORD_BOT_TOKEN": "your-discord-bot-token-here"
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"bot_token": "${DISCORD_BOT_TOKEN}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Export environment variable
|
||||
|
||||
```bash
|
||||
export DISCORD_BOT_TOKEN="your-discord-bot-token-here"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Run and Test
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
# or: pip install -r requirements.txt
|
||||
```
|
||||
|
||||
This will install `discord.py` along with the other dependencies.
|
||||
|
||||
### Run the bot
|
||||
|
||||
```bash
|
||||
# Discord only
|
||||
uv run python main.py --discord
|
||||
|
||||
# Discord + Slack together
|
||||
uv run python main.py --discord
|
||||
|
||||
# Discord with Claude Code runtime
|
||||
uv run python main.py --discord --claude
|
||||
|
||||
# Test mode (echo handler, no AI)
|
||||
uv run python main.py --discord --test
|
||||
|
||||
# Debug logging
|
||||
uv run python main.py --discord --log DEBUG
|
||||
```
|
||||
|
||||
### Verify it's working
|
||||
|
||||
1. Check the console — you should see:
|
||||
```
|
||||
Aetheel Discord Adapter
|
||||
Bot: @Aetheel (123456789)
|
||||
Guilds: My Server
|
||||
```
|
||||
2. In Discord, go to a channel where the bot is present
|
||||
3. Type `@Aetheel help` — you should see the help response
|
||||
4. Type `@Aetheel status` — you should see the bot's status
|
||||
5. Send a DM to the bot — it should respond directly
|
||||
|
||||
### How the bot responds
|
||||
|
||||
| Context | Trigger | Session Isolation |
|
||||
|---------|---------|-------------------|
|
||||
| **Guild channel** | @mention only | Per-channel |
|
||||
| **DM** | Any message | Per-DM channel |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### ❌ "Discord bot token is required"
|
||||
|
||||
**Problem:** `DISCORD_BOT_TOKEN` is not set or empty.
|
||||
|
||||
**Fix:**
|
||||
1. Check your `config.json` has the token in `env.vars`
|
||||
2. Make sure there are no extra spaces or quotes
|
||||
3. Verify the token is from the Bot page, not the application client secret
|
||||
|
||||
### ❌ Bot comes online but doesn't respond to messages
|
||||
|
||||
**Problem:** Message Content Intent is not enabled.
|
||||
|
||||
**Fix:**
|
||||
1. Go to [Developer Portal](https://discord.com/developers/applications) → your app → **Bot**
|
||||
2. Scroll to **Privileged Gateway Intents**
|
||||
3. Enable **Message Content Intent**
|
||||
4. Save and restart Aetheel
|
||||
|
||||
### ❌ Bot doesn't respond in guild channels
|
||||
|
||||
**Problem:** You're not @mentioning the bot.
|
||||
|
||||
**Fix:**
|
||||
- In guild channels, the bot only responds to @mentions: `@Aetheel hello`
|
||||
- In DMs, the bot responds to any message — no @mention needed
|
||||
|
||||
### ❌ "Improper token has been passed"
|
||||
|
||||
**Problem:** The token is malformed or from the wrong place.
|
||||
|
||||
**Fix:**
|
||||
1. Go to **Bot** page in the Developer Portal
|
||||
2. Click **"Reset Token"** to generate a fresh one
|
||||
3. Copy the full token (it's a long string)
|
||||
4. Make sure you're using the Bot token, not the Client ID or Client Secret
|
||||
|
||||
### ❌ "discord.errors.PrivilegedIntentsRequired"
|
||||
|
||||
**Problem:** You're requesting intents that aren't enabled in the portal.
|
||||
|
||||
**Fix:**
|
||||
1. Go to **Bot** → **Privileged Gateway Intents**
|
||||
2. Enable **Message Content Intent**
|
||||
3. Save changes and restart
|
||||
|
||||
### ❌ Bot is offline in the member list
|
||||
|
||||
**Problem:** Aetheel isn't running, or the token is wrong.
|
||||
|
||||
**Fix:**
|
||||
1. Start Aetheel with `--discord` flag
|
||||
2. Check the console for connection errors
|
||||
3. Verify the token in `config.json` matches the one in the Developer Portal
|
||||
|
||||
### ❌ "Missing Permissions" when sending messages
|
||||
|
||||
**Problem:** The bot doesn't have Send Messages permission in that channel.
|
||||
|
||||
**Fix:**
|
||||
1. Check the channel's permission overrides for the bot role
|
||||
2. Re-invite the bot with the correct permissions (see Step 4)
|
||||
3. Or manually grant the bot's role Send Messages in Server Settings → Roles
|
||||
|
||||
---
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Your Discord │
|
||||
│ Server │
|
||||
│ │
|
||||
│ #general │
|
||||
│ #random │
|
||||
│ DMs │
|
||||
└──────┬───────────────┘
|
||||
│ WebSocket (Gateway API)
|
||||
│
|
||||
┌──────▼───────────────┐
|
||||
│ Aetheel Discord │
|
||||
│ Adapter │
|
||||
│ │
|
||||
│ • Token resolution │
|
||||
│ • @mention filter │
|
||||
│ • DM handling │
|
||||
│ • Message chunking │
|
||||
│ (2000 char limit) │
|
||||
│ • Async dispatch │
|
||||
│ via to_thread() │
|
||||
└──────┬───────────────┘
|
||||
│ Callback
|
||||
│
|
||||
┌──────▼───────────────┐
|
||||
│ Message Handler │
|
||||
│ │
|
||||
│ • Echo (test) │
|
||||
│ • AI (OpenCode / │
|
||||
│ Claude Code) │
|
||||
│ • Memory + Skills │
|
||||
│ • Action tags │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `adapters/discord_adapter.py` | Core Discord adapter (Gateway, send/receive) |
|
||||
| `adapters/base.py` | Abstract base class all adapters implement |
|
||||
| `main.py` | Entry point — `--discord` flag enables this adapter |
|
||||
| `~/.aetheel/config.json` | Your Discord token (in `env.vars` block) |
|
||||
|
||||
### Comparison with Other Adapters
|
||||
|
||||
| Feature | Slack | Telegram | Discord |
|
||||
|---------|-------|----------|---------|
|
||||
| **Library** | `slack_bolt` | `python-telegram-bot` | `discord.py` |
|
||||
| **Connection** | Socket Mode (WebSocket) | Long polling | Gateway (WebSocket) |
|
||||
| **Auth** | Bot Token + App Token | Single Bot Token | Single Bot Token |
|
||||
| **Trigger (channels)** | @mention | @mention | @mention |
|
||||
| **Trigger (DMs)** | Any message | Any message | Any message |
|
||||
| **Text Limit** | 4000 chars | 4096 chars | 2000 chars |
|
||||
| **Threading** | Native threads | Reply-to-message | N/A (per-channel) |
|
||||
| **Session Isolation** | Per-thread | Per-chat | Per-channel |
|
||||
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
|
||||
```
|
||||
321
docs/project/future-changes.md
Normal file
321
docs/project/future-changes.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# Future Changes
|
||||
|
||||
Planned features and architectural improvements for Aetheel.
|
||||
|
||||
---
|
||||
|
||||
## Agent Teams & Delegation
|
||||
|
||||
The current subagent system spawns generic background workers that share the main agent's identity. The goal is to evolve this into a proper agent team architecture where specialized agents with their own roles, tools, and identity files can be created and coordinated.
|
||||
|
||||
### Vision
|
||||
|
||||
A main agent (e.g. "CTO") talks to the user and delegates tasks to specialist agents (e.g. "Programmer", "Designer") that each have their own personality, tool access, and domain expertise. Results flow back through the main agent or directly to the channel.
|
||||
|
||||
```
|
||||
User ──► Main Agent (CTO)
|
||||
│
|
||||
├──► Programmer Agent
|
||||
│ • Own SOUL.md (code-focused personality)
|
||||
│ • Tools: Bash, Read, Write, Edit, Grep
|
||||
│ • Model: claude-sonnet for speed
|
||||
│
|
||||
├──► Designer Agent
|
||||
│ • Own SOUL.md (design-focused personality)
|
||||
│ • Tools: WebSearch, WebFetch, Read
|
||||
│ • Model: claude-opus for creativity
|
||||
│
|
||||
└──► Researcher Agent
|
||||
• Own SOUL.md (thorough, citation-heavy)
|
||||
• Tools: WebSearch, WebFetch
|
||||
• Model: gemini-2.5-pro for large context
|
||||
```
|
||||
|
||||
### What Exists Today
|
||||
|
||||
- `SubagentManager` spawns background tasks via `[ACTION:spawn|<task>]`
|
||||
- `SubagentBus` provides pub/sub messaging between subagents
|
||||
- Claude Code's `Task`, `TaskOutput`, `TeamCreate`, `SendMessage` tools are already in the allowed tools list
|
||||
- All subagents currently share the same identity files (SOUL.md, USER.md, MEMORY.md)
|
||||
|
||||
### How NanoClaw Does It
|
||||
|
||||
NanoClaw (the inspiration project) takes a fundamentally different approach — container-based isolation with per-group identity:
|
||||
|
||||
**Architecture:**
|
||||
- Each chat group gets its own folder (`groups/{name}/`) with its own `CLAUDE.md` (identity/memory)
|
||||
- A `groups/global/CLAUDE.md` provides shared context readable by all groups
|
||||
- Each agent runs inside an Apple Container (lightweight Linux VM) with only its own folder mounted
|
||||
- The main channel has elevated privileges (can manage groups, write global memory, schedule tasks for any group)
|
||||
- Non-main groups can only access their own folder + read-only global memory
|
||||
|
||||
**Agent Teams (Swarm):**
|
||||
- Claude Code has native `TeamCreate`, `SendMessage` tools for multi-agent orchestration
|
||||
- NanoClaw enables this via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` env var in the container
|
||||
- The lead agent creates teammates using Claude's built-in team tools
|
||||
- Each teammate gets instructions to use `send_message` with a `sender` parameter
|
||||
- On Telegram, each sender gets a dedicated bot identity (pool of pre-created bots renamed dynamically)
|
||||
- The lead agent coordinates but doesn't relay every teammate message — users see them directly
|
||||
|
||||
**Key design decisions:**
|
||||
- Identity is per-group, not per-agent — a "programmer" agent in one group is different from a "programmer" in another
|
||||
- The AI itself decides when to create teams and how to structure them — it's not pre-configured
|
||||
- Container isolation means agents can't read each other's files or sessions
|
||||
- IPC is file-based (JSON files in a watched directory), not in-memory pub/sub
|
||||
- Secrets are passed via stdin, never mounted as files
|
||||
|
||||
### What Needs to Be Built
|
||||
|
||||
#### 1. Agent Definitions
|
||||
|
||||
A new `agents/` directory in the workspace where each agent gets its own folder:
|
||||
|
||||
```
|
||||
~/.aetheel/workspace/agents/
|
||||
├── programmer/
|
||||
│ ├── AGENT.md # Role, description, triggers
|
||||
│ ├── SOUL.md # Agent-specific personality
|
||||
│ └── SKILL.md # Optional: domain-specific skills
|
||||
├── designer/
|
||||
│ ├── AGENT.md
|
||||
│ ├── SOUL.md
|
||||
│ └── SKILL.md
|
||||
└── researcher/
|
||||
├── AGENT.md
|
||||
└── SOUL.md
|
||||
```
|
||||
|
||||
`AGENT.md` frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: programmer
|
||||
description: Senior software engineer specializing in Python and system design
|
||||
triggers: [code, implement, build, fix, debug, refactor, PR]
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
engine: claude
|
||||
tools: [Bash, Read, Write, Edit, Glob, Grep]
|
||||
max_turns: 5
|
||||
---
|
||||
```
|
||||
|
||||
#### 2. Agent-Aware Routing
|
||||
|
||||
The main agent decides which specialist to delegate to based on:
|
||||
- Explicit user request ("ask the programmer to...")
|
||||
- Trigger word matching from AGENT.md
|
||||
- AI-driven routing (the main agent's system prompt tells it about available agents)
|
||||
|
||||
New action tag: `[ACTION:delegate|<agent_name>|<task>]`
|
||||
|
||||
#### 3. Per-Agent Identity
|
||||
|
||||
Each agent gets its own system prompt built from:
|
||||
1. Its own `SOUL.md` (personality/role)
|
||||
2. Shared `USER.md` (user context is universal)
|
||||
3. Shared `MEMORY.md` (long-term memory is universal)
|
||||
4. Its own skills from `SKILL.md`
|
||||
5. Its tool restrictions from `AGENT.md`
|
||||
|
||||
#### 4. Agent-to-Agent Communication
|
||||
|
||||
Extend `SubagentBus` to support:
|
||||
- Named channels per agent (not just task IDs)
|
||||
- Request/response patterns (agent A asks agent B, waits for reply)
|
||||
- Broadcast messages (main agent sends context to all agents)
|
||||
- Result aggregation (main agent collects results from multiple agents)
|
||||
|
||||
#### 5. Team Orchestration Patterns
|
||||
|
||||
- **Sequential**: Programmer writes code → Designer reviews UI → Main agent summarizes
|
||||
- **Parallel**: Programmer and Designer work simultaneously, main agent merges results
|
||||
- **Supervisory**: Main agent reviews each agent's output before sending to user
|
||||
- **Collaborative**: Agents can message each other directly via the bus
|
||||
|
||||
#### 6. Chat Commands
|
||||
|
||||
```
|
||||
team list # List defined agents
|
||||
team status # Show which agents are active
|
||||
team create <name> # Interactive agent creation
|
||||
team remove <name> # Remove an agent definition
|
||||
delegate <agent> <task> # Manually delegate to a specific agent
|
||||
```
|
||||
|
||||
#### 7. Claude Code Native Teams
|
||||
|
||||
Claude Code already has `TeamCreate`, `TeamDelete`, `SendMessage` tools. These allow the AI itself to create and manage teams during a session. The integration path:
|
||||
|
||||
- When using Claude Code engine, the AI can use these tools natively
|
||||
- Aetheel wraps the results and routes messages back to the right channel
|
||||
- Agent definitions in `AGENT.md` pre-configure teams that get created on startup
|
||||
|
||||
### Implementation Priority
|
||||
|
||||
There are two paths — pick based on complexity appetite:
|
||||
|
||||
**Path A: Lightweight (extend current SubagentManager)**
|
||||
1. Agent definition format (`AGENT.md`) and discovery
|
||||
2. Per-agent identity file loading (own SOUL.md, shared USER.md/MEMORY.md)
|
||||
3. `[ACTION:delegate|agent|task]` tag parsing
|
||||
4. Agent-specific system prompt building with tool restrictions
|
||||
5. Chat commands for team management
|
||||
6. Agent-to-agent communication via extended bus
|
||||
|
||||
**Path B: Full isolation (NanoClaw-style containers)**
|
||||
1. Enable `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` for Claude Code runtime
|
||||
2. Per-group workspace folders with own CLAUDE.md
|
||||
3. Container-based agent execution (Docker or Apple Container)
|
||||
4. File-based IPC between host and containers
|
||||
5. Mount security (allowlist, per-group isolation)
|
||||
6. Bot pool for Telegram/Discord (each agent gets its own bot identity)
|
||||
7. Lead agent orchestration via Claude's native team tools
|
||||
|
||||
Path A is simpler and works today. Path B is more secure and scalable but requires container infrastructure.
|
||||
|
||||
### NanoClaw Reference Files
|
||||
|
||||
Key files to study in `inspirations/nanoclaw/`:
|
||||
- `src/container-runner.ts` — Container spawning, volume mounts, IPC, per-group isolation
|
||||
- `src/types.ts` — `RegisteredGroup`, `ContainerConfig`, `AdditionalMount` types
|
||||
- `groups/global/CLAUDE.md` — Global identity shared across all groups
|
||||
- `groups/main/CLAUDE.md` — Main channel with elevated privileges
|
||||
- `.claude/skills/add-telegram-swarm/SKILL.md` — Full agent swarm implementation for Telegram
|
||||
- `docs/SPEC.md` — Complete architecture spec
|
||||
- `docs/SECURITY.md` — Security model for container isolation
|
||||
|
||||
---
|
||||
|
||||
## Other Planned Changes
|
||||
|
||||
### WebChat UI Overhaul
|
||||
|
||||
The current chat.html is a minimal single-file UI. Upgrade to match OpenClaw's design language:
|
||||
|
||||
**Design elements from OpenClaw to adopt:**
|
||||
- Color palette: `--bg: #12141a`, `--card: #181b22`, `--accent: #ff5c5c` (or Aetheel's own brand color)
|
||||
- Typography: Space Grotesk for body, JetBrains Mono for code
|
||||
- Chat bubbles: user messages right-aligned with accent background, AI messages left-aligned with card background
|
||||
- Streaming indicator: pulsing border on AI bubble while generating
|
||||
- Tool cards: collapsible cards showing tool usage (web search, file ops, etc.)
|
||||
- Compose area: multi-line textarea with send button, sticky at bottom
|
||||
- Status bar: connection status, current engine/model display
|
||||
|
||||
**New panels (sidebar or tabs):**
|
||||
- Settings panel: engine/model/provider switching, timeout config
|
||||
- MCP Servers panel: add/remove/enable MCP servers
|
||||
- Skills panel: view loaded skills, create new ones
|
||||
- Usage panel: cost tracking, request history, rate limit status
|
||||
- Sessions panel: active sessions, session history
|
||||
|
||||
### MCP Server Management
|
||||
|
||||
Add the ability to manage MCP servers from chat, CLI, and the WebChat UI.
|
||||
|
||||
**Chat commands:**
|
||||
```
|
||||
mcp list # List configured MCP servers
|
||||
mcp add <name> <command> [args...] # Add a new MCP server
|
||||
mcp remove <name> # Remove an MCP server
|
||||
mcp enable <name> # Enable a disabled server
|
||||
mcp disable <name> # Disable without removing
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
mcp add brave-search uvx brave-search-mcp@latest
|
||||
mcp add filesystem npx @anthropic-ai/mcp-filesystem /home/user/projects
|
||||
mcp add github uvx github-mcp-server
|
||||
mcp remove brave-search
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Chat commands write to `config.json` → `mcp.servers` section
|
||||
- `write_mcp_config()` already generates `.mcp.json` (Claude) or `opencode.json` (OpenCode)
|
||||
- After adding/removing, auto-call `write_mcp_config()` and notify user to reload
|
||||
- WebChat UI: form with name, command, args fields + env vars
|
||||
- CLI: `aetheel mcp add/remove/list` subcommands
|
||||
|
||||
**Popular MCP servers to suggest during setup:**
|
||||
- `brave-search` — Web search
|
||||
- `filesystem` — File system access
|
||||
- `github` — GitHub API
|
||||
- `postgres` / `sqlite` — Database access
|
||||
- `puppeteer` — Browser automation
|
||||
- `memory` — Persistent memory server
|
||||
|
||||
### Skills System Enhancement
|
||||
|
||||
#### Skill Creator (via chat)
|
||||
|
||||
The AI should be able to create new skills when asked. This works by having the AI write a `SKILL.md` file to the workspace.
|
||||
|
||||
**How it works today:**
|
||||
- The system prompt already tells the AI: "You can create new skills by writing SKILL.md files to ~/.aetheel/workspace/skills/<name>/SKILL.md"
|
||||
- The AI has file write access via its runtime tools (Bash, Write, Edit)
|
||||
- So skill creation via natural language already works — just ask: "Create a skill for checking weather"
|
||||
|
||||
**What to add:**
|
||||
- A `skill` chat command for explicit management:
|
||||
```
|
||||
skill list # List all loaded skills
|
||||
skill create <name> # Interactive skill creation wizard
|
||||
skill remove <name> # Remove a skill
|
||||
skill reload # Reload all skills (same as /reload)
|
||||
skill show <name> # Show a skill's SKILL.md content
|
||||
```
|
||||
- Skill templates: pre-built SKILL.md templates for common use cases
|
||||
- Skill import from URL: `skill import https://github.com/user/repo/SKILL.md`
|
||||
|
||||
#### ClawhHub / Community Skills
|
||||
|
||||
Integration with external skill sources:
|
||||
|
||||
- **ClawHub.ai**: If they provide an API or registry, add `skill search <query>` and `skill install <name>` commands
|
||||
- **GitHub Anthropic skills**: Import from `github.com/anthropics/claude-code/tree/main/skills/`
|
||||
- **User-defined skill repos**: `skill import <git-url>` clones a repo's skills into the workspace
|
||||
|
||||
**Implementation:**
|
||||
```
|
||||
skill search weather # Search ClawHub/GitHub for skills
|
||||
skill install clawhub/weather # Install from ClawHub
|
||||
skill import https://github.com/user/skills-repo # Import from git
|
||||
```
|
||||
|
||||
Each imported skill gets its own folder under `~/.aetheel/workspace/skills/<name>/` with a `SKILL.md` and optionally a `handler.py`.
|
||||
|
||||
### Security Fixes (from security-audit.md)
|
||||
|
||||
- Path containment check in `memory/manager.py` `read_file()`
|
||||
- Mandatory webhook auth when enabled
|
||||
- Input schema validation on webhook POST bodies
|
||||
- Stricter cron expression validation
|
||||
- Rate limiting on HTTP endpoints
|
||||
- WebSocket authentication for WebChat
|
||||
- Dependency version pinning
|
||||
|
||||
### Persistent Usage Stats
|
||||
|
||||
Currently usage stats reset on restart. Persist to SQLite so `usage` command shows lifetime stats, daily/weekly/monthly breakdowns, and cost trends.
|
||||
|
||||
### Multi-Model Routing — ✅ Done
|
||||
|
||||
Per-task model routing is implemented via the `models` config section. Different task types (heartbeat, subagent, default chat) can each use a different model, provider, and engine:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"heartbeat": { "model": "ollama/llama3.2", "provider": "ollama" },
|
||||
"subagent": { "model": "minimax/minimax-m1", "provider": "minimax" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Future extensions:
|
||||
- Auto-routing based on message complexity (short → cheap model, complex → powerful model)
|
||||
- Per-channel model overrides
|
||||
- Cost-aware routing (switch to cheaper model when budget threshold is hit)
|
||||
|
||||
### Conversation Branching
|
||||
|
||||
Allow users to fork a conversation into a new thread with a different model or agent, then merge results back.
|
||||
371
docs/project/memory-system.md
Normal file
371
docs/project/memory-system.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# Aetheel Memory System
|
||||
|
||||
> **Date:** 2026-02-13
|
||||
> **Inspired by:** OpenClaw's `src/memory/` (49 files, 2,300+ LOC manager)
|
||||
> **Implementation:** ~600 lines of Python across 6 modules
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Architecture](#architecture)
|
||||
3. [File Structure](#file-structure)
|
||||
4. [Identity Files](#identity-files)
|
||||
5. [How It Works](#how-it-works)
|
||||
6. [Configuration](#configuration)
|
||||
7. [API Reference](#api-reference)
|
||||
8. [Dependencies](#dependencies)
|
||||
9. [Testing](#testing)
|
||||
10. [OpenClaw Mapping](#openclaw-mapping)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The memory system gives Aetheel **persistent, searchable memory** using a combination of markdown files and SQLite. It follows the same design as OpenClaw's memory architecture:
|
||||
|
||||
- **Markdown IS the database** — identity files (`SOUL.md`, `USER.md`, `MEMORY.md`) are human-readable and editable in any text editor or Obsidian
|
||||
- **Hybrid search** — combines vector similarity (cosine, 0.7 weight) with BM25 keyword search (0.3 weight) for accurate retrieval
|
||||
- **Fully local** — uses fastembed ONNX embeddings (384-dim), zero API calls
|
||||
- **Incremental sync** — only re-indexes files that have changed (SHA-256 hash comparison)
|
||||
- **Session logging** — conversation transcripts stored in `daily/` and indexed for search
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ MemoryManager │
|
||||
│ (memory/manager.py) │
|
||||
├──────────────────────────┤
|
||||
│ • sync() │
|
||||
│ • search() │
|
||||
│ • log_session() │
|
||||
│ • read/update identity │
|
||||
│ • file watching │
|
||||
└────────┬─────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ Workspace │ │ SQLite │ │ fastembed │
|
||||
│ (.md files)│ │ Database │ │ (ONNX) │
|
||||
├──────────────┤ ├─────────────┤ ├──────────────┤
|
||||
│ SOUL.md │ │ files │ │ bge-small │
|
||||
│ USER.md │ │ chunks │ │ 384-dim │
|
||||
│ MEMORY.md │ │ chunks_fts │ │ L2-normalized│
|
||||
│ memory/ │ │ emb_cache │ │ local only │
|
||||
│ daily/ │ │ session_logs│ │ │
|
||||
└──────────────┘ └─────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Search Flow
|
||||
|
||||
```
|
||||
Query: "what are my preferences?"
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ Vector Search │ │ Keyword Search │
|
||||
│ (cosine sim) │ │ (FTS5 / BM25) │
|
||||
│ weight: 0.7 │ │ weight: 0.3 │
|
||||
└────────┬─────────┘ └────────┬─────────┘
|
||||
│ │
|
||||
└──────────┬─────────────┘
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Hybrid Merge │
|
||||
│ dedupe by ID │
|
||||
│ sort by score│
|
||||
└───────┬───────┘
|
||||
▼
|
||||
Top-N results with
|
||||
score ≥ min_score
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. File Structure
|
||||
|
||||
### Source Code
|
||||
|
||||
```
|
||||
memory/
|
||||
├── __init__.py # Package exports (MemoryManager, MemorySearchResult, MemorySource)
|
||||
├── types.py # Data classes: MemoryConfig, MemorySearchResult, MemoryChunk, etc.
|
||||
├── internal.py # Utilities: hashing, chunking, file discovery, cosine similarity
|
||||
├── hybrid.py # Hybrid search merging (0.7 vector + 0.3 BM25)
|
||||
├── schema.py # SQLite schema (files, chunks, FTS5, embedding cache)
|
||||
├── embeddings.py # Local fastembed ONNX embeddings (384-dim)
|
||||
└── manager.py # Main MemoryManager orchestrator (~400 LOC)
|
||||
```
|
||||
|
||||
### Workspace (Created Automatically)
|
||||
|
||||
```
|
||||
~/.aetheel/workspace/
|
||||
├── SOUL.md # Personality & values — "who you are"
|
||||
├── USER.md # User profile — "who I am"
|
||||
├── MEMORY.md # Long-term memory — decisions, lessons, context
|
||||
├── memory/ # Additional markdown memory files (optional)
|
||||
│ └── *.md
|
||||
└── daily/ # Session logs by date
|
||||
├── 2026-02-13.md
|
||||
├── 2026-02-14.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Identity Files
|
||||
|
||||
Inspired by OpenClaw's template system (`docs/reference/templates/SOUL.md`).
|
||||
|
||||
### SOUL.md — Who You Are
|
||||
|
||||
The agent's personality, values, and behavioral guidelines. Created with sensible defaults:
|
||||
|
||||
- Core truths (be helpful, have opinions, be resourceful)
|
||||
- Boundaries (privacy, external actions)
|
||||
- Continuity rules (files ARE the memory)
|
||||
|
||||
### USER.md — Who I Am
|
||||
|
||||
The user's profile — name, role, timezone, preferences, current focus, tools. Fill this in to personalize the agent.
|
||||
|
||||
### MEMORY.md — Long-Term Memory
|
||||
|
||||
Persistent decisions, lessons learned, and context that carries across sessions. The agent appends entries with timestamps:
|
||||
|
||||
```markdown
|
||||
### [2026-02-13 12:48]
|
||||
|
||||
Learned that the user prefers concise responses with code examples.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. How It Works
|
||||
|
||||
### Sync (`await manager.sync()`)
|
||||
|
||||
1. **Discover files** — scans `SOUL.md`, `USER.md`, `MEMORY.md`, `memory/*.md`
|
||||
2. **Check hashes** — compares SHA-256 content hash against stored hash in `files` table
|
||||
3. **Skip unchanged** — files with matching hashes are skipped (incremental sync)
|
||||
4. **Chunk** — splits changed files into overlapping text chunks (~512 tokens, 50 token overlap)
|
||||
5. **Embed** — generates 384-dim vectors via fastembed (checks embedding cache first)
|
||||
6. **Store** — inserts chunks + embeddings into SQLite, updates FTS5 index
|
||||
7. **Clean** — removes stale entries for deleted files
|
||||
8. **Sessions** — repeats for `daily/*.md` session log files
|
||||
|
||||
### Search (`await manager.search("query")`)
|
||||
|
||||
1. **Auto-sync** — triggers sync if workspace is dirty (configurable)
|
||||
2. **Keyword search** — runs FTS5 `MATCH` query with BM25 ranking
|
||||
3. **Vector search** — embeds query, computes cosine similarity against all chunk embeddings
|
||||
4. **Hybrid merge** — combines results: `score = 0.7 × vector + 0.3 × keyword`
|
||||
5. **Deduplicate** — merges chunks found by both methods (by chunk ID)
|
||||
6. **Filter & rank** — removes results below `min_score`, returns top-N sorted by score
|
||||
|
||||
### Session Logging (`manager.log_session(content)`)
|
||||
|
||||
1. Creates/appends to `daily/YYYY-MM-DD.md`
|
||||
2. Adds timestamped entry with channel label
|
||||
3. Marks index as dirty for next sync
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration
|
||||
|
||||
```python
|
||||
from memory.types import MemoryConfig
|
||||
|
||||
config = MemoryConfig(
|
||||
# Workspace directory containing identity files
|
||||
workspace_dir="~/.aetheel/workspace",
|
||||
|
||||
# SQLite database path
|
||||
db_path="~/.aetheel/memory.db",
|
||||
|
||||
# Chunking parameters
|
||||
chunk_tokens=512, # ~2048 characters per chunk
|
||||
chunk_overlap=50, # ~200 character overlap between chunks
|
||||
|
||||
# Search parameters
|
||||
max_results=10, # maximum results per search
|
||||
min_score=0.1, # minimum hybrid score threshold
|
||||
vector_weight=0.7, # weight for vector similarity
|
||||
text_weight=0.3, # weight for BM25 keyword score
|
||||
|
||||
# Embedding model (local ONNX)
|
||||
embedding_model="BAAI/bge-small-en-v1.5",
|
||||
embedding_dims=384,
|
||||
|
||||
# Sync behavior
|
||||
watch=True, # enable file watching via watchdog
|
||||
watch_debounce_ms=2000, # debounce file change events
|
||||
sync_on_search=True, # auto-sync before search if dirty
|
||||
|
||||
# Session logs directory (defaults to workspace_dir/daily/)
|
||||
sessions_dir=None,
|
||||
|
||||
# Sources to index
|
||||
sources=["memory", "sessions"],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API Reference
|
||||
|
||||
### `MemoryManager`
|
||||
|
||||
```python
|
||||
from memory import MemoryManager
|
||||
from memory.types import MemoryConfig
|
||||
|
||||
# Create with custom config (or defaults)
|
||||
mgr = MemoryManager(config=MemoryConfig(...))
|
||||
|
||||
# Sync workspace → index
|
||||
stats = await mgr.sync(force=False)
|
||||
# Returns: {"files_found": 4, "files_indexed": 4, "chunks_created": 5, ...}
|
||||
|
||||
# Hybrid search
|
||||
results = await mgr.search("what are my preferences?", max_results=5, min_score=0.1)
|
||||
# Returns: list[MemorySearchResult]
|
||||
# .path — relative file path (e.g., "USER.md")
|
||||
# .start_line — chunk start line
|
||||
# .end_line — chunk end line
|
||||
# .score — hybrid score (0.0 - 1.0)
|
||||
# .snippet — text snippet (max 700 chars)
|
||||
# .source — MemorySource.MEMORY or MemorySource.SESSIONS
|
||||
|
||||
# Identity files
|
||||
soul = mgr.read_soul() # Read SOUL.md
|
||||
user = mgr.read_user() # Read USER.md
|
||||
memory = mgr.read_long_term_memory() # Read MEMORY.md
|
||||
mgr.append_to_memory("learned X") # Append timestamped entry to MEMORY.md
|
||||
mgr.update_identity_file("USER.md", new_content) # Overwrite a file
|
||||
|
||||
# Session logging
|
||||
path = mgr.log_session("User: hi\nAssistant: hello", channel="slack")
|
||||
|
||||
# File reading
|
||||
data = mgr.read_file("SOUL.md", from_line=1, num_lines=10)
|
||||
|
||||
# Status
|
||||
status = mgr.status()
|
||||
# Returns: {"files": 5, "chunks": 5, "cached_embeddings": 4, ...}
|
||||
|
||||
# File watching
|
||||
mgr.start_watching() # auto-mark dirty on workspace changes
|
||||
mgr.stop_watching()
|
||||
|
||||
# Cleanup
|
||||
mgr.close()
|
||||
```
|
||||
|
||||
### `MemorySearchResult`
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MemorySearchResult:
|
||||
path: str # Relative path to the markdown file
|
||||
start_line: int # First line of the matching chunk
|
||||
end_line: int # Last line of the matching chunk
|
||||
score: float # Hybrid score (0.0 - 1.0)
|
||||
snippet: str # Text snippet (max 700 characters)
|
||||
source: MemorySource # "memory" or "sessions"
|
||||
citation: str | None = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `fastembed` | 0.7.4 | Local ONNX embeddings (BAAI/bge-small-en-v1.5, 384-dim) |
|
||||
| `watchdog` | 6.0.0 | File system watching for auto re-indexing |
|
||||
| `sqlite3` | (stdlib) | Database engine with FTS5 full-text search |
|
||||
|
||||
Added to `pyproject.toml`:
|
||||
```toml
|
||||
dependencies = [
|
||||
"fastembed>=0.7.4",
|
||||
"watchdog>=6.0.0",
|
||||
# ... existing deps
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing
|
||||
|
||||
Run the smoke test:
|
||||
|
||||
```bash
|
||||
uv run python test_memory.py
|
||||
```
|
||||
|
||||
### Test Results (2026-02-13)
|
||||
|
||||
| Test | Result |
|
||||
|------|--------|
|
||||
| `hash_text()` | ✅ SHA-256 produces 64-char hex string |
|
||||
| `chunk_markdown()` | ✅ Splits text into overlapping chunks with correct line numbers |
|
||||
| Identity file creation | ✅ SOUL.md (793 chars), USER.md (417 chars), MEMORY.md (324 chars) |
|
||||
| Append to MEMORY.md | ✅ Content grows with timestamped entry |
|
||||
| Session logging | ✅ Creates `daily/2026-02-13.md` with channel + timestamp |
|
||||
| Sync (first run) | ✅ 4 files found, 4 indexed, 5 chunks, 1 session |
|
||||
| Search "personality values" | ✅ 5 results — top: SOUL.md (score 0.595) |
|
||||
| Search "preferences" | ✅ 5 results — top: USER.md (score 0.583) |
|
||||
| FTS5 keyword search | ✅ Available |
|
||||
| Embedding cache | ✅ 4 entries cached (skip re-computation on next sync) |
|
||||
| Status report | ✅ All fields populated correctly |
|
||||
|
||||
---
|
||||
|
||||
## 10. OpenClaw Mapping
|
||||
|
||||
How our Python implementation maps to OpenClaw's TypeScript source:
|
||||
|
||||
| OpenClaw File | Aetheel File | Description |
|
||||
|---------------|-------------|-------------|
|
||||
| `src/memory/types.ts` | `memory/types.py` | Core types (MemorySearchResult, MemorySource, etc.) |
|
||||
| `src/memory/internal.ts` | `memory/internal.py` | hashText, chunkMarkdown, listMemoryFiles, cosineSimilarity |
|
||||
| `src/memory/hybrid.ts` | `memory/hybrid.py` | buildFtsQuery, bm25RankToScore, mergeHybridResults |
|
||||
| `src/memory/memory-schema.ts` | `memory/schema.py` | ensureMemoryIndexSchema → ensure_schema |
|
||||
| `src/memory/embeddings.ts` | `memory/embeddings.py` | createEmbeddingProvider → embed_query/embed_batch (fastembed) |
|
||||
| `src/memory/manager.ts` (2,300 LOC) | `memory/manager.py` (~400 LOC) | MemoryIndexManager → MemoryManager |
|
||||
| `src/memory/sync-memory-files.ts` | Inlined in `manager.py` | syncMemoryFiles → _run_sync |
|
||||
| `src/memory/session-files.ts` | Inlined in `manager.py` | buildSessionEntry → _sync_session_files |
|
||||
| `docs/reference/templates/SOUL.md` | Auto-created by manager | Default identity file templates |
|
||||
|
||||
### Key Simplifications vs. OpenClaw
|
||||
|
||||
| Feature | OpenClaw | Aetheel |
|
||||
|---------|----------|---------|
|
||||
| **Embedding providers** | OpenAI, Voyage, Gemini, local ONNX (4 providers) | fastembed only (local ONNX, zero API calls) |
|
||||
| **Vector storage** | sqlite-vec extension (C library) | JSON-serialized in chunks table (pure Python) |
|
||||
| **File watching** | chokidar (Node.js) | watchdog (Python) |
|
||||
| **Batch embedding** | OpenAI/Voyage batch APIs, concurrency pools | fastembed batch (single-threaded, local) |
|
||||
| **Config system** | JSON5 + TypeBox + Zod schemas (100k+ LOC) | Simple Python dataclass |
|
||||
| **Codebase** | 49 files, 2,300+ LOC manager alone | 6 files, ~600 LOC total |
|
||||
|
||||
### What We Kept
|
||||
|
||||
- ✅ Same identity file pattern (SOUL.md, USER.md, MEMORY.md)
|
||||
- ✅ Same hybrid search algorithm (0.7 vector + 0.3 BM25)
|
||||
- ✅ Same chunking approach (token-based with overlap)
|
||||
- ✅ Same incremental sync (hash-based change detection)
|
||||
- ✅ Same FTS5 full-text search with BM25 ranking
|
||||
- ✅ Same embedding cache (avoids re-computing unchanged chunks)
|
||||
- ✅ Same session log pattern (daily/ directory)
|
||||
|
||||
---
|
||||
|
||||
*This memory system is Phase 1 of the Aetheel build process as outlined in `openclaw-analysis.md`.*
|
||||
418
docs/project/opencode-setup.md
Normal file
418
docs/project/opencode-setup.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# OpenCode Setup Guide
|
||||
|
||||
> Configure OpenCode CLI as the AI brain for Aetheel.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Install OpenCode](#step-1-install-opencode)
|
||||
3. [Configure a Provider](#step-2-configure-a-provider)
|
||||
4. [Choose a Runtime Mode](#step-3-choose-a-runtime-mode)
|
||||
5. [Configure Aetheel](#step-4-configure-aetheel)
|
||||
6. [Test the Integration](#step-5-test-the-integration)
|
||||
7. [Architecture](#architecture)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aetheel uses [OpenCode](https://opencode.ai) as its AI runtime — the "brain" that
|
||||
generates responses to Slack messages. OpenCode is a terminal-native AI coding agent
|
||||
that supports multiple LLM providers (Anthropic, OpenAI, Google, etc.).
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
Slack Message → Slack Adapter → OpenCode Runtime → LLM → Response → Slack Reply
|
||||
```
|
||||
|
||||
Two runtime modes are available:
|
||||
|
||||
| Mode | Description | Best For |
|
||||
|------|-------------|----------|
|
||||
| **CLI** (default) | Runs `opencode run` as a subprocess per request | Simple setup, no persistent server |
|
||||
| **SDK** | Talks to `opencode serve` via HTTP API | Lower latency, persistent sessions |
|
||||
|
||||
### Relationship to OpenClaw
|
||||
|
||||
This architecture is inspired by OpenClaw's `cli-runner.ts`:
|
||||
- OpenClaw spawns CLI agents (Claude CLI, Codex CLI) as subprocesses
|
||||
- Each CLI call gets: model args, session ID, system prompt, timeout
|
||||
- Output is parsed from JSON/JSONL to extract the response text
|
||||
- Sessions are mapped per-thread for conversation isolation
|
||||
|
||||
We replicate this pattern in Python, adapted for OpenCode's API.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Install OpenCode
|
||||
|
||||
### macOS / Linux (recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### npm (all platforms)
|
||||
|
||||
```bash
|
||||
npm install -g opencode-ai
|
||||
```
|
||||
|
||||
### Homebrew (macOS)
|
||||
|
||||
```bash
|
||||
brew install anomalyco/tap/opencode
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
opencode --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Configure a Provider
|
||||
|
||||
OpenCode needs at least one LLM provider configured. Run:
|
||||
|
||||
```bash
|
||||
opencode auth login
|
||||
```
|
||||
|
||||
This will guide you through connecting to a provider. Options include:
|
||||
|
||||
| Provider | Auth Method |
|
||||
|----------|-------------|
|
||||
| **OpenCode Zen** | Token-based (opencode.ai account) |
|
||||
| **Anthropic** | API key (`ANTHROPIC_API_KEY`) |
|
||||
| **OpenAI** | API key (`OPENAI_API_KEY`) |
|
||||
| **Google** | API key (`GEMINI_API_KEY`) |
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
Alternatively, set provider API keys in your `config.json` → `env.vars` block:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"ANTHROPIC_API_KEY": "sk-ant-...",
|
||||
"OPENAI_API_KEY": "sk-...",
|
||||
"GEMINI_API_KEY": "AI..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or as process environment variables:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
### Verify models are available
|
||||
|
||||
```bash
|
||||
opencode models
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Choose a Runtime Mode
|
||||
|
||||
### CLI Mode (Default — Recommended to Start)
|
||||
|
||||
CLI mode spawns `opencode run` for each message. No persistent server needed.
|
||||
|
||||
**Pros:**
|
||||
- ✅ Simple — just install OpenCode and go
|
||||
- ✅ No server to manage
|
||||
- ✅ Isolated — each request is independent
|
||||
|
||||
**Cons:**
|
||||
- ⚠️ Higher latency (cold start per request)
|
||||
- ⚠️ Limited session continuity (uses `--continue` flag)
|
||||
|
||||
```env
|
||||
OPENCODE_MODE=cli
|
||||
```
|
||||
|
||||
### SDK Mode (Advanced — Lower Latency)
|
||||
|
||||
SDK mode talks to a running `opencode serve` instance via HTTP.
|
||||
|
||||
**Pros:**
|
||||
- ✅ Lower latency (warm server, no cold start)
|
||||
- ✅ Better session management
|
||||
- ✅ Full API access
|
||||
|
||||
**Cons:**
|
||||
- ⚠️ Requires running `opencode serve` separately
|
||||
- ⚠️ Needs the `opencode-ai` Python package
|
||||
|
||||
```env
|
||||
OPENCODE_MODE=sdk
|
||||
```
|
||||
|
||||
#### Start the OpenCode server:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start the headless server
|
||||
opencode serve --port 4096
|
||||
|
||||
# Optional: with authentication
|
||||
OPENCODE_SERVER_PASSWORD=my-secret opencode serve
|
||||
```
|
||||
|
||||
#### Install the Python SDK:
|
||||
|
||||
```bash
|
||||
pip install opencode-ai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Configure Aetheel
|
||||
|
||||
Edit your `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-...",
|
||||
"SLACK_APP_TOKEN": "xapp-..."
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"bot_token": "${SLACK_BOT_TOKEN}",
|
||||
"app_token": "${SLACK_APP_TOKEN}"
|
||||
},
|
||||
"runtime": {
|
||||
"mode": "cli"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Selection
|
||||
|
||||
You can specify a model in config.json or via process env:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": {
|
||||
"model": "anthropic/claude-sonnet-4-20250514"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or override at launch:
|
||||
|
||||
```bash
|
||||
python main.py --model anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Test the Integration
|
||||
|
||||
### 1. Verify OpenCode works standalone
|
||||
|
||||
```bash
|
||||
# Quick test
|
||||
opencode run "What is Python?"
|
||||
|
||||
# With a specific model
|
||||
opencode run --model anthropic/claude-sonnet-4-20250514 "Hello"
|
||||
```
|
||||
|
||||
### 2. Test the runtime directly
|
||||
|
||||
```bash
|
||||
# Quick Python test
|
||||
python -c "
|
||||
from agent.opencode_runtime import OpenCodeRuntime
|
||||
runtime = OpenCodeRuntime()
|
||||
print(runtime.get_status())
|
||||
response = runtime.chat('Hello, what are you?')
|
||||
print(f'Response: {response.text[:200]}')
|
||||
print(f'OK: {response.ok}, Duration: {response.duration_ms}ms')
|
||||
"
|
||||
```
|
||||
|
||||
### 3. Test via Slack
|
||||
|
||||
```bash
|
||||
# Start in test mode first (echo only, no AI)
|
||||
python main.py --test
|
||||
|
||||
# Then start with AI
|
||||
python main.py
|
||||
|
||||
# Or force a specific mode
|
||||
python main.py --cli
|
||||
python main.py --sdk
|
||||
```
|
||||
|
||||
### 4. In Slack
|
||||
|
||||
- Send `status` — see the runtime status
|
||||
- Send `help` — see available commands
|
||||
- Send any question — get an AI response
|
||||
- Reply in a thread — conversation continues in context
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Slack │
|
||||
│ (messages) │
|
||||
└──────┬──────────────┘
|
||||
│ WebSocket
|
||||
│
|
||||
┌──────▼──────────────┐
|
||||
│ Slack Adapter │
|
||||
│ (slack_adapter.py) │
|
||||
│ │
|
||||
│ • Socket Mode │
|
||||
│ • Event handling │
|
||||
│ • Thread isolation │
|
||||
└──────┬──────────────┘
|
||||
│ ai_handler()
|
||||
│
|
||||
┌──────▼──────────────┐
|
||||
│ OpenCode Runtime │
|
||||
│ (opencode_runtime) │
|
||||
│ │
|
||||
│ • Session store │
|
||||
│ • System prompt │
|
||||
│ • Mode routing │
|
||||
└──────┬──────────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
CLI Mode SDK Mode
|
||||
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│ opencode │ │ opencode │
|
||||
│ run │ │ serve API │
|
||||
│ (subproc)│ │ (HTTP/SDK) │
|
||||
└──────────┘ └──────────────┘
|
||||
│ │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ LLM │
|
||||
│ (Anthropic, │
|
||||
│ OpenAI, │
|
||||
│ Gemini) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### How OpenClaw Inspired This
|
||||
|
||||
| OpenClaw Pattern | Aetheel Implementation |
|
||||
|------------------|----------------------|
|
||||
| `cli-runner.ts` → `runCliAgent()` | `opencode_runtime.py` → `OpenCodeRuntime.chat()` |
|
||||
| `cli-backends.ts` → `CliBackendConfig` | `OpenCodeConfig` dataclass |
|
||||
| `buildCliArgs()` | `_build_cli_args()` |
|
||||
| `runCommandWithTimeout()` | `subprocess.run(timeout=...)` |
|
||||
| `parseCliJson()` / `collectText()` | `_parse_cli_output()` / `_collect_text()` |
|
||||
| `pickSessionId()` | `_extract_session_id()` |
|
||||
| `buildSystemPrompt()` | `build_aetheel_system_prompt()` |
|
||||
| Session per thread | `SessionStore` mapping conversation_id → session_id |
|
||||
|
||||
### File Map
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `agent/__init__.py` | Agent package init |
|
||||
| `agent/opencode_runtime.py` | OpenCode runtime (CLI + SDK modes) |
|
||||
| `adapters/slack_adapter.py` | Slack Socket Mode adapter |
|
||||
| `main.py` | Entry point with AI handler |
|
||||
| `docs/opencode-setup.md` | This setup guide |
|
||||
| `docs/slack-setup.md` | Slack bot setup guide |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### ❌ "opencode not found in PATH"
|
||||
|
||||
**Fix:** Install OpenCode:
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
Then verify:
|
||||
```bash
|
||||
opencode --version
|
||||
```
|
||||
|
||||
### ❌ "CLI command failed" or empty responses
|
||||
|
||||
**Check:**
|
||||
1. Verify OpenCode works standalone: `opencode run "Hello"`
|
||||
2. Check that a provider is configured: `opencode auth login`
|
||||
3. Check that the model is available: `opencode models`
|
||||
4. Check your API key is set (e.g., `ANTHROPIC_API_KEY`)
|
||||
|
||||
### ❌ "Request timed out"
|
||||
|
||||
**Fix:** Increase the timeout in config.json:
|
||||
```json
|
||||
{
|
||||
"runtime": { "timeout_seconds": 300 }
|
||||
}
|
||||
```
|
||||
|
||||
Or simplify your prompt — complex prompts take longer.
|
||||
|
||||
### ❌ SDK mode: "connection test failed"
|
||||
|
||||
**Fix:**
|
||||
1. Make sure `opencode serve` is running: `opencode serve --port 4096`
|
||||
2. Check the URL in config.json: `runtime.server_url`
|
||||
3. If using auth, set `OPENCODE_SERVER_PASSWORD` in `env.vars` and when starting the server
|
||||
|
||||
### ❌ "opencode-ai SDK not installed"
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
pip install opencode-ai
|
||||
```
|
||||
|
||||
If you don't want to install the SDK, switch to CLI mode in config.json:
|
||||
```json
|
||||
{
|
||||
"runtime": { "mode": "cli" }
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Responses are cut off or garbled
|
||||
|
||||
This usually means the output format parsing failed.
|
||||
|
||||
**Fix:** Try setting the format to text:
|
||||
```env
|
||||
OPENCODE_FORMAT=text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Memory System** — Add conversation persistence (SQLite)
|
||||
2. **Heartbeat** — Proactive messages via cron/scheduler
|
||||
3. **Skills** — Loadable skill modules (like OpenClaw's skills/)
|
||||
4. **Multi-Channel** — Discord, Telegram adapters
|
||||
179
docs/project/security-audit.md
Normal file
179
docs/project/security-audit.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Aetheel Security Audit
|
||||
|
||||
**Date:** February 17, 2026
|
||||
**Scope:** Full codebase review of all modules
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL
|
||||
|
||||
### 1. Path Traversal in `memory/manager.py` → `read_file()`
|
||||
|
||||
The method accepts absolute paths and resolves them with `os.path.realpath()` but never validates the result is within the workspace directory. An attacker (or the AI itself) could read arbitrary files:
|
||||
|
||||
```python
|
||||
# Current code — no containment check
|
||||
if os.path.isabs(raw):
|
||||
abs_path = os.path.realpath(raw)
|
||||
```
|
||||
|
||||
**Fix:** Add a check like `if not abs_path.startswith(self._workspace_dir): raise ValueError("path outside workspace")`
|
||||
|
||||
### 2. Arbitrary Code Execution via Hook `handler.py` Loading
|
||||
|
||||
`hooks/hooks.py` → `_load_handler` uses `importlib.util.spec_from_file_location` to dynamically load and execute arbitrary Python from `handler.py` files found in the workspace. If an attacker can write a file to `~/.aetheel/workspace/hooks/<name>/handler.py`, they get full code execution. There's no sandboxing, signature verification, or allowlisting.
|
||||
|
||||
### 3. Webhook Auth Defaults to Open Access
|
||||
|
||||
`webhooks/receiver.py` → `_check_auth`:
|
||||
|
||||
```python
|
||||
if not self._config.token:
|
||||
return True # No token configured = open access
|
||||
```
|
||||
|
||||
If the webhook receiver is enabled without a token, anyone on the network can trigger AI actions. The default config writes `"token": ""` which means open access.
|
||||
|
||||
### 4. AI-Controlled Action Tags Execute Without Validation
|
||||
|
||||
`main.py` → `_process_action_tags` parses the AI's response text for action tags like `[ACTION:cron|...]`, `[ACTION:spawn|...]`, and `[ACTION:remind|...]`. The AI can:
|
||||
|
||||
- Schedule arbitrary cron jobs with any expression
|
||||
- Spawn unlimited subagent tasks
|
||||
- Set reminders with any delay
|
||||
|
||||
There's no validation that the AI was asked to do this, no user confirmation, and no rate limiting. A prompt injection attack via any adapter could trigger these.
|
||||
|
||||
---
|
||||
|
||||
## HIGH
|
||||
|
||||
### 5. No Input Validation on Webhook POST Bodies
|
||||
|
||||
`webhooks/receiver.py` — JSON payloads are parsed but never schema-validated. Fields like `channel_id`, `sender`, `channel` are passed through directly. The `body` dict is stored in `raw_event` and could contain arbitrarily large data.
|
||||
|
||||
### 6. No Request Size Limits on HTTP Endpoints
|
||||
|
||||
Neither the webhook receiver nor the WebChat adapter set `client_max_size` on the aiohttp `Application`. Default is 2MB but there's no explicit limit, and no per-request timeout.
|
||||
|
||||
### 7. WebSocket Has No Authentication
|
||||
|
||||
`adapters/webchat_adapter.py` — Anyone who can reach the WebSocket endpoint at `/ws` can interact with the AI. No token, no session cookie, no origin check. If the host is changed from `127.0.0.1` to `0.0.0.0`, this becomes remotely exploitable.
|
||||
|
||||
### 8. No Rate Limiting Anywhere
|
||||
|
||||
No rate limiting on:
|
||||
|
||||
- Webhook endpoints
|
||||
- WebSocket messages
|
||||
- Adapter message handlers
|
||||
- Subagent spawning (only a concurrent limit of 3, but no cooldown)
|
||||
- Scheduler job creation
|
||||
|
||||
### 9. Cron Expression Not Validated Before APScheduler
|
||||
|
||||
`scheduler/scheduler.py` → `_register_cron_job` only checks `len(parts) != 5`. Malformed values within fields (e.g., `999 999 999 999 999`) are passed directly to `CronTrigger`, which could cause unexpected behavior or exceptions.
|
||||
|
||||
### 10. Webhook Token in Query Parameter
|
||||
|
||||
`webhooks/receiver.py`:
|
||||
|
||||
```python
|
||||
if request.query.get("token") == self._config.token:
|
||||
return True
|
||||
```
|
||||
|
||||
Query parameters are logged in web server access logs, browser history, and proxy logs. This leaks the auth token.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM
|
||||
|
||||
### 11. SQLite Databases Created with Default Permissions
|
||||
|
||||
`sessions.db`, `scheduler.db`, and `memory.db` are all created under `~/.aetheel/` with default umask permissions. On multi-user systems, these could be world-readable.
|
||||
|
||||
### 12. Webhook Token Stored in `config.json`
|
||||
|
||||
The `webhooks.token` field in `config.py` is read from and written to `config.json`, which is a plaintext file. Consider using the `env.vars` block with a `${VAR}` reference instead of storing the token directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": { "vars": { "WEBHOOK_TOKEN": "your-secret" } },
|
||||
"webhooks": { "token": "${WEBHOOK_TOKEN}" }
|
||||
}
|
||||
```
|
||||
|
||||
### 13. No HTTPS on Any HTTP Endpoint
|
||||
|
||||
Both WebChat (port 8080) and webhooks (port 8090) run plain HTTP. Even on localhost, this is vulnerable to local network sniffing.
|
||||
|
||||
### 14. Full Environment Passed to Subprocesses
|
||||
|
||||
`_build_cli_env()` in both runtimes copies `os.environ` entirely to the subprocess, which may include sensitive variables beyond what the CLI needs.
|
||||
|
||||
### 15. Session Logs Contain Full Conversations in Plaintext
|
||||
|
||||
`memory/manager.py` → `log_session()` writes unencrypted markdown files to `~/.aetheel/workspace/daily/`. No access control, no encryption, no retention policy.
|
||||
|
||||
### 16. XSS Partially Mitigated in `chat.html` but Fragile
|
||||
|
||||
The `renderMarkdown()` function escapes `<`, `>`, `&` first, then applies regex-based markdown rendering. User messages use `textContent` (safe). AI messages use `innerHTML` with the escaped+rendered output. The escaping happens before markdown processing, which is the right order, but the regex-based approach is fragile — edge cases in the markdown regexes could potentially bypass the escaping.
|
||||
|
||||
### 17. No CORS Headers on WebChat
|
||||
|
||||
The aiohttp app doesn't configure CORS. If exposed beyond localhost, cross-origin requests could interact with the WebSocket.
|
||||
|
||||
---
|
||||
|
||||
## LOW
|
||||
|
||||
### 18. Loose Dependency Version Constraints
|
||||
|
||||
`pyproject.toml`:
|
||||
|
||||
- `python-telegram-bot>=21.0` — no upper bound
|
||||
- `discord.py>=2.4.0` — no upper bound
|
||||
- `fastembed>=0.7.4` — no upper bound
|
||||
|
||||
These could pull in breaking or vulnerable versions on fresh installs.
|
||||
|
||||
### 19. No Security Scanning in CI/Test Pipeline
|
||||
|
||||
No `bandit`, `safety`, `pip-audit`, or similar tools in the test suite or project config.
|
||||
|
||||
### 20. `config edit` Uses `$EDITOR` Without Sanitization
|
||||
|
||||
`cli.py`:
|
||||
|
||||
```python
|
||||
editor = os.environ.get("EDITOR", "nano")
|
||||
subprocess.run([editor, CONFIG_PATH], check=True)
|
||||
```
|
||||
|
||||
If `$EDITOR` contains spaces or special characters, this could behave unexpectedly (though `subprocess.run` with a list is safe from shell injection).
|
||||
|
||||
### 21. No Data Retention/Cleanup for Session Logs
|
||||
|
||||
Session logs accumulate indefinitely in `daily/`. No automatic pruning.
|
||||
|
||||
### 22. `SubagentBus` Has No Authentication
|
||||
|
||||
The pub/sub bus allows any code in the process to publish/subscribe to any channel. No isolation between subagents.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Priority Fixes
|
||||
|
||||
The most impactful changes to make first:
|
||||
|
||||
1. **Add path containment check in `read_file()`** — one-line fix, prevents file system escape
|
||||
2. **Make webhook auth mandatory** when `webhooks.enabled = true` — refuse to start without a token
|
||||
3. **Add input schema validation** on webhook POST bodies
|
||||
4. **Validate cron expressions** more strictly before passing to APScheduler
|
||||
5. **Add rate limiting** to webhook and WebSocket endpoints (e.g., aiohttp middleware)
|
||||
6. **Use `${VAR}` references for `webhooks.token`** in config.json instead of storing the raw value
|
||||
7. **Add WebSocket origin checking or token auth** to WebChat
|
||||
8. **Set explicit `client_max_size`** on aiohttp apps
|
||||
9. **Pin dependency upper bounds** in `pyproject.toml`
|
||||
10. **Add `bandit`** to the test pipeline
|
||||
836
docs/project/setup.md
Normal file
836
docs/project/setup.md
Normal file
@@ -0,0 +1,836 @@
|
||||
# Aetheel Server Setup Guide
|
||||
|
||||
Step-by-step guide to deploy Aetheel on a Linux or macOS machine.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Recommended)
|
||||
|
||||
The interactive installer handles everything automatically:
|
||||
|
||||
```bash
|
||||
# If repo is public:
|
||||
curl -fsSL http://10.0.0.59:3051/tanmay/Aetheel/raw/branch/main/install.sh | bash
|
||||
|
||||
# If repo is private (clone first):
|
||||
git clone http://10.0.0.59:3051/tanmay/Aetheel.git && cd Aetheel && bash install.sh
|
||||
```
|
||||
|
||||
Or if you already have the repo cloned:
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
- Check prerequisites (Python 3.12+, uv)
|
||||
- Install dependencies
|
||||
- Detect or install AI runtimes (OpenCode / Claude Code)
|
||||
- Walk you through token configuration
|
||||
- Install the `aetheel` command
|
||||
- Optionally set up a background service (launchd/systemd)
|
||||
|
||||
After install, use the `aetheel` command:
|
||||
|
||||
```bash
|
||||
aetheel start # Start the bot
|
||||
aetheel stop # Stop the background service
|
||||
aetheel restart # Restart the service
|
||||
aetheel status # Check status
|
||||
aetheel logs # Tail live logs
|
||||
aetheel setup # Re-run setup wizard
|
||||
aetheel update # Pull latest + update deps
|
||||
aetheel doctor # Run diagnostics
|
||||
aetheel config # Edit config.json
|
||||
```
|
||||
|
||||
For flags and options: `aetheel help`
|
||||
|
||||
---
|
||||
|
||||
## Manual Setup
|
||||
|
||||
If you prefer to set things up manually, follow the steps below.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#1-prerequisites)
|
||||
2. [Install System Dependencies](#2-install-system-dependencies)
|
||||
3. [Install uv (Python Package Manager)](#3-install-uv)
|
||||
4. [Install an AI Runtime](#4-install-an-ai-runtime)
|
||||
5. [Clone the Repository](#5-clone-the-repository)
|
||||
6. [Install Python Dependencies](#6-install-python-dependencies)
|
||||
7. [Configure Secrets (config.json env.vars)](#7-configure-secrets)
|
||||
8. [Configure Settings (config.json)](#8-configure-settings)
|
||||
9. [Set Up Messaging Channels](#9-set-up-messaging-channels)
|
||||
10. [Run the Test Suite](#10-run-the-test-suite)
|
||||
11. [Start Aetheel](#11-start-aetheel)
|
||||
12. [Run as a systemd Service](#12-run-as-a-systemd-service)
|
||||
13. [Verify Everything Works](#13-verify-everything-works)
|
||||
14. [Optional: Enable WebChat](#14-optional-enable-webchat)
|
||||
15. [Optional: Enable Webhooks](#15-optional-enable-webhooks)
|
||||
16. [Optional: Configure Hooks](#16-optional-configure-hooks)
|
||||
17. [Optional: Configure Heartbeat](#17-optional-configure-heartbeat)
|
||||
18. [Optional: Add MCP Servers](#18-optional-add-mcp-servers)
|
||||
19. [Optional: Create Skills](#19-optional-create-skills)
|
||||
20. [Updating](#20-updating)
|
||||
21. [Troubleshooting](#21-troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
| Requirement | Minimum | Recommended |
|
||||
|---|---|---|
|
||||
| OS | Ubuntu 20.04 / Debian 11 | Ubuntu 22.04+ / Debian 12+ |
|
||||
| Python | 3.12 | 3.13 |
|
||||
| RAM | 512 MB | 1 GB+ (fastembed uses ~300 MB for embeddings) |
|
||||
| Disk | 500 MB | 1 GB+ |
|
||||
| Network | Outbound HTTPS | Outbound HTTPS |
|
||||
|
||||
You also need at least one of:
|
||||
- Slack workspace with admin access (for Slack adapter)
|
||||
- Telegram bot token (for Telegram adapter)
|
||||
- Discord bot token (for Discord adapter)
|
||||
- Nothing (for WebChat-only mode)
|
||||
|
||||
And at least one AI runtime:
|
||||
- [OpenCode](https://opencode.ai) CLI
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI
|
||||
- An API key for the LLM provider (Anthropic, OpenAI, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 2. Install System Dependencies
|
||||
|
||||
```bash
|
||||
# Update packages
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install essentials
|
||||
sudo apt install -y git curl build-essential
|
||||
|
||||
# Verify
|
||||
git --version
|
||||
curl --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Install uv
|
||||
|
||||
[uv](https://docs.astral.sh/uv/) is the recommended Python package manager. It handles Python versions, virtual environments, and dependencies.
|
||||
|
||||
```bash
|
||||
# Install uv
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Add to PATH (restart shell or source profile)
|
||||
source ~/.bashrc # or ~/.zshrc
|
||||
|
||||
# Verify
|
||||
uv --version
|
||||
```
|
||||
|
||||
uv will automatically install the correct Python version (3.13) when you run `uv sync`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Install an AI Runtime
|
||||
|
||||
You need at least one AI runtime. Pick one (or both):
|
||||
|
||||
### Option A: OpenCode (recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Verify
|
||||
opencode --version
|
||||
|
||||
# Configure a provider
|
||||
opencode auth login
|
||||
```
|
||||
|
||||
### Option B: Claude Code
|
||||
|
||||
```bash
|
||||
# Requires Node.js
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# Install Claude Code
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Verify
|
||||
claude --version
|
||||
```
|
||||
|
||||
### Set your API key
|
||||
|
||||
Whichever runtime you use, you need an API key for the LLM provider:
|
||||
|
||||
```bash
|
||||
# For Anthropic (used by both OpenCode and Claude Code)
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
|
||||
# For OpenAI
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
Add these to your `.env` file later (step 7).
|
||||
|
||||
---
|
||||
|
||||
## 5. Clone the Repository
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
cd ~
|
||||
git clone http://10.0.0.59:3051/tanmay/Aetheel.git
|
||||
cd Aetheel
|
||||
|
||||
# Verify
|
||||
ls pyproject.toml # Should exist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Install Python Dependencies
|
||||
|
||||
```bash
|
||||
# Install project + test dependencies
|
||||
uv sync --extra test
|
||||
|
||||
# Verify Python version
|
||||
uv run python --version # Should be 3.13.x
|
||||
|
||||
# Verify key packages
|
||||
uv run python -c "import click; import aiohttp; import apscheduler; print('All packages OK')"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Configure Secrets
|
||||
|
||||
Secrets (tokens, API keys) go in the `env.vars` block inside `~/.aetheel/config.json`. Generate the default config first, then edit it:
|
||||
|
||||
```bash
|
||||
# Create the config directory and default config
|
||||
mkdir -p ~/.aetheel/workspace
|
||||
uv run python -c "from config import save_default_config; save_default_config()"
|
||||
|
||||
# Edit config
|
||||
nano ~/.aetheel/config.json
|
||||
```
|
||||
|
||||
Fill in the `env.vars` block with your tokens:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
|
||||
"SLACK_APP_TOKEN": "xapp-your-app-token",
|
||||
"TELEGRAM_BOT_TOKEN": "your-telegram-token",
|
||||
"DISCORD_BOT_TOKEN": "your-discord-token",
|
||||
"ANTHROPIC_API_KEY": "sk-ant-your-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The default config already has `${VAR}` references in the adapter sections (e.g. `"bot_token": "${SLACK_BOT_TOKEN}"`), so tokens defined in `env.vars` are automatically resolved.
|
||||
|
||||
Alternatively, you can set tokens as process environment variables — they override everything:
|
||||
|
||||
```bash
|
||||
export SLACK_BOT_TOKEN="xoxb-your-bot-token"
|
||||
export ANTHROPIC_API_KEY="sk-ant-your-key"
|
||||
```
|
||||
|
||||
See [docs/slack-setup.md](slack-setup.md) and [docs/discord-setup.md](discord-setup.md) for how to get these tokens.
|
||||
|
||||
---
|
||||
|
||||
## 8. Configure Settings
|
||||
|
||||
Non-secret settings also live in `~/.aetheel/config.json` (same file as secrets). If you already created it in step 7, just edit the relevant sections:
|
||||
|
||||
```bash
|
||||
nano ~/.aetheel/config.json
|
||||
```
|
||||
|
||||
Key settings to review:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": {
|
||||
"mode": "cli",
|
||||
"model": null
|
||||
},
|
||||
"claude": {
|
||||
"no_tools": false,
|
||||
"allowed_tools": ["Bash", "Read", "Write", "..."]
|
||||
},
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"default_channel": "slack",
|
||||
"default_channel_id": ""
|
||||
},
|
||||
"webchat": {
|
||||
"enabled": false,
|
||||
"port": 8080
|
||||
},
|
||||
"webhooks": {
|
||||
"enabled": false,
|
||||
"port": 8090,
|
||||
"token": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Set Up Messaging Channels
|
||||
|
||||
### Slack (see [docs/slack-setup.md](slack-setup.md))
|
||||
|
||||
1. Create a Slack app at https://api.slack.com/apps
|
||||
2. Add bot scopes: `app_mentions:read`, `chat:write`, `im:history`, `im:read`, `im:write`, `channels:history`, `users:read`
|
||||
3. Enable Socket Mode, generate an app-level token (`xapp-...`)
|
||||
4. Install to workspace, copy bot token (`xoxb-...`)
|
||||
5. Invite bot to channels: `/invite @Aetheel`
|
||||
|
||||
### Discord (see [docs/discord-setup.md](discord-setup.md))
|
||||
|
||||
1. Create app at https://discord.com/developers/applications
|
||||
2. Create bot, copy token
|
||||
3. Enable Message Content Intent
|
||||
4. Invite with `bot` scope + Send Messages + Read Message History
|
||||
|
||||
### Telegram
|
||||
|
||||
1. Message @BotFather on Telegram
|
||||
2. `/newbot` → follow prompts → copy token
|
||||
3. Set `TELEGRAM_BOT_TOKEN` in `config.json` → `env.vars`
|
||||
|
||||
---
|
||||
|
||||
## 10. Run the Test Suite
|
||||
|
||||
Before starting, verify everything works:
|
||||
|
||||
```bash
|
||||
cd ~/Aetheel
|
||||
|
||||
# Run the full test + smoke check script
|
||||
uv run python test_all.py
|
||||
```
|
||||
|
||||
You should see all checks pass:
|
||||
|
||||
```
|
||||
━━━ RESULTS ━━━
|
||||
Total: 45 checks
|
||||
Passed: 45
|
||||
Failed: 0
|
||||
Skipped: 0
|
||||
All checks passed! 🎉
|
||||
```
|
||||
|
||||
If anything fails, fix it before proceeding. Common issues:
|
||||
- Missing packages → `uv sync --extra test`
|
||||
- Wrong directory → `cd ~/Aetheel`
|
||||
|
||||
You can also run just the pytest suite:
|
||||
|
||||
```bash
|
||||
uv run python -m pytest tests/ -v --ignore=tests/test_scheduler.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Start Aetheel
|
||||
|
||||
### Basic start (Slack only)
|
||||
|
||||
```bash
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
### With additional adapters
|
||||
|
||||
```bash
|
||||
# Slack + Discord
|
||||
uv run python main.py --discord
|
||||
|
||||
# Slack + Telegram
|
||||
uv run python main.py --telegram
|
||||
|
||||
# Slack + Discord + WebChat
|
||||
uv run python main.py --discord --webchat
|
||||
|
||||
# All adapters
|
||||
uv run python main.py --discord --telegram --webchat
|
||||
```
|
||||
|
||||
### With Claude Code runtime
|
||||
|
||||
```bash
|
||||
uv run python main.py --claude
|
||||
```
|
||||
|
||||
### Test mode (no AI, echo handler)
|
||||
|
||||
```bash
|
||||
uv run python main.py --test
|
||||
```
|
||||
|
||||
### Using the CLI
|
||||
|
||||
```bash
|
||||
# Same as above but via the click CLI
|
||||
uv run python cli.py start --discord --webchat
|
||||
|
||||
# One-shot chat (no adapters needed)
|
||||
uv run python cli.py chat "What is Python?"
|
||||
|
||||
# Diagnostics
|
||||
uv run python cli.py doctor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Run as a systemd Service
|
||||
|
||||
To keep Aetheel running after you disconnect from SSH:
|
||||
|
||||
### Create the service file
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/aetheel.service
|
||||
```
|
||||
|
||||
Paste (adjust paths to match your setup):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Aetheel AI Assistant
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your-username
|
||||
WorkingDirectory=/home/your-username/Aetheel
|
||||
ExecStart=/home/your-username/.local/bin/uv run python main.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
Environment=PATH=/home/your-username/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
|
||||
# Secrets are loaded from ~/.aetheel/config.json env.vars block.
|
||||
# If you need process-level env overrides, add them here:
|
||||
# Environment=ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# Optional: add more adapters
|
||||
# ExecStart=/home/your-username/.local/bin/uv run python main.py --discord --webchat
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Enable and start
|
||||
|
||||
```bash
|
||||
# Reload systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Enable on boot
|
||||
sudo systemctl enable aetheel
|
||||
|
||||
# Start now
|
||||
sudo systemctl start aetheel
|
||||
|
||||
# Check status
|
||||
sudo systemctl status aetheel
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u aetheel -f
|
||||
```
|
||||
|
||||
### Manage the service
|
||||
|
||||
```bash
|
||||
sudo systemctl stop aetheel # Stop
|
||||
sudo systemctl restart aetheel # Restart
|
||||
sudo systemctl disable aetheel # Disable on boot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Verify Everything Works
|
||||
|
||||
### Check the startup log
|
||||
|
||||
```bash
|
||||
sudo journalctl -u aetheel --no-pager | tail -20
|
||||
```
|
||||
|
||||
You should see:
|
||||
|
||||
```
|
||||
============================================================
|
||||
Aetheel Starting
|
||||
============================================================
|
||||
Config: /home/user/.aetheel/config.json
|
||||
Runtime: opencode/cli, model=default
|
||||
Channels: slack
|
||||
Skills: 0
|
||||
Scheduler: ✅
|
||||
Heartbeat: ✅ 3 tasks
|
||||
Subagents: ✅
|
||||
Hooks: ✅ 0 hooks
|
||||
Webhooks: ❌
|
||||
============================================================
|
||||
```
|
||||
|
||||
### Test from Slack/Discord/Telegram
|
||||
|
||||
1. Send `status` → should show runtime info
|
||||
2. Send `help` → should show command list
|
||||
3. Send any question → should get an AI response
|
||||
4. Send `/reload` → should confirm reload
|
||||
|
||||
### Test from CLI
|
||||
|
||||
```bash
|
||||
uv run python cli.py doctor
|
||||
uv run python cli.py status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Optional: Enable WebChat
|
||||
|
||||
Browser-based chat at `http://your-server:8080`.
|
||||
|
||||
### Via config
|
||||
|
||||
Edit `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"webchat": {
|
||||
"enabled": true,
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `host` to `0.0.0.0` to accept connections from other machines (not just localhost).
|
||||
|
||||
### Via CLI flag
|
||||
|
||||
```bash
|
||||
uv run python main.py --webchat
|
||||
```
|
||||
|
||||
### Firewall
|
||||
|
||||
If using `ufw`:
|
||||
|
||||
```bash
|
||||
sudo ufw allow 8080/tcp
|
||||
```
|
||||
|
||||
Then open `http://your-server-ip:8080` in a browser.
|
||||
|
||||
---
|
||||
|
||||
## 15. Optional: Enable Webhooks
|
||||
|
||||
HTTP endpoints for external systems to trigger the agent.
|
||||
|
||||
### Configure
|
||||
|
||||
Edit `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"webhooks": {
|
||||
"enabled": true,
|
||||
"port": 8090,
|
||||
"host": "0.0.0.0",
|
||||
"token": "your-secret-webhook-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Generate a random token:
|
||||
|
||||
```bash
|
||||
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
```
|
||||
|
||||
### Firewall
|
||||
|
||||
```bash
|
||||
sudo ufw allow 8090/tcp
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# Health check (no auth)
|
||||
curl http://localhost:8090/hooks/health
|
||||
|
||||
# Wake the agent
|
||||
curl -X POST http://localhost:8090/hooks/wake \
|
||||
-H "Authorization: Bearer your-secret-webhook-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "What time is it?"}'
|
||||
|
||||
# Send to a specific channel
|
||||
curl -X POST http://localhost:8090/hooks/agent \
|
||||
-H "Authorization: Bearer your-secret-webhook-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "New alert from monitoring",
|
||||
"channel": "slack",
|
||||
"channel_id": "C123456"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Optional: Configure Hooks
|
||||
|
||||
Lifecycle hooks run custom code on gateway events.
|
||||
|
||||
### Create a hook
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.aetheel/workspace/hooks/startup-logger
|
||||
```
|
||||
|
||||
Create `~/.aetheel/workspace/hooks/startup-logger/HOOK.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: startup-logger
|
||||
description: Log when the gateway starts
|
||||
events: [gateway:startup]
|
||||
enabled: true
|
||||
---
|
||||
# Startup Logger
|
||||
Logs a message when Aetheel starts.
|
||||
```
|
||||
|
||||
Create `~/.aetheel/workspace/hooks/startup-logger/handler.py`:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("aetheel.hooks.startup-logger")
|
||||
|
||||
def handle(event):
|
||||
logger.info("Gateway started — startup hook fired!")
|
||||
```
|
||||
|
||||
Hooks are discovered automatically on startup.
|
||||
|
||||
---
|
||||
|
||||
## 17. Optional: Configure Heartbeat
|
||||
|
||||
The heartbeat system runs periodic tasks automatically.
|
||||
|
||||
### Edit HEARTBEAT.md
|
||||
|
||||
```bash
|
||||
nano ~/.aetheel/workspace/HEARTBEAT.md
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```markdown
|
||||
# Heartbeat Tasks
|
||||
|
||||
## Every 30 minutes
|
||||
- Check if any scheduled reminders need attention
|
||||
|
||||
## Every morning (9:00 AM)
|
||||
- Summarize yesterday's conversations
|
||||
- Check for any pending follow-ups
|
||||
|
||||
## Every evening (6:00 PM)
|
||||
- Update MEMORY.md with today's key learnings
|
||||
```
|
||||
|
||||
### Configure where heartbeat responses go
|
||||
|
||||
Edit `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"default_channel": "slack",
|
||||
"default_channel_id": "C123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `default_channel_id` to the Slack channel ID where heartbeat responses should be sent.
|
||||
|
||||
---
|
||||
|
||||
## 18. Optional: Add MCP Servers
|
||||
|
||||
External tool servers that extend the agent's capabilities.
|
||||
|
||||
Edit `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"brave-search": {
|
||||
"command": "uvx",
|
||||
"args": ["brave-search-mcp@latest"],
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "your-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Aetheel writes the appropriate config file (`.mcp.json` or `opencode.json`) to the workspace before launching the runtime.
|
||||
|
||||
---
|
||||
|
||||
## 19. Optional: Create Skills
|
||||
|
||||
Skills teach the agent how to handle specific types of requests.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.aetheel/workspace/skills/weather
|
||||
```
|
||||
|
||||
Create `~/.aetheel/workspace/skills/weather/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: weather
|
||||
description: Check weather for any city
|
||||
triggers: [weather, forecast, temperature, rain]
|
||||
---
|
||||
|
||||
# Weather Skill
|
||||
|
||||
When the user asks about weather:
|
||||
1. Use web search to find current conditions
|
||||
2. Include temperature, conditions, and forecast
|
||||
3. Format the response with emoji
|
||||
```
|
||||
|
||||
Skills are loaded at startup and can be reloaded with `/reload`.
|
||||
|
||||
---
|
||||
|
||||
## 20. Updating
|
||||
|
||||
```bash
|
||||
cd ~/Aetheel
|
||||
|
||||
# Pull latest
|
||||
git pull
|
||||
|
||||
# Update dependencies
|
||||
uv sync --extra test
|
||||
|
||||
# Run tests
|
||||
uv run python test_all.py
|
||||
|
||||
# Restart service
|
||||
sudo systemctl restart aetheel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. Troubleshooting
|
||||
|
||||
### "No channel adapters initialized!"
|
||||
|
||||
No messaging tokens are set. Check your `~/.aetheel/config.json` has tokens in the `env.vars` block:
|
||||
- `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`
|
||||
- `TELEGRAM_BOT_TOKEN` (with `--telegram` flag)
|
||||
- `DISCORD_BOT_TOKEN` (with `--discord` flag)
|
||||
- `--webchat` flag (no tokens needed)
|
||||
|
||||
### "AI runtime not initialized"
|
||||
|
||||
The AI CLI (opencode or claude) isn't installed or isn't in PATH.
|
||||
|
||||
```bash
|
||||
# Check
|
||||
which opencode
|
||||
which claude
|
||||
|
||||
# Install OpenCode
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### "Request timed out"
|
||||
|
||||
The AI took too long. Increase timeout in config:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": { "timeout_seconds": 300 },
|
||||
"claude": { "timeout_seconds": 300 }
|
||||
}
|
||||
```
|
||||
|
||||
### Memory system init failed
|
||||
|
||||
Usually means fastembed can't download the embedding model on first run. Check internet access and disk space.
|
||||
|
||||
```bash
|
||||
# Test manually
|
||||
uv run python -c "from fastembed import TextEmbedding; e = TextEmbedding(); print('OK')"
|
||||
```
|
||||
|
||||
### Port already in use (WebChat/Webhooks)
|
||||
|
||||
Another process is using the port. Change it in config or find the process:
|
||||
|
||||
```bash
|
||||
sudo lsof -i :8080 # Find what's using port 8080
|
||||
```
|
||||
|
||||
### systemd service won't start
|
||||
|
||||
Check logs:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u aetheel -n 50 --no-pager
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- Wrong `WorkingDirectory` path
|
||||
- Wrong `User`
|
||||
- uv not in PATH (check `Environment=PATH=...`)
|
||||
|
||||
### Run diagnostics
|
||||
|
||||
```bash
|
||||
uv run python cli.py doctor
|
||||
```
|
||||
|
||||
This checks config validity, workspace, runtime CLIs, tokens, and memory DB.
|
||||
365
docs/project/slack-setup.md
Normal file
365
docs/project/slack-setup.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Slack Bot Setup Guide
|
||||
|
||||
> Complete guide to creating a Slack bot and connecting it to Aetheel.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Create a Slack App](#step-1-create-a-slack-app)
|
||||
3. [Configure Bot Permissions](#step-2-configure-bot-permissions)
|
||||
4. [Enable Socket Mode](#step-3-enable-socket-mode)
|
||||
5. [Enable Event Subscriptions](#step-4-enable-event-subscriptions)
|
||||
6. [Install the App to Your Workspace](#step-5-install-the-app-to-your-workspace)
|
||||
7. [Get Your Tokens](#step-6-get-your-tokens)
|
||||
8. [Configure Aetheel](#step-7-configure-aetheel)
|
||||
9. [Run and Test](#step-8-run-and-test)
|
||||
10. [Troubleshooting](#troubleshooting)
|
||||
11. [Architecture Reference](#architecture-reference)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aetheel connects to Slack using **Socket Mode**, which means:
|
||||
- ✅ **No public URL needed** — works behind firewalls and NAT
|
||||
- ✅ **No webhook setup** — Slack pushes events via WebSocket
|
||||
- ✅ **Real-time** — instant message delivery
|
||||
- ✅ **Secure** — encrypted WebSocket connection
|
||||
|
||||
This is the same approach used by [OpenClaw](https://github.com/openclaw/openclaw) (see `src/slack/monitor/provider.ts`), where they use `@slack/bolt` with `socketMode: true`.
|
||||
|
||||
### What You'll Need
|
||||
|
||||
| Item | Description |
|
||||
|------|-------------|
|
||||
| **Slack Workspace** | A Slack workspace where you have admin permissions |
|
||||
| **Bot Token** | `xoxb-...` — for API calls (sending messages, reading info) |
|
||||
| **App Token** | `xapp-...` — for Socket Mode connection |
|
||||
| **Python 3.10+** | Runtime for the Aetheel service |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create a Slack App
|
||||
|
||||
1. Go to [https://api.slack.com/apps](https://api.slack.com/apps)
|
||||
2. Click **"Create New App"**
|
||||
3. Choose **"From scratch"**
|
||||
4. Fill in:
|
||||
- **App Name:** `Aetheel` (or any name you prefer)
|
||||
- **Workspace:** Select your workspace
|
||||
5. Click **"Create App"**
|
||||
|
||||
You'll be taken to your app's **Basic Information** page.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Configure Bot Permissions
|
||||
|
||||
Navigate to **OAuth & Permissions** in the left sidebar.
|
||||
|
||||
Scroll down to **Scopes** → **Bot Token Scopes** and add the following:
|
||||
|
||||
### Required Scopes
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `app_mentions:read` | Receive @mentions in channels |
|
||||
| `channels:history` | Read messages in public channels |
|
||||
| `channels:read` | View basic channel info |
|
||||
| `chat:write` | Send messages |
|
||||
| `groups:history` | Read messages in private channels |
|
||||
| `groups:read` | View private channel info |
|
||||
| `im:history` | Read direct messages |
|
||||
| `im:read` | View DM info |
|
||||
| `im:write` | Open DM conversations |
|
||||
| `mpim:history` | Read group DMs |
|
||||
| `mpim:read` | View group DM info |
|
||||
| `users:read` | Look up user info (for display names) |
|
||||
|
||||
### Optional Scopes (for future features)
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `files:read` | Read files shared in messages |
|
||||
| `files:write` | Upload files |
|
||||
| `reactions:read` | Read emoji reactions |
|
||||
| `reactions:write` | Add emoji reactions |
|
||||
|
||||
> **Tip:** You can always add more scopes later, but you'll need to reinstall the app.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Enable Socket Mode
|
||||
|
||||
1. Navigate to **Socket Mode** in the left sidebar
|
||||
2. Toggle **"Enable Socket Mode"** to **ON**
|
||||
3. You'll be prompted to create an **App-Level Token**:
|
||||
- **Token Name:** `aetheel-socket` (or any name)
|
||||
- **Scopes:** Add `connections:write`
|
||||
4. Click **"Generate"**
|
||||
5. **⚠️ Copy the `xapp-...` token now!** You won't be able to see it again.
|
||||
- Save it somewhere safe — you'll need it in Step 6.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Enable Event Subscriptions
|
||||
|
||||
1. Navigate to **Event Subscriptions** in the left sidebar
|
||||
2. Toggle **"Enable Events"** to **ON**
|
||||
3. Under **Subscribe to bot events**, add:
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `message.channels` | Messages in public channels the bot is in |
|
||||
| `message.groups` | Messages in private channels the bot is in |
|
||||
| `message.im` | Direct messages to the bot |
|
||||
| `message.mpim` | Group DMs that include the bot |
|
||||
| `app_mention` | When someone @mentions the bot |
|
||||
|
||||
4. Click **"Save Changes"**
|
||||
|
||||
> **Note:** With Socket Mode enabled, you do NOT need a Request URL.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Install the App to Your Workspace
|
||||
|
||||
1. Navigate to **Install App** in the left sidebar
|
||||
2. Click **"Install to Workspace"**
|
||||
3. Review the permissions and click **"Allow"**
|
||||
4. You'll see the **Bot User OAuth Token** (`xoxb-...`) — copy it!
|
||||
|
||||
> After installation, invite the bot to any channels where you want it to respond:
|
||||
> - In Slack, go to the channel
|
||||
> - Type `/invite @Aetheel` (or your bot's name)
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Get Your Tokens
|
||||
|
||||
After completing the steps above, you should have two tokens:
|
||||
|
||||
| Token | Format | Where to Find |
|
||||
|-------|--------|---------------|
|
||||
| **Bot Token** | `xoxb-1234-5678-abc...` | **OAuth & Permissions** → Bot User OAuth Token |
|
||||
| **App Token** | `xapp-1-A0123-456...` | **Basic Information** → App-Level Tokens (or from Step 3) |
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Configure Aetheel
|
||||
|
||||
### Option A: Using config.json (recommended)
|
||||
|
||||
Edit `~/.aetheel/config.json` and add your tokens to the `env.vars` block:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"vars": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-actual-bot-token",
|
||||
"SLACK_APP_TOKEN": "xapp-your-actual-app-token"
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"bot_token": "${SLACK_BOT_TOKEN}",
|
||||
"app_token": "${SLACK_APP_TOKEN}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Export environment variables
|
||||
|
||||
```bash
|
||||
export SLACK_BOT_TOKEN="xoxb-your-actual-bot-token"
|
||||
export SLACK_APP_TOKEN="xapp-your-actual-app-token"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Run and Test
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Run the bot
|
||||
|
||||
```bash
|
||||
# Start with smart handler (default)
|
||||
python main.py
|
||||
|
||||
# Start in test/echo mode
|
||||
python main.py --test
|
||||
|
||||
# Start with debug logging
|
||||
python main.py --log DEBUG
|
||||
```
|
||||
|
||||
### Test sending and receiving
|
||||
|
||||
```bash
|
||||
# Run the test suite — sends test messages to a channel
|
||||
python test_slack.py --channel C0123456789
|
||||
|
||||
# Or send a DM test
|
||||
python test_slack.py --dm U0123456789
|
||||
|
||||
# Send-only (no listening)
|
||||
python test_slack.py --channel C0123456789 --send-only
|
||||
```
|
||||
|
||||
### Verify it's working
|
||||
|
||||
1. **In Slack**, go to a channel where the bot is invited
|
||||
2. Type `@Aetheel help` — you should see the help response
|
||||
3. Type `@Aetheel status` — you should see the bot's status
|
||||
4. Send a DM to the bot — it should echo back with details
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### ❌ "Slack bot token is required"
|
||||
|
||||
**Problem:** `SLACK_BOT_TOKEN` is not set or empty.
|
||||
|
||||
**Fix:**
|
||||
1. Check your `config.json` has the token in `env.vars`
|
||||
2. Make sure there are no extra spaces or quotes around the token
|
||||
3. Verify the token starts with `xoxb-`
|
||||
|
||||
### ❌ "Slack app-level token is required for Socket Mode"
|
||||
|
||||
**Problem:** `SLACK_APP_TOKEN` is not set.
|
||||
|
||||
**Fix:**
|
||||
1. Go to your Slack app → **Basic Information** → **App-Level Tokens**
|
||||
2. If no token exists, generate one with `connections:write` scope
|
||||
3. Add it to `config.json` → `env.vars`
|
||||
|
||||
### ❌ "not_authed" or "invalid_auth"
|
||||
|
||||
**Problem:** The bot token is invalid or revoked.
|
||||
|
||||
**Fix:**
|
||||
1. Go to **OAuth & Permissions** → check the Bot User OAuth Token
|
||||
2. If it says "Not installed", reinstall the app
|
||||
3. If you recently changed scopes, you need to reinstall
|
||||
|
||||
### ❌ Bot doesn't respond in channels
|
||||
|
||||
**Problem:** The bot is not invited to the channel, or you're not @mentioning it.
|
||||
|
||||
**Fix:**
|
||||
1. In the Slack channel, type `/invite @Aetheel`
|
||||
2. Make sure you @mention the bot: `@Aetheel hello`
|
||||
3. For DMs, just message the bot directly — no @mention needed
|
||||
|
||||
### ❌ "channel_not_found" when sending
|
||||
|
||||
**Problem:** Using a channel name instead of ID, or bot isn't in the channel.
|
||||
|
||||
**Fix:**
|
||||
1. Use channel **ID** not name. Find it in Slack:
|
||||
- Right-click the channel name → "View channel details"
|
||||
- The ID is at the bottom (starts with `C`)
|
||||
2. Invite the bot to the channel first
|
||||
|
||||
### ❌ Socket Mode connection drops
|
||||
|
||||
**Problem:** The WebSocket connection is unstable.
|
||||
|
||||
**Fix:**
|
||||
1. Check your internet connection
|
||||
2. The SDK automatically reconnects — this is usually transient
|
||||
3. If persistent, check Slack's [status page](https://status.slack.com/)
|
||||
|
||||
### ❌ "missing_scope"
|
||||
|
||||
**Problem:** The bot token doesn't have the required OAuth scopes.
|
||||
|
||||
**Fix:**
|
||||
1. Go to **OAuth & Permissions** → **Bot Token Scopes**
|
||||
2. Add the missing scope mentioned in the error
|
||||
3. **Reinstall the app** (scope changes require reinstallation)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Your Slack │
|
||||
│ Workspace │
|
||||
│ │
|
||||
│ #general │
|
||||
│ #random │
|
||||
│ DMs │
|
||||
└──────┬───────────────┘
|
||||
│ WebSocket (Socket Mode)
|
||||
│
|
||||
┌──────▼───────────────┐
|
||||
│ Aetheel Slack │
|
||||
│ Adapter │
|
||||
│ │
|
||||
│ • Token resolution │
|
||||
│ • Event handling │
|
||||
│ • Thread isolation │
|
||||
│ • Message chunking │
|
||||
│ • User/channel │
|
||||
│ name resolution │
|
||||
└──────┬───────────────┘
|
||||
│ Callback
|
||||
│
|
||||
┌──────▼───────────────┐
|
||||
│ Message Handler │
|
||||
│ │
|
||||
│ • Echo (test) │
|
||||
│ • Smart (commands) │
|
||||
│ • AI (future) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `adapters/slack_adapter.py` | Core Slack adapter (Socket Mode, send/receive) |
|
||||
| `main.py` | Entry point with echo and smart handlers |
|
||||
| `test_slack.py` | Integration test suite |
|
||||
| `~/.aetheel/config.json` | Your Slack tokens (in `env.vars` block) |
|
||||
| `requirements.txt` | Python dependencies |
|
||||
|
||||
### Comparison with OpenClaw
|
||||
|
||||
| Feature | OpenClaw (TypeScript) | Aetheel (Python) |
|
||||
|---------|----------------------|------------------|
|
||||
| **Library** | `@slack/bolt` | `slack_bolt` (official Python SDK) |
|
||||
| **Mode** | Socket Mode (`socketMode: true`) | Socket Mode (`SocketModeHandler`) |
|
||||
| **Auth** | `auth.test()` for identity | `auth_test()` for identity |
|
||||
| **Sending** | `chat.postMessage` with chunking | `chat_postMessage` with chunking |
|
||||
| **Threading** | `thread_ts` for conversation isolation | `thread_ts` for conversation isolation |
|
||||
| **DM Handling** | `conversations.open` for user DMs | `conversations_open` for user DMs |
|
||||
| **Text Limit** | 4000 chars (chunked) | 4000 chars (chunked) |
|
||||
| **Config** | JSON5 config file | `config.json` with `env.vars` + `${VAR}` |
|
||||
| **Accounts** | Multi-account support | Single account (MVP) |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once the Slack adapter is working, you can:
|
||||
|
||||
1. **Connect AI** — Replace the echo handler with an AI-powered handler (Claude API)
|
||||
2. **Add Memory** — Integrate the memory system for conversation context
|
||||
3. **Add Heartbeat** — Set up proactive notifications via Slack
|
||||
4. **Add Skills** — Load skills from the `.claude/skills/` directory
|
||||
|
||||
See the main [OpenClaw Analysis](../openclaw-analysis.md) for the full architecture plan.
|
||||
560
docs/project/spec-phase3-features.md
Normal file
560
docs/project/spec-phase3-features.md
Normal file
@@ -0,0 +1,560 @@
|
||||
# Aetheel Phase 3 Feature Spec
|
||||
|
||||
> Specification for implementing heartbeat, CLI, webchat, self-modification, agent-to-agent communication, and tool/MCP/web search integration.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Runtime Capabilities Audit](#1-runtime-capabilities-audit)
|
||||
2. [Tool System, MCP & Web Search](#2-tool-system-mcp--web-search)
|
||||
3. [Heartbeat / Proactive System](#3-heartbeat--proactive-system)
|
||||
4. [CLI Interface](#4-cli-interface)
|
||||
5. [WebChat Interface](#5-webchat-interface)
|
||||
6. [Self-Modification](#6-self-modification)
|
||||
7. [Agent-to-Agent Communication](#7-agent-to-agent-communication)
|
||||
8. [Implementation Order](#8-implementation-order)
|
||||
|
||||
---
|
||||
|
||||
## 1. Runtime Capabilities Audit
|
||||
|
||||
Before building anything, here's what OpenCode and Claude Code already provide natively that Aetheel can leverage without custom implementation.
|
||||
|
||||
### OpenCode Built-in Tools
|
||||
|
||||
OpenCode ships with these tools enabled by default (no config needed):
|
||||
|
||||
| Tool | Description | Aetheel Status |
|
||||
|------|-------------|----------------|
|
||||
| `bash` | Execute shell commands | ✅ Available via runtime |
|
||||
| `read` | Read file contents | ✅ Available via runtime |
|
||||
| `write` | Create/overwrite files | ✅ Available via runtime |
|
||||
| `edit` | Modify files via string replacement | ✅ Available via runtime |
|
||||
| `grep` | Regex search across files | ✅ Available via runtime |
|
||||
| `glob` | Find files by pattern | ✅ Available via runtime |
|
||||
| `list` | List directory contents | ✅ Available via runtime |
|
||||
| `websearch` | Web search via Exa AI (no API key needed) | ✅ Available — no setup required |
|
||||
| `webfetch` | Fetch and read web pages | ✅ Available via runtime |
|
||||
| `skill` | Load SKILL.md files | ✅ Available via runtime |
|
||||
| `todowrite` / `todoread` | Task tracking | ✅ Available via runtime |
|
||||
| `lsp` | Code intelligence (experimental) | ✅ Available via runtime |
|
||||
| `patch` | Apply diffs | ✅ Available via runtime |
|
||||
| Custom tools | User-defined JS/TS tools | 🟡 Available but not wired |
|
||||
| MCP servers | External tool servers | 🟡 Available but not configured |
|
||||
|
||||
### Claude Code Built-in Tools
|
||||
|
||||
Claude Code provides these tools natively:
|
||||
|
||||
| Tool | Description | Aetheel Status |
|
||||
|------|-------------|----------------|
|
||||
| `Bash` | Shell execution | ✅ Available via runtime |
|
||||
| `Read`, `Write`, `Edit` | File operations | ✅ Available via runtime |
|
||||
| `Glob`, `Grep` | File search | ✅ Available via runtime |
|
||||
| `WebSearch`, `WebFetch` | Web access | ✅ Available via runtime |
|
||||
| `Task`, `TaskOutput`, `TaskStop` | Subagent spawning | ✅ Available via runtime |
|
||||
| `TeamCreate`, `TeamDelete`, `SendMessage` | Agent teams | ✅ Available via runtime |
|
||||
| `Skill` | Load skills | ✅ Available via runtime |
|
||||
| MCP servers | Via `.mcp.json` config | 🟡 Available but not configured |
|
||||
|
||||
### Key Insight
|
||||
|
||||
Both runtimes already have web search, file ops, bash, and subagent tools built in. Aetheel doesn't need to implement these — it just needs to stop blocking them. Currently Aetheel's system prompt doesn't tell the agent about these capabilities, and Claude Code's `--allowedTools ""` (no_tools=true) actively disables them.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tool System, MCP & Web Search
|
||||
|
||||
### What Needs to Change
|
||||
|
||||
**Problem**: Aetheel currently runs both runtimes in "pure chat" mode — tools are disabled or not mentioned in the system prompt.
|
||||
|
||||
**Solution**: Enable tool access and configure MCP servers.
|
||||
|
||||
### 2.1 Enable Runtime Tools
|
||||
|
||||
For OpenCode (CLI mode), tools are enabled by default. No change needed.
|
||||
|
||||
For OpenCode (SDK mode), tools are enabled by default on the server. No change needed.
|
||||
|
||||
For Claude Code, change `no_tools` default from `true` to `false` in config, and set a sensible `allowedTools` list:
|
||||
|
||||
```python
|
||||
# config.py — ClaudeConfig
|
||||
@dataclass
|
||||
class ClaudeConfig:
|
||||
no_tools: bool = False # Changed from True
|
||||
allowed_tools: list[str] = field(default_factory=lambda: [
|
||||
"Bash", "Read", "Write", "Edit", "Glob", "Grep",
|
||||
"WebSearch", "WebFetch",
|
||||
"Task", "Skill",
|
||||
])
|
||||
```
|
||||
|
||||
### 2.2 Update System Prompt
|
||||
|
||||
Add tool awareness to `build_aetheel_system_prompt()`:
|
||||
|
||||
```
|
||||
# Your Tools
|
||||
- You have access to shell commands, file operations, and web search
|
||||
- Use web search to look up current information when needed
|
||||
- You can read and write files in the workspace (~/.aetheel/workspace/)
|
||||
- You can execute shell commands for system tasks
|
||||
```
|
||||
|
||||
### 2.3 MCP Server Configuration
|
||||
|
||||
Create an `opencode.json` in the workspace for OpenCode MCP config, or a `.mcp.json` for Claude Code. This lets users add external tool servers.
|
||||
|
||||
Add to `~/.aetheel/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"example": {
|
||||
"command": "uvx",
|
||||
"args": ["some-mcp-server"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Aetheel writes this to the appropriate config file (`opencode.json` or `.mcp.json`) in the workspace directory before launching the runtime.
|
||||
|
||||
### 2.4 Implementation Tasks
|
||||
|
||||
1. Change `ClaudeConfig.no_tools` default to `False`
|
||||
2. Add `allowed_tools` to `ClaudeConfig` with sensible defaults
|
||||
3. Update `_build_cli_args()` in `claude_runtime.py` to pass `--allowedTools` from config
|
||||
4. Update `build_aetheel_system_prompt()` to mention available tools
|
||||
5. Add `mcp` section to `config.py` and `config.json`
|
||||
6. Write MCP config files to workspace before runtime launch
|
||||
7. Add `TOOLS.md` to workspace identity files (like PicoClaw/OpenClaw)
|
||||
|
||||
### Inspiration
|
||||
|
||||
- **NanoClaw**: Passes `allowedTools` list to `query()` call, runs custom MCP server for IPC
|
||||
- **PicoClaw**: Built-in tool registry in `pkg/tools/`, `TOOLS.md` describes tools to agent
|
||||
- **OpenClaw**: Tool registry with streaming, `TOOLS.md` in workspace
|
||||
|
||||
---
|
||||
|
||||
## 3. Heartbeat / Proactive System
|
||||
|
||||
### How Inspiration Repos Do It
|
||||
|
||||
**PicoClaw**: Reads `HEARTBEAT.md` every 30 minutes. Quick tasks run directly, long tasks spawn subagents. The heartbeat file contains prompts like "Check if any cron jobs need attention" or "Review MEMORY.md for stale entries."
|
||||
|
||||
**Nanobot**: Has a `heartbeat/` module that triggers periodic agent runs.
|
||||
|
||||
**NanoClaw**: Uses scheduled tasks via MCP tools — the agent itself schedules recurring tasks using `schedule_task` with cron expressions. No separate heartbeat file.
|
||||
|
||||
**OpenClaw**: Cron system + wakeups. Heartbeat runs are suppressed from WebChat broadcast when `showOk: false`.
|
||||
|
||||
### Design for Aetheel
|
||||
|
||||
Combine PicoClaw's `HEARTBEAT.md` approach (simple, file-based) with Aetheel's existing scheduler:
|
||||
|
||||
### 3.1 HEARTBEAT.md
|
||||
|
||||
Create `~/.aetheel/workspace/HEARTBEAT.md` as a user-editable file:
|
||||
|
||||
```markdown
|
||||
# Heartbeat Tasks
|
||||
|
||||
Tasks that run periodically. Each section is a task prompt.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
### 3.2 Heartbeat Runner
|
||||
|
||||
New module: `heartbeat/heartbeat.py`
|
||||
|
||||
```python
|
||||
class HeartbeatRunner:
|
||||
def __init__(self, runtime, send_fn, workspace_dir, scheduler):
|
||||
...
|
||||
|
||||
def start(self):
|
||||
"""Register heartbeat tasks with the scheduler."""
|
||||
# Parse HEARTBEAT.md
|
||||
# Register cron jobs for each section
|
||||
# Jobs route through ai_handler with synthetic messages
|
||||
|
||||
def _parse_heartbeat_md(self) -> list[HeartbeatTask]:
|
||||
"""Parse HEARTBEAT.md into structured tasks."""
|
||||
...
|
||||
|
||||
def _on_heartbeat(self, task: HeartbeatTask):
|
||||
"""Called when a heartbeat fires. Routes to AI handler."""
|
||||
...
|
||||
```
|
||||
|
||||
### 3.3 Integration
|
||||
|
||||
- Parse `HEARTBEAT.md` at startup, register tasks with existing `Scheduler`
|
||||
- Heartbeat tasks create synthetic `IncomingMessage` with `source="heartbeat"`
|
||||
- Results can be sent to a configured channel or logged silently
|
||||
- Add `heartbeat` section to config:
|
||||
|
||||
```json
|
||||
{
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"default_channel": "slack",
|
||||
"default_channel_id": "C123456",
|
||||
"silent": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Implementation Tasks
|
||||
|
||||
1. Create `heartbeat/` module with `HeartbeatRunner`
|
||||
2. Create default `HEARTBEAT.md` in workspace (like SOUL.md bootstrap)
|
||||
3. Parse markdown sections into cron expressions + prompts
|
||||
4. Register with existing `Scheduler` at startup
|
||||
5. Add heartbeat config section
|
||||
6. Wire into `main.py` initialization
|
||||
|
||||
---
|
||||
|
||||
## 4. CLI Interface
|
||||
|
||||
### How Inspiration Repos Do It
|
||||
|
||||
**Nanobot**: `nanobot agent -m "..."`, `nanobot gateway`, `nanobot status`, `nanobot cron add/list/remove`
|
||||
|
||||
**OpenClaw**: `openclaw gateway`, `openclaw agent --message "..."`, `openclaw doctor`, `openclaw pairing approve`
|
||||
|
||||
**PicoClaw**: Binary with subcommands via Go's `cobra` library
|
||||
|
||||
### Design for Aetheel
|
||||
|
||||
Use Python's `click` library for a clean CLI with subcommands.
|
||||
|
||||
### 4.1 CLI Structure
|
||||
|
||||
```
|
||||
aetheel # Start with default adapters (same as current main.py)
|
||||
aetheel start # Start with all configured adapters
|
||||
aetheel start --discord # Start with specific adapters
|
||||
aetheel chat "message" # One-shot chat (no adapter, just AI)
|
||||
aetheel status # Show runtime status
|
||||
aetheel cron list # List scheduled jobs
|
||||
aetheel cron remove <id> # Remove a job
|
||||
aetheel sessions list # List active sessions
|
||||
aetheel sessions clear # Clear stale sessions
|
||||
aetheel config show # Print current config
|
||||
aetheel config edit # Open config in $EDITOR
|
||||
aetheel config init # Reset to defaults
|
||||
aetheel memory search "q" # Search memory
|
||||
aetheel memory sync # Force memory re-index
|
||||
aetheel doctor # Diagnostics (check runtime, tokens, etc.)
|
||||
```
|
||||
|
||||
### 4.2 Implementation
|
||||
|
||||
New file: `cli.py`
|
||||
|
||||
```python
|
||||
import click
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""Aetheel — AI-Powered Personal Assistant"""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@click.option("--discord", is_flag=True)
|
||||
@click.option("--telegram", is_flag=True)
|
||||
@click.option("--claude", is_flag=True)
|
||||
@click.option("--model", default=None)
|
||||
@click.option("--test", is_flag=True)
|
||||
@click.option("--log", default="INFO")
|
||||
def start(discord, telegram, claude, model, test, log):
|
||||
"""Start Aetheel with configured adapters."""
|
||||
# Current main() logic moves here
|
||||
...
|
||||
|
||||
@cli.command()
|
||||
@click.argument("message")
|
||||
def chat(message):
|
||||
"""One-shot chat with the AI (no adapter needed)."""
|
||||
...
|
||||
|
||||
@cli.group()
|
||||
def cron():
|
||||
"""Manage scheduled jobs."""
|
||||
pass
|
||||
|
||||
@cron.command("list")
|
||||
def cron_list():
|
||||
...
|
||||
|
||||
@cron.command("remove")
|
||||
@click.argument("job_id")
|
||||
def cron_remove(job_id):
|
||||
...
|
||||
|
||||
# ... etc
|
||||
```
|
||||
|
||||
### 4.3 Entry Point
|
||||
|
||||
Add to `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project.scripts]
|
||||
aetheel = "cli:cli"
|
||||
```
|
||||
|
||||
### 4.4 Implementation Tasks
|
||||
|
||||
1. Add `click` dependency to `pyproject.toml`
|
||||
2. Create `cli.py` with command groups
|
||||
3. Move `main()` logic into `start` command
|
||||
4. Add `chat`, `status`, `cron`, `sessions`, `config`, `memory`, `doctor` commands
|
||||
5. Add entry point to `pyproject.toml`
|
||||
6. Keep `main.py` as backward-compatible wrapper
|
||||
|
||||
---
|
||||
|
||||
## 5. WebChat Interface
|
||||
|
||||
### How Inspiration Repos Do It
|
||||
|
||||
**OpenClaw**: Full WebSocket-based WebChat as an internal channel. Messages routed through the gateway with `messageChannel: "webchat"`. Has a web UI built with React.
|
||||
|
||||
**Nanobot/NanoClaw/PicoClaw**: No webchat.
|
||||
|
||||
### Design for Aetheel
|
||||
|
||||
A lightweight HTTP server with WebSocket support, served as a new adapter.
|
||||
|
||||
### 5.1 Architecture
|
||||
|
||||
```
|
||||
Browser ←→ WebSocket ←→ WebChatAdapter ←→ ai_handler ←→ Runtime
|
||||
↕
|
||||
Static HTML/JS
|
||||
```
|
||||
|
||||
### 5.2 Implementation
|
||||
|
||||
New adapter: `adapters/webchat_adapter.py`
|
||||
|
||||
Use `aiohttp` for both the static file server and WebSocket handler:
|
||||
|
||||
- `GET /` — Serves the chat UI (single HTML file with embedded JS/CSS)
|
||||
- `WS /ws` — WebSocket for real-time chat
|
||||
- Each WebSocket connection = one conversation (session isolation)
|
||||
- Messages flow through the same `BaseAdapter._dispatch` → `ai_handler` path
|
||||
|
||||
### 5.3 Chat UI
|
||||
|
||||
Single self-contained HTML file at `static/chat.html`:
|
||||
- Minimal chat interface (message list + input box)
|
||||
- WebSocket connection to the local server
|
||||
- Markdown rendering for AI responses
|
||||
- No build step, no npm, no framework — just vanilla HTML/JS/CSS
|
||||
|
||||
### 5.4 Config
|
||||
|
||||
```json
|
||||
{
|
||||
"webchat": {
|
||||
"enabled": false,
|
||||
"port": 8080,
|
||||
"host": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 Implementation Tasks
|
||||
|
||||
1. Add `aiohttp` dependency
|
||||
2. Create `adapters/webchat_adapter.py` extending `BaseAdapter`
|
||||
3. Create `static/chat.html` — self-contained chat UI
|
||||
4. Add webchat config section
|
||||
5. Wire into `main.py` / CLI `start` command with `--webchat` flag
|
||||
6. Add to `_adapters` dict like other adapters
|
||||
|
||||
---
|
||||
|
||||
## 6. Self-Modification
|
||||
|
||||
### Can the Runtime Do This?
|
||||
|
||||
**Yes.** Both OpenCode and Claude Code have `Write`, `Edit`, and `Bash` tools that can modify any file the process has access to. The agent can already:
|
||||
|
||||
- Edit `~/.aetheel/config.json` (via file write tools)
|
||||
- Create new skill files in `~/.aetheel/workspace/skills/<name>/SKILL.md`
|
||||
- Update `SOUL.md`, `USER.md`, `MEMORY.md`
|
||||
- Modify `HEARTBEAT.md` to add/remove periodic tasks
|
||||
|
||||
### What Aetheel Needs to Do
|
||||
|
||||
The agent just needs to be told it can do this. Update the system prompt:
|
||||
|
||||
```
|
||||
# Self-Modification
|
||||
- You can edit your own config at ~/.aetheel/config.json
|
||||
- You can create new skills by writing SKILL.md files to ~/.aetheel/workspace/skills/<name>/SKILL.md
|
||||
- You can update your identity files (SOUL.md, USER.md, MEMORY.md)
|
||||
- You can modify HEARTBEAT.md to change your periodic tasks
|
||||
- After editing config, tell the user to restart for changes to take effect
|
||||
- After adding a skill, it will be available on next restart (or use /reload if implemented)
|
||||
```
|
||||
|
||||
### 6.1 Hot Reload (Optional Enhancement)
|
||||
|
||||
Add a `/reload` command that re-reads config and skills without restart:
|
||||
|
||||
```python
|
||||
if text_lower in ("reload", "/reload"):
|
||||
cfg = load_config()
|
||||
_skills.reload()
|
||||
return "🔄 Config and skills reloaded."
|
||||
```
|
||||
|
||||
### 6.2 Implementation Tasks
|
||||
|
||||
1. Update system prompt to mention self-modification capabilities
|
||||
2. Ensure tools are enabled (see section 2)
|
||||
3. Add `/reload` command to `ai_handler`
|
||||
4. Add workspace path to system prompt so agent knows where files are
|
||||
|
||||
---
|
||||
|
||||
## 7. Agent-to-Agent Communication
|
||||
|
||||
### Can the Runtime Do This?
|
||||
|
||||
**OpenCode**: Has `Task` tool for spawning subagents. Subagents run in child sessions. No direct inter-session messaging.
|
||||
|
||||
**Claude Code**: Has `Task`, `TaskOutput`, `TaskStop` for subagent management, plus `TeamCreate`, `TeamDelete`, `SendMessage` for agent teams. `SendMessage` allows agents to communicate within a team.
|
||||
|
||||
### How Inspiration Repos Do It
|
||||
|
||||
**NanoClaw**: Uses Claude Code's `TeamCreate` + `SendMessage` tools. The `allowedTools` list includes both. Agent teams allow multiple agents to work together with message passing.
|
||||
|
||||
**OpenClaw**: `sessions_send` tool allows one session to send a message to another session. Supports fire-and-forget or wait-for-reply modes. Visibility config controls which sessions can see each other.
|
||||
|
||||
### Design for Aetheel
|
||||
|
||||
Aetheel already has `SubagentManager` for spawning background tasks. What's missing is the ability for subagents to communicate back to the main agent or to each other.
|
||||
|
||||
### 7.1 Approach: Leverage Runtime's Native Tools
|
||||
|
||||
Since both OpenCode and Claude Code have subagent/team tools built in, the simplest approach is to enable them:
|
||||
|
||||
For Claude Code, add to `allowed_tools`:
|
||||
```python
|
||||
["Task", "TaskOutput", "TaskStop", "TeamCreate", "TeamDelete", "SendMessage"]
|
||||
```
|
||||
|
||||
For OpenCode, these are enabled by default.
|
||||
|
||||
### 7.2 Aetheel-Level Inter-Subagent Messaging
|
||||
|
||||
For Aetheel's own `SubagentManager`, add a message bus:
|
||||
|
||||
```python
|
||||
class SubagentBus:
|
||||
"""Simple pub/sub for subagent communication."""
|
||||
|
||||
def __init__(self):
|
||||
self._channels: dict[str, list[Callable]] = {}
|
||||
|
||||
def subscribe(self, channel: str, callback: Callable):
|
||||
self._channels.setdefault(channel, []).append(callback)
|
||||
|
||||
def publish(self, channel: str, message: str, sender: str):
|
||||
for cb in self._channels.get(channel, []):
|
||||
cb(message, sender)
|
||||
```
|
||||
|
||||
Wire into `SubagentManager` so subagents can:
|
||||
- Publish results to a channel
|
||||
- Subscribe to messages from other subagents
|
||||
- Send messages to the originating user via `_send_fn`
|
||||
|
||||
### 7.3 Implementation Tasks
|
||||
|
||||
1. Enable `TeamCreate`, `SendMessage`, `Task` tools in Claude Code config
|
||||
2. Update system prompt to mention subagent capabilities
|
||||
3. Add `SubagentBus` to `agent/subagent.py`
|
||||
4. Wire bus into `SubagentManager.spawn()` so subagents can communicate
|
||||
5. Add `/subagents` command to list active subagents and their status
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Order
|
||||
|
||||
Ordered by dependency and impact:
|
||||
|
||||
### Phase 3A: Enable What Already Exists (1-2 days)
|
||||
1. **Tool enablement** — Change `no_tools` default, update allowed tools, update system prompt
|
||||
2. **Self-modification** — Just system prompt changes + `/reload` command
|
||||
3. **Agent-to-agent** — Enable runtime tools + system prompt
|
||||
|
||||
### Phase 3B: New Modules (3-5 days)
|
||||
4. **CLI interface** — `cli.py` with click, move main() logic
|
||||
5. **Heartbeat system** — `heartbeat/` module, `HEARTBEAT.md`, scheduler integration
|
||||
|
||||
### Phase 3C: WebChat (3-5 days)
|
||||
6. **WebChat adapter** — aiohttp server, WebSocket handler, static HTML UI
|
||||
|
||||
### Dependencies
|
||||
|
||||
```
|
||||
Tool enablement ──→ Self-modification (needs tools enabled)
|
||||
──→ Agent-to-agent (needs tools enabled)
|
||||
──→ Heartbeat (agent needs tools for heartbeat tasks)
|
||||
|
||||
CLI ──→ WebChat (webchat flag in CLI)
|
||||
|
||||
Heartbeat ──→ (standalone, uses existing scheduler)
|
||||
```
|
||||
|
||||
### New Dependencies to Add
|
||||
|
||||
```toml
|
||||
[project.dependencies]
|
||||
# ... existing ...
|
||||
click = ">=8.1.0"
|
||||
aiohttp = ">=3.9.0"
|
||||
```
|
||||
|
||||
### New Files
|
||||
|
||||
```
|
||||
Aetheel/
|
||||
├── cli.py # CLI entry point
|
||||
├── heartbeat/
|
||||
│ ├── __init__.py
|
||||
│ └── heartbeat.py # HeartbeatRunner
|
||||
├── adapters/
|
||||
│ └── webchat_adapter.py # WebChat adapter
|
||||
├── static/
|
||||
│ └── chat.html # WebChat UI
|
||||
└── ~/.aetheel/workspace/
|
||||
├── HEARTBEAT.md # Periodic task prompts
|
||||
└── TOOLS.md # Tool descriptions for agent
|
||||
```
|
||||
Reference in New Issue
Block a user