<p align="center">
  <img src="assets/logo_text.png" alt="Denkeeper" width="300">
</p>

<p align="center">
  <a href="https://github.com/Temikus/denkeeper/actions/workflows/ci.yml"><img src="https://github.com/Temikus/denkeeper/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://github.com/Temikus/denkeeper/actions/workflows/security.yml"><img src="https://github.com/Temikus/denkeeper/actions/workflows/security.yml/badge.svg" alt="Security"></a>
  <a href="https://github.com/Temikus/denkeeper/releases/latest"><img src="https://img.shields.io/github/v/release/Temikus/denkeeper?label=release" alt="Latest Release"></a>
  <a href="https://github.com/Temikus/denkeeper/pkgs/container/denkeeper"><img src="https://img.shields.io/github/v/release/Temikus/denkeeper?label=ghcr.io&logo=docker" alt="Docker Image"></a>
  <a href="https://goreportcard.com/report/github.com/Temikus/denkeeper"><img src="https://goreportcard.com/badge/github.com/Temikus/denkeeper" alt="Go Report Card"></a>
  <a href="LICENSE"><img src="https://img.shields.io/github/license/Temikus/denkeeper" alt="License"></a>
  <a href="https://github.com/Temikus/denkeeper/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/UI_coverage-enforced-blue" alt="UI Coverage"></a>
</p>

A security-first personal AI agent that lives in your chat. Built in Go as a single binary, designed to run anywhere from a Raspberry Pi to a cloud VM.

