Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ __pycache__/
data/
tailwindcss/
tests/
tools/wacli/.turbo/
tools/wacli/.next/
tools/wacli/node_modules/
*.md
!skills/*.md
.tmux-session
7 changes: 2 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ ADMIN_API_KEY=
# Web search (Tavily) — get a free API key at https://app.tavily.com
TAVILY_API_KEY=

# Himalaya email passwords (used by cli-configs/himalaya.toml via `printenv`)
# For Gmail: generate at https://myaccount.google.com/apppasswords
# For Fastmail: generate at https://www.fastmail.com/settings/security/devicekeys
HIMALAYA_PERSONAL_PASSWORD=
#HIMALAYA_WORK_PASSWORD=
# Email accounts are configured via the Admin UI (stored in the config DB and
# materialized to a Himalaya config at runtime) — no env vars needed here.

# CalDAV calendar credentials (used by config.yml calendar providers)
# For Google: use your Gmail + App Password (https://myaccount.google.com/apppasswords)
Expand Down
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ build/
config.yml
character.md
personalia.md
cli-configs/*
!cli-configs/*.example

# Data (runtime state, DB, models)
data/
Expand All @@ -40,6 +38,3 @@ tailwindcss
# Tailwind CSS build output
api/static/style.css
node_modules

# WhatsApp CLI auth/session data
tools/wacli/.wacli/
25 changes: 14 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ FROM python:3.14-slim
WORKDIR /app

# System deps (ffmpeg for voice pipeline, curl for health checks, sqlite3 for memory)
# golang + build-essential build wacli (CGO, sqlite_fts5) from the pinned upstream tag.
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg curl ca-certificates jq sqlite3 \
bash tar gzip xz-utils \
golang build-essential pkg-config \
nodejs npm \
&& rm -rf /var/lib/apt/lists/*

# Install Himalaya CLI (pre-built Rust binary for email management)
Expand All @@ -22,15 +22,21 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& apt-get update && apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/*

# Install wacli from pinned upstream tag (github.com/openclaw/wacli).
# Bump WACLI_VERSION to cross WhatsApp protocol breaks (e.g. 405 Client Outdated).
ARG WACLI_VERSION=v0.11.0
RUN CGO_ENABLED=1 CGO_CFLAGS="-Wno-error=missing-braces" \
GOBIN=/usr/local/bin \
go install -tags sqlite_fts5 github.com/openclaw/wacli/cmd/wacli@${WACLI_VERSION} \
&& wacli version

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Install dependencies (cached layer — only re-runs when lockfile changes)
COPY pyproject.toml uv.lock ./
RUN uv sync --no-dev --no-install-project

# Contacts tooling handled by native CLI in /app/tools

# Create non-root user
RUN groupadd --gid 10001 mpa && \
useradd --uid 10001 --gid 10001 --create-home --shell /bin/bash mpa
Expand All @@ -52,17 +58,14 @@ RUN ARCH=$(dpkg --print-architecture) && \
/tmp/tailwindcss --input api/static/input.css --output api/static/style.css --minify && \
rm /tmp/tailwindcss

# CLI config directories
RUN mkdir -p /home/mpa/.config/himalaya /app/data \
# Data directory
RUN mkdir -p /app/data \
&& chown -R mpa:mpa /home/mpa /app

# Build wacli
RUN corepack enable && \
corepack prepare pnpm@9.15.2 --activate && \
cd tools/wacli && \
pnpm -s build

USER mpa

# Identify the linked WhatsApp device as "MPA" (native since wacli 0.2.0).
ENV WACLI_DEVICE_LABEL=MPA

EXPOSE 8000
CMD ["uv", "run", "python", "-m", "core.main"]
27 changes: 16 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ TAILWIND := ./tailwindcss
CSS_IN := api/static/input.css
CSS_OUT := api/static/style.css

# Pinned upstream wacli (github.com/openclaw/wacli). Keep in sync with Dockerfile.
WACLI_VERSION := v0.11.0

# Show available targets
help:
@echo ""
Expand All @@ -23,7 +26,7 @@ help:
@echo " make dev Show instructions for running dev services"
@echo " make dev-agent Run agent with auto-reload"
@echo " make dev-css Run Tailwind CSS watcher"
@echo " make dev-wa Build WhatsApp CLI (wacli)"
@echo " make dev-wa Install WhatsApp CLI (wacli) from upstream"
@echo " make docs-dev Run docs site with hot reload"
@echo ""
@echo " Quality:"
Expand Down Expand Up @@ -94,11 +97,9 @@ dev:
@echo " 2. Tailwind CSS watcher:"
@echo " make dev-css"
@echo ""
@if [ -d tools/wacli ]; then \
echo " 3. WhatsApp (wacli build):"; \
echo " make dev-wa"; \
echo ""; \
fi
@echo " 3. WhatsApp (wacli install):"
@echo " make dev-wa"
@echo ""
@echo " Docs (optional):"
@echo " make docs-dev"
@echo ""
Expand All @@ -114,12 +115,16 @@ dev-agent:
dev-css:
$(TAILWIND) --input $(CSS_IN) --output $(CSS_OUT) --watch

# Dev: WhatsApp (wacli)
# Dev: WhatsApp (wacli) — install the pinned upstream binary into $GOBIN/~/go/bin.
# Needs Go + a C toolchain (CGO, sqlite_fts5). Override WACLI_BIN to point the
# agent elsewhere; otherwise core/wacli.py resolves it from PATH or ~/go/bin.
dev-wa:
@if [ ! -x tools/wacli/dist/wacli ]; then \
echo "Building wacli..."; \
cd tools/wacli && pnpm -s build; \
fi
@command -v wacli >/dev/null 2>&1 || test -x "$(HOME)/go/bin/wacli" || { \
echo "Installing wacli $(WACLI_VERSION) from github.com/openclaw/wacli..."; \
CGO_ENABLED=1 CGO_CFLAGS="-Wno-error=missing-braces" \
go install -tags sqlite_fts5 github.com/openclaw/wacli/cmd/wacli@$(WACLI_VERSION); \
}
@wacli version 2>/dev/null || "$(HOME)/go/bin/wacli" version

# Remove venv and caches
clean:
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ MPA uses a dual-layer config system:
| `character.md` | Agent personality and communication style (editable) |
| `personalia.md` | Agent identity facts — name, owner, context (append-only) |
| `skills/*.md` | Skill documents that teach the agent how to use tools |
| `cli-configs/` | Configuration for Himalaya |

## Project structure

Expand Down Expand Up @@ -163,8 +162,8 @@ Behavior and identity are configured in `character.md.example` and `personalia.m

## WhatsApp

MPA uses wacli to authenticate and sync WhatsApp locally. The admin UI starts auth, displays the QR code, and manages sync.
See `tools/wacli/` for the vendored CLI source and build instructions.
MPA uses [wacli](https://github.com/openclaw/wacli) to authenticate and sync WhatsApp locally. The admin UI starts auth, displays the QR code, and manages sync.
The binary is installed from a pinned upstream tag (`WACLI_VERSION` in the `Dockerfile`; `make dev-wa` for local dev) — not vendored. See `docs/content/docs/channels.mdx` for upgrade/re-auth notes.

## Tech stack

Expand Down
11 changes: 9 additions & 2 deletions api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1750,7 +1750,7 @@ async def test_channel_whatsapp(body: WhatsAppTestIn) -> dict:
if not available:
result["error"] = (
"wacli binary not found. "
"Run 'make dev-wa' or 'cd tools/wacli && pnpm build' to compile it."
"Run 'make dev-wa' to install it, or set WACLI_BIN to its path."
)
return result

Expand All @@ -1761,8 +1761,15 @@ async def whatsapp_auth_status() -> dict:

@app.post("/channels/whatsapp/auth/start", dependencies=[Depends(auth)])
async def whatsapp_auth_start() -> dict:
if not wacli.available():
return {
"ok": False,
"available": False,
"error": "wacli binary not found. Run 'make dev-wa' to install it, "
"or set WACLI_BIN to its path.",
}
await wacli.start_auth()
return {"ok": True}
return {"ok": True, "available": True}

@app.post("/channels/whatsapp/auth/stop", dependencies=[Depends(auth)])
async def whatsapp_auth_stop() -> dict:
Expand Down
4 changes: 4 additions & 0 deletions api/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,10 @@ <h2 class="text-base text-accent">Agent Control</h2>
if (!r.ok) {
throw new Error('Auth start failed');
}
const d = await r.json();
if (d && d.ok === false) {
throw new Error(d.error || 'Auth start failed');
}
await pollWhatsAppAuth();
} catch (e) {
result.textContent = e.message || 'Auth start failed.';
Expand Down
64 changes: 0 additions & 64 deletions cli-configs/himalaya.toml.example

This file was deleted.

18 changes: 9 additions & 9 deletions core/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@ def _find_wacli_bin() -> str:
from_path = shutil.which("wacli")
if from_path:
return from_path
# Docker: /app/tools/wacli/dist/wacli
docker_path = Path("/app/tools/wacli/dist/wacli")
if docker_path.exists():
return str(docker_path)
# Local dev: <project_root>/tools/wacli/dist/wacli
local_path = Path(__file__).resolve().parents[1] / "tools" / "wacli" / "dist" / "wacli"
if local_path.exists():
return str(local_path)
# Local dev: `go install` / `make dev-wa` drops the binary in ~/go/bin.
go_bin = Path.home() / "go" / "bin" / "wacli"
if go_bin.exists():
return str(go_bin)
return "wacli" # fallback — let the shell try PATH


Expand Down Expand Up @@ -95,10 +91,14 @@ async def run_command_trusted(self, command: str, timeout: int = 30) -> dict:
async def _exec(self, command: str, timeout: int) -> dict:
"""Run a shell command and capture output."""
env = None
if "himalaya" in command or self.tool_env:
wants_wacli_label = "wacli" in command and "WACLI_DEVICE_LABEL" not in os.environ
if "himalaya" in command or self.tool_env or wants_wacli_label:
env = os.environ.copy()
if "himalaya" in command:
env.update(himalaya_env())
# wacli: identify the linked device as MPA (matches the Docker ENV).
if wants_wacli_label:
env.setdefault("WACLI_DEVICE_LABEL", "MPA")
# Tool auth (e.g. GH_TOKEN) — only set when a tool is enabled.
env.update(self.tool_env)
proc = await asyncio.create_subprocess_shell(
Expand Down
Loading
Loading