diff --git a/.gitignore b/.gitignore index 50d87c4..c31d43e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ dist/ wheels/ *.egg-info inspiration/ +archive/ # Virtual environments .venv diff --git a/docs/opencode-integration-summary.md b/docs/opencode-integration-summary.md deleted file mode 100644 index 1308c22..0000000 --- a/docs/opencode-integration-summary.md +++ /dev/null @@ -1,232 +0,0 @@ -# OpenCode Runtime Integration — Summary - -> Integration of OpenCode CLI as the agent runtime for Aetheel. -> Completed: 2026-02-13 - ---- - -## Overview - -OpenCode CLI has been integrated as the AI "brain" for Aetheel, replacing the placeholder `smart_handler` with a full agent runtime. The architecture is directly inspired by OpenClaw's `cli-runner.ts` and `cli-backends.ts`, adapted for OpenCode's API and Python. - ---- - -## Files Created & Modified - -### New Files - -| File | Purpose | -|------|---------| -| `agent/__init__.py` | Package init for the agent module | -| `agent/opencode_runtime.py` | Core runtime — ~750 lines covering both CLI and SDK modes | -| `docs/opencode-setup.md` | Comprehensive setup guide | -| `docs/opencode-integration-summary.md` | This summary document | - -### Modified Files - -| File | Change | -|------|--------| -| `main.py` | Rewired to use `ai_handler` backed by `OpenCodeRuntime` instead of placeholder `smart_handler` | -| `.env.example` | Added all OpenCode config variables | -| `requirements.txt` | Added optional `opencode-ai` SDK dependency note | - ---- - -## Architecture - -``` -Slack Message → ai_handler() → OpenCodeRuntime.chat() → OpenCode → LLM → Response -``` - -### Two Runtime Modes - -1. **CLI Mode** (default) — Spawns `opencode run` as a subprocess per request. - Direct port of OpenClaw's `runCliAgent()` → `runCommandWithTimeout()` pattern - from `cli-runner.ts`. - -2. **SDK Mode** — Connects to `opencode serve` via the official Python SDK - (`opencode-ai`). Uses `client.session.create()` → `client.session.chat()` - for lower latency and better session management. - -### 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) │ - └─────────────┘ -``` - ---- - -## Key Components (OpenClaw → Aetheel Mapping) - -| OpenClaw (`cli-runner.ts`) | Aetheel (`opencode_runtime.py`) | -|---|---| -| `CliBackendConfig` | `OpenCodeConfig` dataclass | -| `runCliAgent()` | `OpenCodeRuntime.chat()` | -| `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` (thread_ts → session_id) | - ---- - -## Key Design Decisions - -### 1. Dual-Mode Runtime (CLI + SDK) -- **CLI mode** is the default because it requires no persistent server — just `opencode` in PATH. -- **SDK mode** is preferred for production because it avoids cold-start latency and provides better session management. -- The runtime gracefully falls back from SDK → CLI if the server is unreachable or the SDK is not installed. - -### 2. Session Isolation per Thread -- Each Slack thread (`thread_ts`) maps to a unique OpenCode session via the `SessionStore`. -- New threads get new sessions; replies within a thread reuse the same session. -- Stale sessions are cleaned up after `session_ttl_hours` (default 24h). - -### 3. System Prompt Injection -- `build_aetheel_system_prompt()` constructs a per-message system prompt with the bot's identity, guidelines, and context (user name, channel, DM vs. mention). -- This mirrors OpenClaw's `buildAgentSystemPrompt()` from `cli-runner/helpers.ts`. - -### 4. Output Parsing (from OpenClaw) -- The `_parse_cli_output()` method tries JSON → JSONL → raw text, matching OpenClaw's `parseCliJson()` and `parseCliJsonl()`. -- The `_collect_text()` method recursively traverses JSON objects to find text content, a direct port of OpenClaw's `collectText()`. - -### 5. Built-in Commands Bypass AI -- Commands like `status`, `help`, `time`, and `sessions` are handled directly without calling the AI, for instant responses. - ---- - -## Configuration Reference - -All settings go in `.env`: - -```env -# Runtime mode -OPENCODE_MODE=cli # "cli" or "sdk" - -# Model (optional — uses OpenCode default if not set) -OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514 - -# CLI mode settings -OPENCODE_COMMAND=opencode # path to the opencode binary -OPENCODE_TIMEOUT=120 # seconds before timeout - -# SDK mode settings (only needed when OPENCODE_MODE=sdk) -OPENCODE_SERVER_URL=http://localhost:4096 -OPENCODE_SERVER_PASSWORD= # optional HTTP basic auth -OPENCODE_SERVER_USERNAME=opencode # default username - -# Workspace directory for OpenCode -OPENCODE_WORKSPACE=/path/to/project - -# Output format -OPENCODE_FORMAT=text # "text" or "json" -``` - -CLI flags can override config: - -```bash -python main.py --cli # force CLI mode -python main.py --sdk # force SDK mode -python main.py --model anthropic/claude-sonnet-4-20250514 -python main.py --test # echo-only (no AI) -``` - ---- - -## OpenCode Research Summary - -### OpenCode CLI -- **What:** Go-based AI coding agent for the terminal -- **Install:** `curl -fsSL https://opencode.ai/install | bash` or `npm install -g opencode-ai` -- **Key commands:** - - `opencode` — TUI mode - - `opencode run "prompt"` — non-interactive, returns output - - `opencode serve` — headless HTTP server (OpenAPI 3.1 spec) - - `opencode auth login` — configure LLM providers - - `opencode models` — list available models - - `opencode init` — generate `AGENTS.md` for a project - -### OpenCode Server API (via `opencode serve`) -- Default: `http://localhost:4096` -- Auth: HTTP basic auth via `OPENCODE_SERVER_PASSWORD` -- Key endpoints: - - `GET /session` — list sessions - - `POST /session` — create session - - `POST /session/:id/message` — send message (returns `AssistantMessage`) - - `POST /session/:id/abort` — abort in-progress request - - `GET /event` — SSE event stream - -### OpenCode Python SDK (`opencode-ai`) -- Install: `pip install opencode-ai` -- Key methods: - - `client.session.create()` → `Session` - - `client.session.chat(id, parts=[...])` → `AssistantMessage` - - `client.session.list()` → `Session[]` - - `client.session.abort(id)` → abort - - `client.app.get()` → app info - - `client.app.providers()` → available providers - ---- - -## Quick Start - -1. Install OpenCode: `curl -fsSL https://opencode.ai/install | bash` -2. Configure a provider: `opencode auth login` -3. Test standalone: `opencode run "Hello, what are you?"` -4. Configure `.env` (copy from `.env.example`) -5. Run Aetheel: `python main.py` -6. In Slack: send a message to the bot and get an AI response - ---- - -## Next Steps - -1. **Memory System** — Add conversation persistence (SQLite) so sessions survive restarts -2. **Heartbeat** — Proactive messages via cron/scheduler -3. **Skills** — Loadable skill modules (like OpenClaw's `skills/` directory) -4. **Multi-Channel** — Discord, Telegram adapters -5. **Streaming** — Use SSE events from `opencode serve` for real-time streaming responses diff --git a/openclaw-analysis.md b/openclaw-analysis.md deleted file mode 100644 index 9b1f7c0..0000000 --- a/openclaw-analysis.md +++ /dev/null @@ -1,414 +0,0 @@ -# OpenClaw Analysis & "My Own OpenClaw" Comparison - -> **Date:** 2026-02-13 -> **Source Repo:** `inspiration/openclaw/` (local clone) -> **Diagram Reference:** `inspiration/MyOwnOpenClaw.png` - ---- - -## Table of Contents - -1. [What Is OpenClaw?](#what-is-openclaw) -2. [OpenClaw Architecture Deep Dive](#openclaw-architecture-deep-dive) -3. [MyOwnOpenClaw — The Simplified Blueprint](#myownopenclaw--the-simplified-blueprint) -4. [Side-by-Side Comparison](#side-by-side-comparison) -5. [Key Takeaways for Building Our Own](#key-takeaways-for-building-our-own) -6. [Recommended Build Process for Aetheel](#recommended-build-process-for-aetheel) - ---- - -## 1. What Is OpenClaw? - -OpenClaw is an **open-source personal AI assistant** (MIT licensed, 176k+ stars, 443 contributors, 175k+ lines of TypeScript). It runs locally on your own devices and acts as a **gateway-centric control plane** that connects an AI agent to every messaging channel you already use. - -**Core value proposition:** A single, always-on AI assistant that talks to you through WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, Google Chat, Matrix, WebChat, and more — while keeping everything local and under your control. - ---- - -## 2. OpenClaw Architecture Deep Dive - -### 2.1 The Four Pillars - -Based on both the source code analysis and the `MyOwnOpenClaw.png` diagram, OpenClaw's architecture rests on **four core subsystems**: - ---- - -### Pillar 1: Memory System — "How It Remembers You" - -**Source files:** `src/memory/` (49 files, including `manager.ts` at 2,300+ lines) - -**How it works:** - -| Component | Details | -|-----------|---------| -| **Identity Files** | `SOUL.md` — personality & values; `USER.md` — who you are; `AGENTS.md` — agent behavior rules; `HEARTBEAT.md` — what to proactively check | -| **Long-term Memory** | `MEMORY.md` — persisted decisions, lessons, context | -| **Session Logs** | `daily/` — session logs organized by date | -| **Search** | **Hybrid search** = vector (embeddings) + keyword (BM25) via `sqlite-vec` or `pgvector` | -| **Embedding Providers** | OpenAI, Voyage AI, Gemini, or local via `node-llama-cpp` (ONNX) | -| **Storage** | SQLite database with `sqlite-vec` extension for vector similarity | -| **Sync** | File watcher (chokidar) monitors workspace for changes, auto-re-indexes | - -**Key architectural details from the code:** -- `MemoryIndexManager` class (2,300 LOC) manages the full lifecycle: sync → chunk → embed → store → search -- Hybrid search weighting: configurable vector weight + keyword weight (default 0.7 × vector + 0.3 × keyword as shown in the diagram) -- Supports batch embedding with Voyage, OpenAI, and Gemini batch APIs -- FTS5 full-text search table for keyword matching -- Vector table via `sqlite-vec` for similarity search -- Automatic chunking with configurable token sizes and overlap - ---- - -### Pillar 2: Heartbeat — "How It Acts Proactively" - -**Source files:** `src/cron/` (37 files including service, scheduling, delivery) - -**How it works:** - -| Component | Details | -|-----------|---------| -| **Scheduling** | Cron-based scheduling using the `croner` library | -| **Service Architecture** | `src/cron/service/` — manages job lifecycle, timers, catch-up after restarts | -| **Normalization** | `normalize.ts` (13k) — normalizes cron expressions and job definitions | -| **Delivery** | `delivery.ts` — routes cron job output to the correct channel/session | -| **Run Logging** | `run-log.ts` — persists execution history | -| **Session Reaper** | `session-reaper.ts` — cleans up stale sessions | - -**What happens on each heartbeat:** -1. Cron fires at scheduled intervals -2. Gateway processes the event -3. Checks all integrated services (Gmail, Calendar, Asana, Slack, etc.) -4. AI reasons over the data -5. Sends notification if needed (e.g., "Meeting in 15 min — prep doc is empty") -6. Or returns `HEARTBEAT_OK` (nothing to report) - -**Key detail:** Runs **without user prompting** — this is what makes it feel "proactive." - ---- - -### Pillar 3: Channel Adapters — "How It Works Everywhere" - -**Source files:** `src/channels/`, `src/whatsapp/`, `src/telegram/`, `src/discord/`, `src/slack/`, `src/signal/`, `src/imessage/`, `src/web/`, plus `extensions/` (35 extension directories) - -**Built-in channels:** - -| Channel | Library | Status | -|---------|---------|--------| -| WhatsApp | `@whiskeysockets/baileys` | Core | -| Telegram | `grammy` | Core | -| Slack | `@slack/bolt` | Core | -| Discord | `discord.js` / `@buape/carbon` | Core | -| Signal | `signal-cli` | Core | -| iMessage | BlueBubbles (recommended) or legacy `imsg` | Core | -| WebChat | Built into Gateway WS | Core | - -**Extension channels** (via plugin system): -Microsoft Teams, Matrix, Zalo, Zalo Personal, Google Chat, IRC, Mattermost, Twitch, LINE, Feishu, Nextcloud Talk, Nostr, Tlon, voice calls - -**Architecture:** -- **Gateway-centric** — all channels connect through a single WebSocket control plane (`ws://127.0.0.1:18789`) -- **Channel Dock** (`src/channels/dock.ts`, 17k) — unified registration and lifecycle management -- **Session isolation** — each channel/conversation gets its own session with isolated context -- **Group routing** — configurable mention gating, reply tags, per-channel chunking -- **DM security** — pairing codes for unknown senders, allowlists - ---- - -### Pillar 4: Skills Registry — "How It Extends to Anything" - -**Source files:** `skills/` (52 skill directories) - -**How it works:** - -| Component | Details | -|-----------|---------| -| **Structure** | Each skill is a directory with a `SKILL.md` file | -| **Installation** | Drop a file in `~/.openclaw/workspace/skills//SKILL.md` — instantly available | -| **Registry** | ClawHub (5,700+ skills) — community-built extensions | -| **Types** | Bundled, managed, and workspace skills | -| **Scope** | Local files only — no public registry dependency, no supply chain attack surface | - -**Built-in skill examples:** -`1password`, `apple-notes`, `apple-reminders`, `bear-notes`, `github`, `notion`, `obsidian`, `spotify-player`, `weather`, `canvas`, `coding-agent`, `discord`, `slack`, `openai-image-gen`, `openai-whisper`, `session-logs`, `summarize`, `video-frames`, `voice-call`, etc. - ---- - -### 2.2 Gateway Architecture - -The Gateway is the **central nervous system** of OpenClaw: - -``` -WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Teams / WebChat - │ - ▼ -┌───────────────────────────────┐ -│ Gateway │ -│ (WS control plane) │ -│ ws://127.0.0.1:18789 │ -├───────────────────────────────┤ -│ • Session management │ -│ • Channel routing │ -│ • Cron/heartbeat engine │ -│ • Tool registration │ -│ • Presence & typing │ -│ • Auth & pairing │ -│ • Plugin loading │ -│ • Memory manager │ -│ • Config hot-reload │ -└──────────────┬────────────────┘ - │ - ├─ Pi agent (RPC) — AI reasoning engine - ├─ CLI (openclaw …) - ├─ WebChat UI - ├─ macOS app (menu bar) - ├─ iOS / Android nodes - └─ Browser control (CDP) -``` - -**Key source files:** -- `src/gateway/server.impl.ts` (22k) — main gateway server implementation -- `src/gateway/server-http.ts` (17k) — HTTP server -- `src/gateway/ws-log.ts` (14k) — WebSocket logging -- `src/gateway/session-utils.ts` (22k) — session management -- `src/gateway/config-reload.ts` (11k) — hot config reload - -### 2.3 Configuration - -- **File:** `~/.openclaw/openclaw.json` (JSON5 format) -- **Schema:** Massive TypeBox schema system (`src/config/schema.ts`, `schema.hints.ts` at 46k, `schema.field-metadata.ts` at 45k) -- **Validation:** Zod schemas (`src/config/zod-schema.ts`, 20k) -- **Hot Reload:** Config changes apply without restart - -### 2.4 Tech Stack - -| Category | Technology | -|----------|-----------| -| **Language** | TypeScript (ESM) | -| **Runtime** | Node.js ≥22 (Bun also supported) | -| **Package Manager** | pnpm (bun optional) | -| **Build** | `tsdown` (based on Rolldown) | -| **Testing** | Vitest with V8 coverage | -| **Linting** | Oxlint + Oxfmt | -| **AI Runtime** | Pi agent (`@mariozechner/pi-agent-core`) in RPC mode | -| **Database** | SQLite with `sqlite-vec` for vector search | -| **Embedding** | OpenAI, Voyage, Gemini, or local ONNX | -| **HTTP** | Express 5 | -| **WebSocket** | `ws` library | - ---- - -## 3. MyOwnOpenClaw — The Simplified Blueprint - -The `MyOwnOpenClaw.png` diagram presents a **dramatically simplified** version of the same architecture, built with: - -**Tools:** Claude Code + Claude Agent SDK + SQLite + Markdown + Obsidian - -### The 4 Custom Modules - -#### ① My Memory (SQLite + Markdown + Obsidian) - -| Feature | Implementation | -|---------|---------------| -| `SOUL.md` | Personality & values | -| `USER.md` | Who I am, preferences | -| `MEMORY.md` | Decisions & lessons | -| `daily/` | Session logs | -| **Hybrid Search** | 0.7 × vector + 0.3 × keyword (BM25) | -| **Embeddings** | SQLite (or Postgres) + FastEmbed (384-dim, ONNX) | -| **Key principle** | Fully local — zero API calls | -| **Storage philosophy** | "Markdown IS the database" — Obsidian syncs it everywhere | - -#### ② My Heartbeat (Claude Agent SDK + Python APIs) - -| Feature | Implementation | -|---------|---------------| -| **Frequency** | Every 30 minutes | -| **Action** | Python gathers data from sources: Gmail, Calendar, Asana, Slack | -| **Reasoning** | Claude reasons over the data, decides what's important | -| **Notification** | Sends notification if needed | -| **Example** | "Meeting in 15 min — prep doc is empty" | -| **Fallback** | `HEARTBEAT_OK (nothing to report)` | - -#### ③ My Adapters (Slack + Terminal) - -| Feature | Implementation | -|---------|---------------| -| **Slack** | Socket Mode — no public URL needed; each thread = persistent conversation | -| **Terminal** | Claude Code — direct interaction; full skill + hook access either way | -| **One-shot** | With Claude Code | -| **Future** | Discord, Teams — add when needed | - -#### ④ My Skills (Local `.claude/skills/`) - -| Feature | Implementation | -|---------|---------------| -| **Location** | Local `.claude/skills/` directory | -| **Examples** | `content-engine/`, `direct-integrations/`, `yt-script/`, `pptx-generator/`, `excalidraw-diagram/`, `...15+ more` | -| **Installation** | Drop in `SKILL.md` — instantly available | -| **Security** | Local files only — NO public registry, no supply chain attack surface | - -### The Vision: "Your Ultra-Personalized AI Agent" - -> - 🔵 Remembers your decisions, preferences, and context -> - 🟣 Checks your email and calendar — before you ask -> - 🟢 Talk to it from Slack, terminal, anywhere -> - 🟡 Add any capability with a single file -> -> **"Acts on your behalf. Anticipates what you need. Knows you better every day."** - -### Build Stack - -``` -Claude Code ──→ Claude Agent SDK ──→ SQLite + Markdown ──→ Obsidian -(skills + hooks) (heartbeat + background) (hybrid search, fully local) (your canvas, sync anywhere) -``` - -**~2,000 lines of Python + Markdown** — "You can build it in just a couple days." - ---- - -## 4. Side-by-Side Comparison - -| Feature | OpenClaw (Full) | MyOwnOpenClaw (Custom) | -|---------|----------------|----------------------| -| **Codebase Size** | 175k+ lines TypeScript | ~2,000 lines Python + Markdown | -| **Language** | TypeScript (ESM) | Python | -| **AI Provider** | Any (Anthropic, OpenAI, etc. via Pi) | Claude (via Claude Agent SDK) | -| **Memory System** | SQLite + sqlite-vec, multiple embedding providers | SQLite + FastEmbed (384-dim ONNX) | -| **Hybrid Search** | Vector + BM25 (configurable weights) | 0.7 vector + 0.3 keyword (BM25) | -| **Embeddings** | OpenAI, Voyage, Gemini, local ONNX | FastEmbed local ONNX only — zero API calls | -| **Prompt Files** | SOUL.md, USER.md, AGENTS.md, HEARTBEAT.md, TOOLS.md | SOUL.md, USER.md, MEMORY.md, daily/ | -| **Heartbeat** | Full cron system with croner library | Simple 30-minute Python script | -| **Data Sources** | Configurable via plugins/skills | Gmail, Calendar, Asana, Slack | -| **Channels** | 15+ (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Teams, Matrix, etc.) | Slack (Socket Mode) + Terminal (Claude Code) | -| **Gateway** | Full WS control plane with auth, routing, sessions | None — direct connection | -| **Skills** | 52 bundled + ClawHub registry (5,700+) | Local `.claude/skills/` directory (15+ custom) | -| **Skill Format** | `SKILL.md` file in directory | `SKILL.md` file in directory (same pattern!) | -| **Apps** | macOS, iOS, Android, WebChat | None — Slack + CLI | -| **Voice** | Voice Wake + Talk Mode (ElevenLabs) | Not included | -| **Browser** | Playwright-based CDP control | Not included | -| **Canvas** | Agent-driven visual workspace (A2UI) | Not included | -| **Config** | JSON5 with massive schema validation | Simple Markdown files | -| **Sync** | File watcher (chokidar) | Obsidian sync | -| **Storage Philosophy** | SQLite is the DB | "Markdown IS the database" — Obsidian syncs everywhere | -| **Installation** | `npm install -g openclaw` + wizard | Clone repo + point Claude Code at it | -| **Security** | DM pairing, allowlists, Docker sandboxing | Local only by default | -| **Multi-agent** | Session isolation, agent-to-agent messaging | Not included | -| **Complexity** | Enterprise-grade, production-ready | Personal, lightweight, hackable | - ---- - -## 5. Key Takeaways for Building Our Own - -### What OpenClaw Gets Right (and we should learn from): - -1. **The Memory Architecture** — The combination of identity files (`SOUL.md`, `USER.md`) + long-term memory (`MEMORY.md`) + session logs (`daily/`) is the core pattern. Both systems use this. - -2. **Hybrid Search** — Vector + keyword search is essential for good memory retrieval. The 0.7/0.3 weighting is a good starting point. - -3. **Skill Drop-in Pattern** — Just put a `SKILL.md` file in a directory and it's instantly available. No compilation, no registry. OpenClaw invented this pattern and the custom version copies it directly. - -4. **Proactive Heartbeat** — Running on a schedule, checking your data sources before you ask. This is what makes the agent feel like an assistant rather than a chatbot. - -5. **The Separation of Concerns** — Memory, Heartbeat, Adapters, and Skills are clean, independent modules. Each can be built and tested separately. - -### What MyOwnOpenClaw Simplifies: - -1. **No Gateway** — Direct connections instead of a WS control plane. Much simpler but less flexible. - -2. **Python over TypeScript** — More accessible for quick prototyping and data processing. - -3. **Claude-only** — No model switching, no failover. Simpler but locked to one provider. - -4. **Obsidian as sync** — Uses Obsidian's existing sync infrastructure instead of building custom file watching. - -5. **Two adapters max** — Slack + Terminal vs. 15+ channels. Start small, add as needed. - -### The Process (from the diagram): - -> 1. Clone the OpenClaw repository (MIT licensed, 100% open source) -> 2. Point your coding agent at it — "Explain how the memory system works" -> 3. "Now build that into my own system here (optional: with customization XYZ)" -> 4. Repeat for heartbeat, adapters, skills. That's it. - -**Use OpenClaw as your blueprint, not your dependency.** - ---- - -## 6. Recommended Build Process for Aetheel - -Based on this analysis, here's the recommended order for building a custom AI assistant inspired by OpenClaw: - -### Phase 1: Memory System -- Create `SOUL.md`, `USER.md`, `MEMORY.md` files -- Implement SQLite database with `sqlite-vec` or FastEmbed for vector search -- Build hybrid search (vector + BM25 keyword) -- Set up file watching for automatic re-indexing -- Use Obsidian for cross-device sync - -### Phase 2: Heartbeat -- Build a Python script using Claude Agent SDK -- Connect to Gmail, Calendar, Asana (start with most-used services) -- Set up 30-minute cron schedule -- Implement notification delivery (start with terminal notifications) - -### Phase 3: Adapters -- Start with Terminal (Claude Code) for direct interaction -- Add Slack (Socket Mode) for messaging -- Build conversation threading support - -### Phase 4: Skills -- Create `.claude/skills/` directory structure -- Port most-used skills from OpenClaw as inspiration -- Build custom skills specific to your workflow - ---- - -## Appendix: OpenClaw File Structure Reference - -``` -openclaw/ -├── src/ # Core source code (175k+ LOC) -│ ├── memory/ # Memory system (49 files) -│ │ ├── manager.ts # Main memory manager (2,300 LOC) -│ │ ├── hybrid.ts # Hybrid search (vector + keyword) -│ │ ├── embeddings.ts # Embedding provider abstraction -│ │ ├── qmd-manager.ts # Query+doc management (33k) -│ │ └── ... -│ ├── cron/ # Heartbeat/cron system (37 files) -│ │ ├── service/ # Cron service lifecycle -│ │ ├── schedule.ts # Scheduling logic -│ │ ├── delivery.ts # Output delivery -│ │ └── ... -│ ├── channels/ # Channel adapter framework (28 files) -│ │ ├── dock.ts # Unified channel dock (17k) -│ │ ├── registry.ts # Channel registration -│ │ └── ... -│ ├── gateway/ # Gateway WS control plane (129+ files) -│ │ ├── server.impl.ts # Main server (22k) -│ │ ├── server-http.ts # HTTP layer (17k) -│ │ ├── session-utils.ts # Session management (22k) -│ │ └── ... -│ ├── config/ # Configuration system (130+ files) -│ ├── agents/ # Agent runtime -│ ├── browser/ # Browser control (Playwright) -│ └── ... -├── skills/ # Built-in skills (52 directories) -│ ├── obsidian/ -│ ├── github/ -│ ├── notion/ -│ ├── spotify-player/ -│ └── ... -├── extensions/ # Extension channels (35 directories) -│ ├── msteams/ -│ ├── matrix/ -│ ├── voice-call/ -│ └── ... -├── apps/ # Companion apps -│ ├── macos/ -│ ├── ios/ -│ └── android/ -├── AGENTS.md # Agent behavior guidelines -├── openclaw.json # Configuration -└── package.json # Dependencies & scripts -``` diff --git a/test_memory.py b/test_memory.py deleted file mode 100644 index 61c573e..0000000 --- a/test_memory.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Quick smoke test for the memory system.""" - -import asyncio -import os -import shutil - -from memory import MemoryManager -from memory.types import MemoryConfig -from memory.internal import chunk_markdown, hash_text, list_memory_files - - -def test_internals(): - print("── Internal utilities ──") - - # Hashing - h = hash_text("hello world") - assert len(h) == 64 - print(f"✅ hash_text: {h[:16]}...") - - # Chunking - text = "# Title\n\nLine1\nLine2\nLine3\n\n## Section\n\nMore text here" - chunks = chunk_markdown(text, chunk_tokens=50, chunk_overlap=10) - assert len(chunks) >= 1 - print(f"✅ chunk_markdown: {len(chunks)} chunks") - for c in chunks: - print(f" lines {c.start_line}-{c.end_line}: {c.text[:50]!r}") - - print() - - -async def test_manager(): - print("── MemoryManager ──") - - # Clean slate - test_dir = "/tmp/aetheel_test_workspace" - test_db = "/tmp/aetheel_test_memory.db" - for p in [test_dir, test_db]: - if os.path.exists(p): - if os.path.isdir(p): - shutil.rmtree(p) - else: - os.remove(p) - - config = MemoryConfig( - workspace_dir=test_dir, - db_path=test_db, - ) - - mgr = MemoryManager(config) - print(f"✅ Created: workspace={mgr._workspace_dir}") - - # Identity files - soul = mgr.read_soul() - assert soul and len(soul) > 0 - print(f"✅ SOUL.md: {len(soul)} chars") - - user = mgr.read_user() - assert user and len(user) > 0 - print(f"✅ USER.md: {len(user)} chars") - - memory = mgr.read_long_term_memory() - assert memory and len(memory) > 0 - print(f"✅ MEMORY.md: {len(memory)} chars") - - # Append to memory - mgr.append_to_memory("Test entry: Python 3.14 works great!") - memory2 = mgr.read_long_term_memory() - assert len(memory2) > len(memory) - print(f"✅ Appended to MEMORY.md: {len(memory2)} chars") - - # Log a session - log_path = mgr.log_session( - "User: Hello!\nAssistant: Hi, how can I help?", - channel="terminal", - ) - assert os.path.exists(log_path) - print(f"✅ Session logged: {log_path}") - - # Sync - print("\n⏳ Syncing (loading embedding model on first run)...") - stats = await mgr.sync() - print(f"✅ Sync complete:") - for k, v in stats.items(): - print(f" {k}: {v}") - - # Search - print("\n🔍 Searching for 'personality values'...") - results = await mgr.search("personality values") - print(f"✅ Found {len(results)} results:") - for i, r in enumerate(results[:3]): - print(f" [{i+1}] score={r.score:.3f} path={r.path} lines={r.start_line}-{r.end_line}") - print(f" {r.snippet[:80]}...") - - print("\n🔍 Searching for 'preferences'...") - results2 = await mgr.search("preferences") - print(f"✅ Found {len(results2)} results:") - for i, r in enumerate(results2[:3]): - print(f" [{i+1}] score={r.score:.3f} path={r.path} lines={r.start_line}-{r.end_line}") - print(f" {r.snippet[:80]}...") - - # Status - print("\n📊 Status:") - status = mgr.status() - for k, v in status.items(): - print(f" {k}: {v}") - - mgr.close() - print("\n✅ All memory system tests passed!") - - -if __name__ == "__main__": - test_internals() - asyncio.run(test_manager()) diff --git a/test_slack.py b/test_slack.py deleted file mode 100644 index f31300c..0000000 --- a/test_slack.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3 -""" -Aetheel Slack Adapter — Integration Test -========================================== -Tests the Slack adapter by: - 1. Connecting to Slack via Socket Mode - 2. Sending a test message to a specified channel - 3. Verifying the bot can send and receive - -Usage: - python test_slack.py # Interactive — prompts for channel - python test_slack.py --channel C0123456789 # Send to a specific channel - python test_slack.py --dm U0123456789 # Send a DM to a user - python test_slack.py --send-only # Just send, don't listen - -Requirements: - - SLACK_BOT_TOKEN and SLACK_APP_TOKEN set in .env - - Bot must be invited to the target channel -""" - -import argparse -import logging -import os -import sys -import time -import threading - -from dotenv import load_dotenv - -load_dotenv() - -from adapters.slack_adapter import SlackAdapter, SlackMessage - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", -) -logger = logging.getLogger("aetheel.test") - - -# --------------------------------------------------------------------------- -# Test 1: Send a message -# --------------------------------------------------------------------------- -def test_send_message(adapter: SlackAdapter, target: str) -> bool: - """Test sending a message to a channel or user.""" - print("\n" + "=" * 60) - print(" TEST 1: Send Message") - print("=" * 60) - - try: - result = adapter.send_message( - channel=target, - text=( - "🧪 *Aetheel Slack Test*\n\n" - "If you can see this message, the Slack adapter is working!\n\n" - f"• Bot ID: `{adapter._bot_user_id}`\n" - f"• Bot Name: `@{adapter._bot_user_name}`\n" - f"• Timestamp: `{time.strftime('%Y-%m-%d %H:%M:%S')}`\n" - f"• Mode: Socket Mode\n\n" - "_Reply to this message to test receiving._" - ), - ) - print(f" ✅ Message sent successfully!") - print(f" Channel: {result.channel_id}") - print(f" Message ID: {result.message_id}") - return True - - except Exception as e: - print(f" ❌ Failed to send: {e}") - return False - - -# --------------------------------------------------------------------------- -# Test 2: Send a threaded reply -# --------------------------------------------------------------------------- -def test_threaded_reply(adapter: SlackAdapter, target: str) -> bool: - """Test sending a message and then replying in a thread.""" - print("\n" + "=" * 60) - print(" TEST 2: Threaded Reply") - print("=" * 60) - - try: - # Send parent message - parent = adapter.send_message( - channel=target, - text="🧵 *Thread Test* — This is the parent message.", - ) - print(f" ✅ Parent message sent (ts={parent.message_id})") - - time.sleep(1) - - # Send threaded reply - reply = adapter.send_message( - channel=target, - text="↳ This is a threaded reply! Thread isolation is working.", - thread_ts=parent.message_id, - ) - print(f" ✅ Thread reply sent (ts={reply.message_id})") - return True - - except Exception as e: - print(f" ❌ Failed: {e}") - return False - - -# --------------------------------------------------------------------------- -# Test 3: Long message chunking -# --------------------------------------------------------------------------- -def test_long_message(adapter: SlackAdapter, target: str) -> bool: - """Test that long messages are properly chunked.""" - print("\n" + "=" * 60) - print(" TEST 3: Long Message Chunking") - print("=" * 60) - - try: - # Create a message that exceeds 4000 chars - long_text = "📜 *Long Message Test*\n\n" - for i in range(1, 101): - long_text += f"{i}. This is line number {i} of the long message test. " \ - f"It contains enough text to test the chunking behavior.\n" - - result = adapter.send_message(channel=target, text=long_text) - print(f" ✅ Long message sent (length={len(long_text)}, id={result.message_id})") - return True - - except Exception as e: - print(f" ❌ Failed: {e}") - return False - - -# --------------------------------------------------------------------------- -# Test 4: Receive messages (interactive) -# --------------------------------------------------------------------------- -def test_receive_messages(adapter: SlackAdapter, duration: int = 30) -> bool: - """ - Test receiving messages by listening for a specified duration. - The bot will echo back any messages it receives. - """ - print("\n" + "=" * 60) - print(" TEST 4: Receive Messages (Interactive)") - print("=" * 60) - print(f" Listening for {duration} seconds...") - print(f" Send a message to @{adapter._bot_user_name} to test receiving.") - print(f" Press Ctrl+C to stop early.\n") - - received = [] - - def test_handler(msg: SlackMessage) -> str: - received.append(msg) - print(f" 📨 Received: '{msg.text}' from @{msg.user_name}") - return f"✅ Got it! You said: _{msg.text}_" - - adapter.on_message(test_handler) - - try: - adapter.start_async() - time.sleep(duration) - except KeyboardInterrupt: - print("\n Stopped by user.") - finally: - adapter.stop() - - print(f"\n Messages received: {len(received)}") - if received: - print(" ✅ Receive test PASSED") - return True - else: - print(" ⚠️ No messages received (send a message to the bot to test)") - return True # Not a failure — just no one sent a message - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- -def main(): - parser = argparse.ArgumentParser(description="Test the Aetheel Slack Adapter") - group = parser.add_mutually_exclusive_group() - group.add_argument("--channel", help="Channel ID to send test messages to (C...)") - group.add_argument("--dm", help="User ID to DM for testing (U...)") - parser.add_argument( - "--send-only", - action="store_true", - help="Only run send tests (don't listen for messages)", - ) - parser.add_argument( - "--duration", - type=int, - default=30, - help="How long to listen for messages in seconds (default: 30)", - ) - args = parser.parse_args() - - # Validate tokens - if not os.environ.get("SLACK_BOT_TOKEN") or not os.environ.get("SLACK_APP_TOKEN"): - print("❌ Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN in environment.") - print(" Copy .env.example to .env and fill in your tokens.") - sys.exit(1) - - # Get target - target = args.channel or args.dm - if not target: - print("You need to specify a target for send tests.") - print(" --channel C0123456789 (channel ID)") - print(" --dm U0123456789 (user ID for DM)") - target = input("\nEnter a channel or user ID (or press Enter to skip send tests): ").strip() - - # Create adapter - adapter = SlackAdapter(log_level="INFO") - - # Resolve identity first - adapter._resolve_identity() - - # Run tests - results = {} - - if target: - results["send"] = test_send_message(adapter, target) - results["thread"] = test_threaded_reply(adapter, target) - results["chunking"] = test_long_message(adapter, target) - else: - print("\n⏭️ Skipping send tests (no target specified)") - - if not args.send_only: - results["receive"] = test_receive_messages(adapter, duration=args.duration) - - # Summary - print("\n" + "=" * 60) - print(" TEST RESULTS") - print("=" * 60) - for test_name, passed in results.items(): - icon = "✅" if passed else "❌" - print(f" {icon} {test_name}") - - total = len(results) - passed = sum(1 for v in results.values() if v) - print(f"\n {passed}/{total} tests passed") - print("=" * 60) - - return 0 if all(results.values()) else 1 - - -if __name__ == "__main__": - sys.exit(main())