Denkeeper connects to your Telegram or Discord, routes messages through LLM providers via [Anthropic](https://anthropic.com), [OpenAI](https://openai.com), [OpenRouter](https://openrouter.ai), or a local [Ollama](https://ollama.com) instance, and remembers conversations across sessions using a local SQLite database. It enforces per-session cost budgets, user allowlists, and a tiered permission system — so you stay in control of what it can do and how much it can spend.

## Installation

### One-liner (Linux and macOS)

```sh
curl -fsSL https://raw.githubusercontent.com/Temikus/denkeeper/main/install.sh | sh
```

To install to a custom prefix (e.g. without sudo):

```sh
curl -fsSL https://raw.githubusercontent.com/Temikus/denkeeper/main/install.sh | sh -s -- --prefix ~/.local
```

The installer detects OS/arch, downloads the correct release archive, verifies the SHA-256 checksum, and places the binary in `<prefix>/bin`.

### Debian / Ubuntu (.deb)

```sh
VERSION=$(curl -fsSL https://api.github.com/repos/Temikus/denkeeper/releases/latest | grep '"tag_name"' | sed 's/.*"\(v[^"]*\)".*/\1/')
curl -fsSL "https://github.com/Temikus/denkeeper/releases/download/${VERSION}/denkeeper_${VERSION#v}_linux_amd64.deb" -o denkeeper.deb
sudo dpkg -i denkeeper.deb
```

Configure and start the service:

```sh
sudo cp /etc/denkeeper/denkeeper.toml.example /etc/denkeeper/denkeeper.toml
sudoedit /etc/denkeeper/denkeeper.toml
sudo systemctl enable --now denkeeper
journalctl -u denkeeper -f
```

### RHEL / Fedora (.rpm)

```sh
VERSION=$(curl -fsSL https://api.github.com/repos/Temikus/denkeeper/releases/latest | grep '"tag_name"' | sed 's/.*"\(v[^"]*\)".*/\1/')
curl -fsSL "https://github.com/Temikus/denkeeper/releases/download/${VERSION}/denkeeper_${VERSION#v}_linux_amd64.rpm" -o denkeeper.rpm
sudo rpm -i denkeeper.rpm
```

### Docker

```sh
docker pull ghcr.io/temikus/denkeeper:latest
docker run -d --name denkeeper \
  -v ~/.denkeeper:/data \
  ghcr.io/temikus/denkeeper:latest
```

The container reads config from `DENKEEPER_CONFIG` (default `/data/denkeeper.toml`). Override with `-e DENKEEPER_CONFIG=/path/to/config.toml`.

### Helm (Kubernetes)

A Helm chart is available in [`deploy/helm/denkeeper/`](deploy/helm/denkeeper/) with support for Ingress, PVC persistence, secrets management, and security-hardened pod defaults:

```sh
helm install denkeeper deploy/helm/denkeeper/ \
  --set secrets.llmAnthropicApiKey=sk-ant-... \
  --set secrets.telegramToken=123456:ABC...
```

### Homebrew (macOS)

```sh
brew install Temikus/denkeeper/denkeeper
```

### Verify release signatures

All release archives are signed with [cosign](https://github.com/sigstore/cosign) (keyless OIDC — no long-lived keys):

```sh
cosign verify-blob \
  --signature checksums.txt.sig \
  --certificate checksums.txt.pem \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  --certificate-identity-regexp='https://github.com/Temikus/denkeeper/.github/workflows/release.yml.*' \
  checksums.txt
```

Docker images are signed and carry SLSA build provenance attestations:

```sh
cosign verify \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  --certificate-identity-regexp='https://github.com/Temikus/denkeeper/.github/workflows/release.yml.*' \
  ghcr.io/temikus/denkeeper:latest
```

## Features

- **Single binary** — no runtime dependencies, no containers required
- **Multi-agent routing** — run multiple named agents, each with their own persona, skills, LLM model, and permission tier
- **Telegram + Discord** — chat with your agent from your phone or Discord server, including inline Approve/Deny buttons for supervised actions; both adapters can run simultaneously
- **User allowlist** — only approved user IDs can interact (per-adapter)
- **LLM routing** — pluggable provider interface; Anthropic (direct), OpenAI (direct + Azure/vLLM-compatible), OpenRouter (cloud, hundreds of models), and Ollama (local inference) built-in
- **Fallback strategies** — automatic model/provider switching on errors, rate limits, or low funds
- **Cost tracking** — per-session budgets with automatic cutoff
- **Conversation memory** — SQLite-backed, persistent across restarts
- **Scheduler** — cron expressions, named intervals, and `@daily`/`@hourly` shorthand; per-schedule agent targeting and session modes
- **Skills** — flat markdown files with TOML frontmatter; trigger-based filtering (`command:`/`schedule:`) and per-agent skill merging
- **MCP tools** — spawn MCP servers via stdio (subprocess) or SSE/Streamable HTTP (remote), discover tools, and execute tool calls in an agentic loop; auto-restart on crash with configurable backoff; OAuth 2.1 authorization for remote MCP servers
- **MCP security** — SSRF protection (blocks localhost, link-local, and cloud metadata endpoints), HTTP header injection prevention, redirect target validation, env var denylist for secrets, and URL/arg redaction in API responses
- **Plugin system** — subprocess and Docker-sandboxed plugins with capability declarations and Ed25519 signature verification; tools capability wires plugin tools into the agent's LLM loop
- **Runtime tool management** — add and remove MCP tools and plugins at runtime without restarting; changes are persisted to TOML config
- **Agent KV store** — per-agent key-value storage with optional TTL, exposed as MCP tools (`kv_get`/`kv_set`/`kv_delete`/`kv_list`/`kv_set_nx`); useful for locks, counters, caches, and cross-session state
- **Supervisor agents** — a supervised agent can designate another agent as its supervisor via `supervisor = "agent-name"` in TOML; the supervisor sits between auto-approve rules and human approval, returning APPROVE/DENY/ESCALATE for each tool call; supervisor prompt includes skill/schedule context for scheduled invocations; configurable timeout (`supervisor_timeout`, default 30s) and context message count (`supervisor_context_messages`, default 5); LLM failures emit a `supervisor_error` event before falling through to human approval
- **Audit log** — unified audit trail with buffered emitter, SQLite storage, and 11 event categories (`tool_call`, `skill`, `channel`, `approval`, `schedule`, `llm`, `config`, `session`, `mcp`, `safety`, `supervisor`); web UI page with timeline and table views, category/status/agent/time filters
- **Channels** — named routing endpoints (`[[channels]]`) that decouple sessions from adapters; cross-adapter session sharing, ephemeral session mode, `/session` command for runtime switching; auto-synthesized from agent `adapters` bindings when absent (backward compatible)
- **Safety commands** — `/stop` cancels the current in-flight request, `/panic` emergency-stops all in-flight requests and pauses the scheduler, `/resume` clears panic state; available in Telegram, Discord, web UI, and REST API
- **Session history management** — `/clear` removes all messages from a session, `/compact` summarises via LLM and replaces all messages with a single summary; available in Telegram, Discord, web UI, and REST API
- **OpenAPI spec** — generated via `swaggo/swag`, served at `GET /api/v1/openapi.json` (no auth required)
- **Web dashboard** — embedded Svelte UI (served via the API server) with 17 pages: overview, chat, sessions, approvals, schedules, skills, tools, browser, KV store, costs, agents, API keys, providers, server config, settings, audit log, and channels; includes dark mode toggle and warm light theme
- **Voice** — speech-to-text and text-to-speech via OpenAI (Whisper + TTS)
- **Permission tiers** — autonomous, supervised (default), and restricted; configurable per-agent or per-schedule
- **Approval workflows** — supervised-tier actions (profile updates, skill creation, schedule additions, tool installation) require explicit human approval via chat buttons (Telegram/Discord) or REST API
- **Config MCP server** — per-agent in-process MCP tools let the LLM manage skills, schedules, tools, plugins, KV storage, and inspect its own permission tier at runtime
- **External REST API** — HTTP server with scoped API key auth, rate limiting, CORS, and TLS support; chat endpoint with real-time token streaming (SSE + WebSocket), session management, approval CRUD, tool/plugin CRUD, LLM provider management, server reload/restart, and API key management
- **Dashboard authentication** — password login (bcrypt), OAuth2/OIDC SSO (PKCE), session cookies (AES-256-GCM)
- **OpenTelemetry observability** — Prometheus `/metrics` endpoint and optional OTLP trace export
- **CLI plugin signing** — `denkeeper plugin keygen/sign/verify` commands for Ed25519 plugin binary signing and verification
- **CLI password hashing** — `denkeeper passwd` generates a bcrypt hash for dashboard password login
- **Personality** — ships with a [`SOUL.md`](agents/default/SOUL.md) that gives the agent character (editable)

## Architecture

```
Adapter (Telegram/Discord) ─┐
Web Dashboard (WS/SSE) ─────┼→ Dispatcher → Engine (per agent) → LLM Router → Provider (Anthropic/OpenAI/OpenRouter/Ollama)
REST API (/api/v1/chat) ────┘                    ↕                    ↕
                                             MemoryStore          CostTracker
                                             (SQLite)              + Pricing Registry

Scheduler ──────────────────────────────────────┘
```

The Dispatcher routes incoming messages to named agent Engines based on adapter bindings. Each Engine checks permissions, loads conversation history, builds the system prompt (persona + skills), calls the LLM (with tool-call loop if MCP tools are configured), stores the response, and sends it back through the adapter.

## Quick start

### Prerequisites

- Go 1.25+ (managed via [mise](https://mise.jdx.dev) — see `mise.toml`)
- A Telegram bot token (from [@BotFather](https://t.me/BotFather))
- An API key for your chosen LLM provider: [OpenRouter](https://openrouter.ai/keys), [Anthropic](https://console.anthropic.com/settings/keys), or a local [Ollama](https://ollama.com) instance
- Your Telegram user ID (from [@userinfobot](https://t.me/userinfobot))

### Setup

```bash
# Clone
git clone https://github.com/Temikus/denkeeper.git
cd denkeeper

# Copy and edit the config
mkdir -p ~/.denkeeper
cp denkeeper.toml.example ~/.denkeeper/denkeeper.toml
# Fill in your token, API key, and user ID
$EDITOR ~/.denkeeper/denkeeper.toml

# Build and run
just build
./pkg/bin/denkeeper serve
```

Or run directly without building:

```bash
just serve
```

### Configuration

Denkeeper uses a single TOML file (default `~/.denkeeper/denkeeper.toml`). See [`denkeeper.toml.example`](denkeeper.toml.example) for all options. The config path can be set via `--config` flag or `DENKEEPER_CONFIG` env var.

**Health check**: `GET /api/v1/health` returns `{"status":"ok"}` with no authentication required. Use this for Docker `HEALTHCHECK` or Kubernetes liveness/readiness probes (requires `api.enabled = true`).

Key sections:

| Section | Purpose |
|---------|---------|
| `[telegram]` | Bot token and allowed user IDs |
| `[discord]` | Bot token and allowed user snowflake IDs |
| `[llm]` | Default provider name, model, and per-session cost limits (`cost_limit_soft`, `cost_limit_hard`) |
| `[[llm.providers]]` | Named provider instances — multiple instances of the same type allowed (e.g. OpenAI + LM Studio) |
| `[llm.anthropic]` | Anthropic API key — legacy single-slot syntax, auto-converted to `[[llm.providers]]` |
| `[llm.openrouter]` | OpenRouter API key — legacy single-slot syntax |
| `[llm.ollama]` | Ollama base URL — legacy single-slot syntax |
| `[[llm.fallback]]` | Fallback strategies (error/rate_limit/cost_limit triggers) |
| `[session]` | Default permission tier (supervised/autonomous/restricted) |
| `[[agents]]` | Multi-agent definitions (persona, skills, LLM provider/model override, adapter bindings, supervisor, supervisor_timeout, supervisor_context_messages, cost limits) |
| `[[channels]]` | Named routing endpoints — bind adapter chats to agents with session identity; `session_mode` (`shared`/`ephemeral`) |
| `[audit]` | Audit log settings (`enabled`, `retention_days`, `cleanup_interval`, `buffer_size`) |
| `[mcp]` | Global MCP settings — request timeout, auto-restart, max restart attempts, restart cooldown, SSE URL allowlist |
| `[tools.*]` | MCP tool server definitions — stdio (subprocess) or SSE (remote) transport, URL, headers, per-server timeout override |
| `[plugins.*]` | Plugin definitions — subprocess or Docker-sandboxed (capability declarations) |
| `[security]` | Ed25519 plugin signing config (`trusted_keys`, `allow_unsigned`) |
| `[voice]` | STT/TTS configuration (OpenAI) |
| `[api]` | External REST API (listen addr, TLS, CORS, rate limiting, API keys with scopes) |
| `[api.auth]` | Dashboard authentication (bcrypt password, session secret, OIDC SSO) |
| `[otel]` | OpenTelemetry observability (Prometheus metrics, OTLP trace export) |
| `[[schedules]]` | Recurring tasks (cron, interval, or named schedules) |
| `[kv]` | Agent KV store limits (`max_keys_per_agent`, `max_value_bytes`, `cleanup_interval`) |
| `[memory]` | SQLite database path |
| `[log]` | Log level and format |

### Environment Variables

Secrets and select config fields can be set via environment variables, which take precedence over values in `denkeeper.toml`. This enables the standard Kubernetes pattern of using a ConfigMap for config and a Secret for credentials.

| Env Var | Config Field |
|---------|-------------|
| `DENKEEPER_CONFIG` | Config file path (replaces `--config` flag) |
| `DENKEEPER_TELEGRAM_TOKEN` | `telegram.token` |
| `DENKEEPER_DISCORD_TOKEN` | `discord.token` |
| `DENKEEPER_LLM_PROVIDER` | `llm.default_provider` |
| `DENKEEPER_LLM_MODEL` | `llm.default_model` |
| `DENKEEPER_LLM_OPENROUTER_API_KEY` | `llm.openrouter.api_key` |
| `DENKEEPER_LLM_ANTHROPIC_API_KEY` | `llm.anthropic.api_key` |
| `DENKEEPER_LLM_ANTHROPIC_BASE_URL` | `llm.anthropic.base_url` |
| `DENKEEPER_LLM_OLLAMA_BASE_URL` | `llm.ollama.base_url` |
| `DENKEEPER_LLM_OPENAI_API_KEY` | `llm.openai.api_key` |
| `DENKEEPER_LLM_OPENAI_BASE_URL` | `llm.openai.base_url` |
| `DENKEEPER_VOICE_OPENAI_API_KEY` | `voice.openai.api_key` |
| `DENKEEPER_LOG_LEVEL` | `log.level` |
| `DENKEEPER_LOG_FORMAT` | `log.format` |
| `DENKEEPER_MEMORY_DB_PATH` | `memory.db_path` |
| `DENKEEPER_API_ENABLED` | `api.enabled` (accepts `"true"` or `"1"`) |
| `DENKEEPER_API_LISTEN` | `api.listen` |
| `DENKEEPER_SESSION_TIER` | `session.tier` |
| `DENKEEPER_API_AUTH_SESSION_SECRET` | `api.auth.session_secret` (AES-256 hex key) |
| `DENKEEPER_OIDC_CLIENT_ID` | `api.auth.oidc.client_id` |
| `DENKEEPER_OIDC_CLIENT_SECRET` | `api.auth.oidc.client_secret` |
| `DENKEEPER_API_WEBSOCKET_ENABLED` | `api.websocket_enabled` (accepts `"true"` or `"false"`) |
| `DENKEEPER_OTEL_ENABLED` | `otel.enabled` (accepts `"true"` or `"false"`) |
| `DENKEEPER_OTEL_TRACES_ENDPOINT` | `otel.traces_endpoint` (OTLP HTTP endpoint) |

A Helm chart is available in [`deploy/helm/denkeeper/`](deploy/helm/denkeeper/) for Kubernetes deployments.

### Skills

Skills are markdown files that teach the agent how to handle specific tasks. They use TOML frontmatter enclosed in `+++` delimiters:

```markdown
+++
name = "daily-briefing"
description = "Compile and deliver a daily briefing"
version = "1.0.0"
triggers = ["schedule:daily:08:00", "command:briefing"]
+++

# Daily Briefing

When triggered, compile a briefing with:
1. Weather forecast for the user's location
2. Top 3 news headlines
3. Any pending reminders
```

Place skill files in `~/.denkeeper/skills/` (configurable via `[agent] skills_dir`). Subdirectories with a `SKILL.md` file are also supported. Skills with `triggers` are only injected when matched; skills without triggers are always included.

Agent-specific skills in `<persona_dir>/skills/` override global skills of the same name.

A sample `help` skill is included in `agents/default/skills/`.

### Multi-Agent

Define multiple agents, each with their own persona, skills, LLM model, and adapter bindings:

```toml
[[agents]]
name = "default"
persona_dir = "~/.denkeeper/agents/default"
adapters = ["telegram"]              # wildcard: all Telegram messages

[[agents]]
name = "work-assistant"
persona_dir = "~/.denkeeper/agents/work-assistant"
adapters = ["telegram:987654321"]    # specific chat only
llm_model = "openai/gpt-4o"
session_tier = "supervised"
supervisor = "default"               # optional: auto-review tool calls before human approval
```

If no `[[agents]]` section is present, a single `"default"` agent is synthesized from `[agent]`/`[session]`.

### Schedules

Schedules support three expression formats, per-schedule agent targeting, and configurable session modes:

```toml
[[schedules]]
name = "daily-briefing"
type = "agent"
schedule = "0 8 * * *"
skill = "daily-briefing"
agent = "default"                # target agent (default: "default")
session_tier = "supervised"
session_mode = "isolated"        # fresh context each run (default: "shared")
channel = "telegram:YOUR_CHAT_ID"
enabled = true

[[schedules]]
name = "hourly-check"
type = "agent"
schedule = "@every 1h"           # or @daily, @hourly, @weekly
channel = "telegram:YOUR_CHAT_ID"
```

`session_mode = "isolated"` creates a fresh conversation context for each run so scheduled jobs don't mix into your regular chat history.

### REST API

The API server and web dashboard are enabled by default (listening on `:8080`). All endpoints (except `/health`) require a `Bearer` token matching a configured API key.

```toml
[api]
listen = "0.0.0.0:8080"

[[api.keys]]
name = "my-client"
key  = "dk-your-secret-key"
scopes = ["chat", "sessions:read", "costs:read"]
```

**Available scopes**: `chat`, `admin`, `agents:read`, `agents:write`, `sessions:read`, `sessions:write`, `costs:read`, `skills:read`, `skills:write`, `schedules:read`, `schedules:write`, `approvals:read`, `approvals:write`, `tools:read`, `tools:write`, `kv:read`, `kv:write`, `channels:read`, `channels:write`, `audit:read`

**Endpoints:**

| Method | Path | Scope | Description |
|--------|------|-------|-------------|
| `GET` | `/api/v1/health` | — | Health check (no auth) |
| `GET` | `/api/v1/openapi.json` | — | OpenAPI 2.0 spec (no auth) |
| `GET` | `/llms.txt` | — | LLM-readable instance summary: base URL, auth notes, key endpoints, configured agents (no auth) |
| `GET` | `/api/v1/setup` | — | First-run setup status |
| `POST` | `/api/v1/setup` | — | Initialize first-run configuration |
| `POST` | `/api/v1/chat` | `chat` | Send a message; returns `{ session_id, response }`. Add `Accept: text/event-stream` for SSE. |
| `GET` | `/api/v1/ws` | `chat` | WebSocket upgrade for bidirectional streaming (auth via `?token=` or session cookie) |
| `GET` | `/api/v1/models` | `agents:read` | List available LLM models from all providers |
| `GET` | `/api/v1/models/details` | `agents:read` | Model details with pricing info |
| `GET` | `/api/v1/llm/providers` | `admin` | List LLM providers with current config |
| `POST` | `/api/v1/llm/providers` | `admin` | Create a named provider instance |
| `PATCH` | `/api/v1/llm/providers/{name}` | `admin` | Update provider config (API key, base URL) |
| `DELETE` | `/api/v1/llm/providers/{name}` | `admin` | Remove a provider instance |
| `PATCH` | `/api/v1/llm/config` | `admin` | Update global LLM config (default provider, model) |
| `GET` | `/api/v1/server/config` | `admin` | Server config (version, build info, CORS, WebSocket) |
| `PATCH` | `/api/v1/server/config` | `admin` | Update server config (CORS origins, WebSocket settings) |
| `POST` | `/api/v1/server/reload` | `admin` | Reload config from disk |
| `POST` | `/api/v1/server/restart` | `admin` | Restart the server process |
| `GET` | `/api/v1/auth/status` | `admin` | Auth config summary (password, OIDC, sessions) |
| `GET` | `/api/v1/auth/sessions` | `admin` | List active sessions |
| `DELETE` | `/api/v1/auth/sessions/{id}` | `admin` | Revoke a session |
| `POST` | `/api/v1/auth/password` | `admin` | Change password |
| `GET` | `/api/v1/auth/oidc/test` | `admin` | Test OIDC provider reachability |
| `POST` | `/api/v1/auth/preferences` | `admin` | Set preferred login method |
| `GET` | `/api/v1/onboarding` | `admin` | Setup checklist status |
| `POST` | `/api/v1/onboarding/dismiss` | `admin` | Dismiss onboarding card |
| `GET` | `/api/v1/sessions` | `sessions:read` | List all conversations |
| `GET` | `/api/v1/sessions/{id}/messages` | `sessions:read` | Get messages for a session |
| `GET` | `/api/v1/sessions/{id}/stats` | `sessions:read` | Session telemetry summary |
| `GET` | `/api/v1/sessions/{id}/tool-calls` | `sessions:read` | Tool call records for a session |
| `GET` | `/api/v1/sessions/{id}/skills` | `sessions:read` | Skill usage for a session |
| `POST` | `/api/v1/sessions/{id}/clear` | `sessions:write` | Clear all messages in a session (keeps conversation row) |
| `POST` | `/api/v1/sessions/{id}/compact` | `sessions:write` | Compact session into LLM summary |
| `POST` | `/api/v1/sessions/{id}/stop` | `chat` | Cancel in-flight request for a session |
| `DELETE` | `/api/v1/sessions/{id}` | `sessions:read` | Delete a session and its history |
| `POST` | `/api/v1/panic` | `admin` | Emergency stop — cancel all in-flight requests, pause scheduler |
| `POST` | `/api/v1/resume` | `admin` | Clear panic state, resume scheduler |
| `GET` | `/api/v1/panic` | `admin` | Get panic state (`{panicked, panic_time}`) |
| `GET` | `/api/v1/telemetry/summary` | `costs:read` | Aggregate telemetry (`?since=&until=`) |
| `GET` | `/api/v1/agents` | `agents:read` | List agents with metadata |
| `POST` | `/api/v1/agents` | `admin` | Create a new agent at runtime |
| `GET` | `/api/v1/agents/{name}` | `agents:read` | Agent details and skills |
| `PATCH` | `/api/v1/agents/{name}` | `agents:write` | Mutate agent config (tier, model, supervisor, cost limits) |
| `DELETE` | `/api/v1/agents/{name}` | `admin` | Remove an agent (rejects if referenced by channels/schedules) |
| `GET` | `/api/v1/skills` | `skills:read` | List all skills across agents |
| `GET` | `/api/v1/skills/{agent}` | `skills:read` | List skills for a specific agent |
| `POST` | `/api/v1/skills/{agent}` | `skills:write` | Create a skill |
| `PUT` | `/api/v1/skills/{agent}/{name}` | `skills:write` | Update a skill |
| `DELETE` | `/api/v1/skills/{agent}/{name}` | `skills:write` | Delete a skill |
| `GET` | `/api/v1/schedules` | `schedules:read` | List schedules with run times |
| `POST` | `/api/v1/schedules` | `schedules:write` | Create a schedule |
| `PATCH` | `/api/v1/schedules/{name}` | `schedules:write` | Update a schedule |
| `DELETE` | `/api/v1/schedules/{name}` | `schedules:write` | Delete a schedule |
| `GET` | `/api/v1/costs` | `costs:read` | Cost summary |
| `GET` | `/api/v1/approvals` | `approvals:read` | List approval requests (filter by `?status=pending`) |
| `GET` | `/api/v1/approvals/{id}` | `approvals:read` | Get a single approval request |
| `POST` | `/api/v1/approvals/{id}/approve` | `approvals:write` | Approve; `?auto_approve=session\|permanent` to create auto-approve rule |
| `POST` | `/api/v1/approvals/{id}/deny` | `approvals:write` | Deny a pending request |
| `GET` | `/api/v1/auto-approve` | `approvals:read` | List auto-approve rules (filter by `?agent=`) |
| `POST` | `/api/v1/auto-approve` | `approvals:write` | Create an auto-approve rule |
| `DELETE` | `/api/v1/auto-approve/{id}` | `approvals:write` | Delete an auto-approve rule |
| `GET` | `/api/v1/keys` | `admin` | List API keys (secrets not returned) |
| `POST` | `/api/v1/keys` | `admin` | Create a new API key |
| `DELETE` | `/api/v1/keys/{id}` | `admin` | Revoke an API key |
| `DELETE` | `/api/v1/keys/{id}/permanent` | `admin` | Permanently delete a revoked key |
| `POST` | `/api/v1/keys/{id}/rotate` | `admin` | Rotate an API key |
| `GET` | `/api/v1/tools` | `tools:read` | List MCP tool servers |
| `GET` | `/api/v1/tools/{name}` | `tools:read` | Get tool server details |
| `POST` | `/api/v1/tools` | `tools:write` | Add a tool server |
| `PUT` | `/api/v1/tools/{name}` | `tools:write` | Edit a tool server |
| `DELETE` | `/api/v1/tools/{name}` | `tools:write` | Remove a tool server |
| `GET` | `/api/v1/tools/{name}/health` | `tools:read` | Tool server health status |
| `POST` | `/api/v1/tools/{name}/restart` | `tools:write` | Manually restart a tool server |
| `GET` | `/api/v1/plugins` | `tools:read` | List plugins |
| `GET` | `/api/v1/plugins/{name}` | `tools:read` | Get plugin details |
| `POST` | `/api/v1/plugins` | `tools:write` | Add a plugin |
| `DELETE` | `/api/v1/plugins/{name}` | `tools:write` | Remove a plugin |
| `GET` | `/api/v1/kv/{agent}` | `kv:read` | List KV keys for an agent |
| `GET` | `/api/v1/kv/{agent}/{key}` | `kv:read` | Get a KV key value |
| `PUT` | `/api/v1/kv/{agent}/{key}` | `kv:write` | Set a KV key value (body: `{"value":"...","ttl":"5m"}`) |
| `DELETE` | `/api/v1/kv/{agent}/{key}` | `kv:write` | Delete a KV key |
| `GET` | `/api/v1/channels` | `channels:read` | List all channels |
| `POST` | `/api/v1/channels` | `channels:write` | Create a channel |
| `GET` | `/api/v1/channels/{name}` | `channels:read` | Channel detail |
| `PATCH` | `/api/v1/channels/{name}` | `channels:write` | Update a channel |
| `DELETE` | `/api/v1/channels/{name}` | `channels:write` | Remove a channel |
| `POST` | `/api/v1/channels/{name}/activate` | `channels:write` | Set active channel for an adapter key |
| `DELETE` | `/api/v1/channels/{name}/activate` | `channels:write` | Clear active channel override |
| `GET` | `/api/v1/audit` | `audit:read` | List audit events (filter by `?category=&agent=&status=&since=&until=`) |
| `GET` | `/api/v1/audit/stats` | `audit:read` | Aggregate counts by category/status |

**Chat example:**

```bash
# Non-streaming
curl -X POST http://localhost:8080/api/v1/chat \
  -H "Authorization: Bearer dk-your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello!", "session_id": "my-session"}'

# SSE streaming
curl -X POST http://localhost:8080/api/v1/chat \
  -H "Authorization: Bearer dk-your-secret-key" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"message": "Hello!", "session_id": "my-session"}'
```

Pass the same `session_id` in subsequent requests to continue the conversation. Omit it to start a new session with an auto-generated ID.

## Development

[just](https://github.com/casey/just) is used as the command runner. Run `just` to see all available recipes:

```
just build           # Build the denkeeper binary (requires web/dist/ to exist)
just build-ui        # Build the Svelte web dashboard (requires Node.js)
just build-full      # Build web dashboard then Go binary in one step
just serve           # Start the agent (just serve ./path/to/config.toml)
just web-dev         # Start Vite dev server for dashboard hot-reload
just test            # Run all tests with race detector
just test-v          # Verbose test output
just test-pkg <pkg>  # Test a single package (e.g. just test-pkg internal/agent)
just test-cover      # Tests with coverage report
just test-cover-html # Open coverage in browser
just test-ui         # Web UI tests (Vitest + jsdom + MSW)
just test-integration # E2E integration tests (full in-process server + mock LLM)
just lint            # Run golangci-lint
just lint-fix        # Lint with auto-fix
just fmt             # Format all Go files
just fmt-check       # CI-friendly format check
just vet             # Run go vet
just check           # Run all checks (fmt + vet + lint + test)
just openapi         # Generate OpenAPI spec (requires swag CLI)
just tidy            # go mod tidy
just clean           # Remove build artifacts
just loc             # Count lines of source vs test code
```

### Project structure

```
cmd/denkeeper/       Entry point
internal/
  adapter/           Platform integrations
    telegram/        Telegram bot adapter
    discord/         Discord bot adapter
  agent/             Dispatcher, engine, and conversation memory
  api/               External REST API server
  approval/          Approval workflow manager, store, registry, and callback handler
  config/            TOML config parsing and validation
  configmcp/         Per-agent Config MCP server (skill/schedule/tier/tool/KV tools)
  kv/                Per-agent key-value store with TTL
  llm/               Provider interface, router, cost tracking
    anthropic/       Anthropic direct client
    openai/          OpenAI direct client (Azure/vLLM-compatible)
    openrouter/      OpenRouter client
    ollama/          Ollama local inference client
  persona/           Persona file loader (SOUL.md, USER.md, MEMORY.md)
  plugin/            Plugin manager (subprocess and Docker-sandboxed)
  sandbox/           Pluggable sandbox runtime (Docker and Kubernetes backends)
  scheduler/         Cron and interval scheduling
  security/          Permission engine (tiers) and Ed25519 plugin signing
  skill/             Skill file loader, trigger matching, merging
  tool/              MCP tool server manager
    oauth/           MCP OAuth 2.1 authorization for remote tool servers
  voice/             STT/TTS provider interface
    openai/          OpenAI Whisper + TTS client
  web/               Embedded web dashboard handler (serves web/dist/)
web/                 Svelte dashboard source (npm build → web/dist/)
pkg/bin/             Build output (gitignored)
agents/default/
  skills/            Bundled skills (e.g. help.md)
  SOUL.md            Agent personality
```

## License

[Apache 2.0](LICENSE)
