feat: add setup
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
GATEWAY_REF=main
|
||||||
|
|
||||||
|
# Uncomment when developing against a sibling ../beaver-gateway checkout.
|
||||||
|
# Pure code edits then need only `docker compose restart gateway`
|
||||||
|
#COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml
|
||||||
|
|
||||||
|
POSTGRES_USER=beaver
|
||||||
|
POSTGRES_PASSWORD=CHANGE-ME-strong-random-password
|
||||||
|
POSTGRES_DB=beaver
|
||||||
|
|
||||||
|
# Admin UI login. Used only to mint/revoke tokens.
|
||||||
|
ADMIN_USER=admin
|
||||||
|
ADMIN_PASS=CHANGE-ME-admin-password
|
||||||
|
|
||||||
|
# openssl rand -hex 32
|
||||||
|
SESSION_SECRET=CHANGE-ME-random-hex
|
||||||
|
|
||||||
|
# Sniffed from the Raycast desktop app
|
||||||
|
RAYCAST_DEVICE_ID=CHANGE-ME-32-byte-hex
|
||||||
|
RAYCAST_BEARER=rca_CHANGE-ME
|
||||||
|
|
||||||
|
FIREFLY_BASE_URL=https://firefly.example.com
|
||||||
|
FIREFLY_PAT=changeme
|
||||||
|
|
||||||
|
# example: CALENDAR_MCPS=work=https://calendar-mcp.com/mcp/AAAA,home=https://calendar-mcp.com/mcp/BBBB
|
||||||
|
CALENDAR_MCPS=
|
||||||
|
|
||||||
|
PORT_MESSAGES=62990
|
||||||
|
PORT_MCP=62991
|
||||||
|
PORT_ADMIN=62992
|
||||||
|
PORT_MARKDOWN=62993
|
||||||
|
|
||||||
|
# Public URL where the reverse proxy in front (see ./caddy) terminates.
|
||||||
|
# Leave empty for raw-port localhost / dev setups.
|
||||||
|
PUBLIC_BASE_URL=
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
config.json
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
.idea
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# beaver-agent
|
||||||
|
|
||||||
|
My real beaver-gateway setup (paired with the protocol from beaver.kotikot.com).
|
||||||
|
|
||||||
|
Uses Claude Code and Raycast subscriptions for agents, has some MCPs set up. You can use this as-is or modify to match your needs (config.py).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker + Docker Compose
|
||||||
|
- Obsidian Sync (for `obsidian-headless`)
|
||||||
|
- claude.ai sub
|
||||||
|
- raycast sub
|
||||||
|
- Firefly III and its PAT
|
||||||
|
|
||||||
|
## First launch
|
||||||
|
|
||||||
|
### 1. Create `.env` and `raycast.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# fill CHANGE-ME
|
||||||
|
nvim .env
|
||||||
|
|
||||||
|
# build raycast config on your mac (beta by default, consider checking raycast-api for instructions)
|
||||||
|
raycast-api init
|
||||||
|
|
||||||
|
# copy config.json to your deployment server
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set up Obsidian Sync
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it beaver-obsidian ob login
|
||||||
|
docker exec -it beaver-obsidian ob sync-setup --vault "yourvault"
|
||||||
|
docker restart beaver-obsidian
|
||||||
|
```
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec beaver-obsidian ls /vault
|
||||||
|
```
|
||||||
|
|
||||||
|
Only markdown is synced by default. To sync everything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec beaver-obsidian ob sync-config \
|
||||||
|
--file-types image,audio,video,pdf,unsupported
|
||||||
|
docker restart beaver-obsidian
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Set up claude
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it beaver-gateway claude /login
|
||||||
|
docker exec -it beaver-gateway bash -c 'cd /vault && claude --dangerously-skip-permissions --model claude-opus-4-7'
|
||||||
|
```
|
||||||
|
|
||||||
|
Click through every dialog, then `/exit` or Ctrl+C.
|
||||||
|
|
||||||
|
### 4. Mint a token
|
||||||
|
|
||||||
|
Open admin at `http://localhost:62992` (or `https://<DOMAIN>/admin/` if Caddy), sign in with `ADMIN_USER` / `ADMIN_PASS`, go to **Tokens → Create**, scope `*` for first run.
|
||||||
|
|
||||||
|
### 5. Smoke test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec beaver-gateway ls /vault | head
|
||||||
|
docker logs beaver-gateway | grep -i "agent registered"
|
||||||
|
|
||||||
|
curl http://localhost:62990/v1/models \
|
||||||
|
-H "Authorization: Bearer <YOUR_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where to plug things in
|
||||||
|
|
||||||
|
The admin dashboard renders ready-to-copy URLs and snippets for each of these
|
||||||
|
|
||||||
|
- any Anthropic client: `http://localhost:62990` or `https://<DOMAIN>/anthropic`, model = agent name (`beaver-opus-high` etc)
|
||||||
|
- MCP clients (Claude Desktop, Raycast extension): just find in admin
|
||||||
|
- Obsidian companion plugin: paste the dashboard's **plugin base** (`http://localhost:62993` raw, or `https://<DOMAIN>/md` behind Caddy) into the plugin's "Base URL" setting
|
||||||
|
- **Admin UI:** `http://localhost:62992` or `https://<DOMAIN>/admin`, login from `ADMIN_USER` / `ADMIN_PASS`
|
||||||
|
|
||||||
|
## Exposing to the internet
|
||||||
|
|
||||||
|
Reference config in `caddy`. Designed for: gateway on rpi, Caddy on a server with a public IP, wired through tailscale.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd caddy
|
||||||
|
cp Caddyfile.example Caddyfile
|
||||||
|
cp .env.example .env # only for cloudflare
|
||||||
|
# replace <DOMAIN>; point upstreams at wherever the gateway is reachable
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `PUBLIC_BASE_URL=https://<DOMAIN>` in beaver-agent's `.env`. For per-subdomain layout, rewrite the Caddyfile and pass full `public_base_url=...` per frontend in `config.py`.
|
||||||
|
|
||||||
|
## Useful commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# shell into the gateway container (claude CLI, bun, python with venv all live here)
|
||||||
|
docker exec -it beaver-gateway bash
|
||||||
|
|
||||||
|
# what obsidian-sync has pulled
|
||||||
|
docker exec beaver-obsidian ob status
|
||||||
|
|
||||||
|
# force a one-off sync (don't wait for the continuous tick)
|
||||||
|
docker exec beaver-obsidian ob sync
|
||||||
|
|
||||||
|
# per-service logs
|
||||||
|
docker compose logs -f gateway
|
||||||
|
docker compose logs -f obsidian-headless
|
||||||
|
|
||||||
|
# pull the latest beaver-gateway (when GATEWAY_REF=main)
|
||||||
|
docker compose build gateway && docker compose up -d gateway
|
||||||
|
|
||||||
|
# reload config.py without a full rebuild
|
||||||
|
docker compose restart gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **claude in the container doesn't see the vault** - `cwd=VAULT` in `config.py` resolves to `/vault` *inside* the container, not on the host. Don't change it.
|
||||||
|
- **gateway hangs on `JSONL file did not appear within 30s`** - claude's onboarding wasn't clicked through (see step 3). Re-enter interactively with `docker exec -it beaver-gateway ...`, click trust folder + bypass permissions, exit.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CLOUDFLARE_API_TOKEN=
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Caddyfile
|
||||||
|
.env
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
admin off
|
||||||
|
# acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||||
|
|
||||||
|
log {
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
|
||||||
|
servers {
|
||||||
|
trusted_proxies cloudflare
|
||||||
|
client_ip_headers Cf-Connecting-Ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<DOMAIN> {
|
||||||
|
redir /anthropic /anthropic/ 308
|
||||||
|
redir /mcp /mcp/ 308
|
||||||
|
redir /admin /admin/ 308
|
||||||
|
redir /md /md/ 308
|
||||||
|
|
||||||
|
handle_path /anthropic/* {
|
||||||
|
reverse_proxy host.docker.internal:62990
|
||||||
|
}
|
||||||
|
handle_path /mcp/* {
|
||||||
|
reverse_proxy host.docker.internal:62991
|
||||||
|
}
|
||||||
|
handle_path /admin/* {
|
||||||
|
reverse_proxy host.docker.internal:62992
|
||||||
|
}
|
||||||
|
handle_path /md/* {
|
||||||
|
reverse_proxy host.docker.internal:62993
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
image: ghcr.io/caddybuilds/caddy-cloudflare:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:80:80"
|
||||||
|
- "0.0.0.0:443:443"
|
||||||
|
- "0.0.0.0:443:443/udp"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- caddy_data:/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import os
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from beaver_gateway.agents.base import ExposedMcp
|
||||||
|
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeCodeOptions
|
||||||
|
from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences
|
||||||
|
from beaver_gateway.core.registry import Gateway
|
||||||
|
from beaver_gateway.core.turn_record import TurnRecord, slugify
|
||||||
|
from beaver_gateway.frontends.admin import AdminFrontend
|
||||||
|
from beaver_gateway.frontends.anthropic import AnthropicMessagesFrontend
|
||||||
|
from beaver_gateway.frontends.markdown import MarkdownFrontend
|
||||||
|
from beaver_gateway.frontends.mcp_server import McpServerFrontend
|
||||||
|
from beaver_gateway.mcp.types import HttpMcp, McpServer
|
||||||
|
|
||||||
|
|
||||||
|
VAULT = Path("/vault")
|
||||||
|
CHATS_DIR = VAULT / "💬 чаты"
|
||||||
|
|
||||||
|
|
||||||
|
def chat_log_path(record: TurnRecord, vault: Path) -> Path:
|
||||||
|
today = date.today()
|
||||||
|
topic = slugify(record.first_user_text, maxlen=60)
|
||||||
|
rel = CHATS_DIR.relative_to(vault) / f"{today:%Y-%m}" / f"{today:%Y-%m-%d} - {topic}.md"
|
||||||
|
return vault / rel
|
||||||
|
|
||||||
|
|
||||||
|
def _calendar_mcps() -> list[HttpMcp]:
|
||||||
|
raw = os.environ.get("CALENDAR_MCPS", "").strip()
|
||||||
|
servers: list[HttpMcp] = []
|
||||||
|
for entry in raw.split(","):
|
||||||
|
entry = entry.strip()
|
||||||
|
if not entry:
|
||||||
|
continue
|
||||||
|
name, sep, url = entry.partition("=")
|
||||||
|
if not sep or not url.strip():
|
||||||
|
raise ValueError(f"CALENDAR_MCPS entry must be `name=url`, got: {entry!r}")
|
||||||
|
servers.append(McpServer.http(name=f"calendar-{name.strip()}", url=url.strip()))
|
||||||
|
return servers
|
||||||
|
|
||||||
|
|
||||||
|
calendar_mcps = _calendar_mcps()
|
||||||
|
calendar_exposed = tuple(ExposedMcp(name=m.name) for m in calendar_mcps)
|
||||||
|
|
||||||
|
mcps = [
|
||||||
|
McpServer.stdio(
|
||||||
|
name="obsidian-fs",
|
||||||
|
command=[
|
||||||
|
"bunx",
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-filesystem",
|
||||||
|
"/vault",
|
||||||
|
],
|
||||||
|
lenient=True,
|
||||||
|
),
|
||||||
|
McpServer.stdio(
|
||||||
|
name="firefly",
|
||||||
|
command=[
|
||||||
|
"bunx",
|
||||||
|
"-y",
|
||||||
|
"@firefly-iii-mcp/local",
|
||||||
|
"--pat",
|
||||||
|
os.environ["FIREFLY_PAT"],
|
||||||
|
"--baseUrl",
|
||||||
|
os.environ["FIREFLY_BASE_URL"],
|
||||||
|
"--preset",
|
||||||
|
"default",
|
||||||
|
],
|
||||||
|
lenient=True,
|
||||||
|
),
|
||||||
|
*calendar_mcps,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
CBO_PROMPT = (Path(__file__).parent / "prompt.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
UserPrefsRu = lambda: UserPreferences( # noqa: E731
|
||||||
|
locale="ru-RU",
|
||||||
|
timezone="Europe/Warsaw",
|
||||||
|
current_date=date.today().isoformat(), # noqa: DTZ011
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def claude(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
|
||||||
|
return ClaudeAgent(
|
||||||
|
name=name,
|
||||||
|
model=model,
|
||||||
|
system_prompt=CBO_PROMPT,
|
||||||
|
cwd=VAULT,
|
||||||
|
options=ClaudeCodeOptions(effort=effort, extra_args=("--remote-control",)),
|
||||||
|
expose_mcps=(ExposedMcp(name="firefly"), *calendar_exposed),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def raycast(name: str, model: str, reasoning_effort: str | None = None) -> RaycastAgent:
|
||||||
|
return RaycastAgent(
|
||||||
|
name=name,
|
||||||
|
model=model,
|
||||||
|
system_prompt=CBO_PROMPT,
|
||||||
|
reasoning_effort=reasoning_effort,
|
||||||
|
available_native_tools=(RemoteTool.WEB_SEARCH, RemoteTool.READ_PAGE),
|
||||||
|
user_preferences=UserPrefsRu,
|
||||||
|
expose_mcps=(
|
||||||
|
ExposedMcp(name="obsidian-fs"),
|
||||||
|
ExposedMcp(name="firefly"),
|
||||||
|
*calendar_exposed,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
agents = [
|
||||||
|
claude("beaver-opus-high", "claude-opus-4-7", effort="high"),
|
||||||
|
raycast("beaver-gemini-pro-high", "google-gemini-3.1-pro", reasoning_effort="high"),
|
||||||
|
claude("beaver-opus-medium", "claude-opus-4-7", effort="medium"),
|
||||||
|
claude("beaver-opus-xhigh", "claude-opus-4-7", effort="xhigh"),
|
||||||
|
raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"),
|
||||||
|
raycast("beaver-gemini-flash-high", "google-gemini-3.5-flash", reasoning_effort="high"),
|
||||||
|
raycast("beaver-gemini-flash-low", "google-gemini-3.5-flash", reasoning_effort="low"),
|
||||||
|
]
|
||||||
|
|
||||||
|
PUBLIC_BASE_URL = os.environ.get("PUBLIC_BASE_URL", "").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _public(suffix: str) -> str | None:
|
||||||
|
return f"{PUBLIC_BASE_URL}{suffix}" if PUBLIC_BASE_URL else None
|
||||||
|
|
||||||
|
|
||||||
|
frontends = [
|
||||||
|
AnthropicMessagesFrontend(
|
||||||
|
host="0.0.0.0", port=62990, public_base_url=_public("/anthropic")
|
||||||
|
),
|
||||||
|
McpServerFrontend(
|
||||||
|
host="0.0.0.0", port=62991, public_base_url=_public("/mcp")
|
||||||
|
),
|
||||||
|
AdminFrontend(
|
||||||
|
host="0.0.0.0", port=62992, public_base_url=_public("/admin")
|
||||||
|
),
|
||||||
|
MarkdownFrontend(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=62993,
|
||||||
|
vault_path=CHATS_DIR,
|
||||||
|
default_agent="research",
|
||||||
|
log_all_chats=True,
|
||||||
|
log_path=chat_log_path,
|
||||||
|
public_base_url=_public("/md"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
gateway = Gateway(agents=agents, mcps=mcps, frontends=frontends)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: ../beaver-gateway
|
||||||
|
volumes:
|
||||||
|
- ../beaver-gateway/src/beaver_gateway:/app/src/beaver_gateway
|
||||||
|
environment:
|
||||||
|
PYTHONDONTWRITEBYTECODE: "1"
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
x-restart: &restart
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
<<: *restart
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${PORT_POSTGRES:-5432}:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
obsidian-headless:
|
||||||
|
container_name: beaver-obsidian
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: obsidian.Dockerfile
|
||||||
|
<<: *restart
|
||||||
|
volumes:
|
||||||
|
- vault:/vault
|
||||||
|
- obsidian-config:/root/.config
|
||||||
|
working_dir: /vault
|
||||||
|
|
||||||
|
gateway:
|
||||||
|
container_name: beaver-gateway
|
||||||
|
build:
|
||||||
|
context: https://git.kotikot.com/beaver/beaver-gateway.git#${GATEWAY_REF:-main}
|
||||||
|
<<: *restart
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
CONFIG_PATH: /config/config.py
|
||||||
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-beaver}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-beaver}
|
||||||
|
RAYCAST_CONFIG_PATH: /config/config.json
|
||||||
|
IS_SANDBOX: "1"
|
||||||
|
ports:
|
||||||
|
- "${PORT_MESSAGES:-62990}:62990" # anthropic
|
||||||
|
- "${PORT_MCP:-62991}:62991" # mcp
|
||||||
|
- "${PORT_ADMIN:-62992}:62992" # admin
|
||||||
|
- "${PORT_MARKDOWN:-62993}:62993" # obsidian companion
|
||||||
|
volumes:
|
||||||
|
- ./config.py:/config/config.py:ro
|
||||||
|
- ./prompt.md:/config/prompt.md:ro
|
||||||
|
- ./config.json:/config/config.json:ro
|
||||||
|
- vault:/vault
|
||||||
|
- claude-home:/root/.claude
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
mkdir -p /root/.claude
|
||||||
|
if [ -f /root/.claude.json ] && [ ! -L /root/.claude.json ]; then
|
||||||
|
if [ ! -e /root/.claude/claude.json ]; then
|
||||||
|
mv /root/.claude.json /root/.claude/claude.json
|
||||||
|
else
|
||||||
|
rm /root/.claude.json
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -e /root/.claude/claude.json ] || echo '{}' > /root/.claude/claude.json
|
||||||
|
ln -sf /root/.claude/claude.json /root/.claude.json
|
||||||
|
exec python -m beaver_gateway
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
vault:
|
||||||
|
obsidian-config:
|
||||||
|
claude-home:
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN npm install -g obsidian-headless@latest
|
||||||
|
|
||||||
|
WORKDIR /vault
|
||||||
|
|
||||||
|
COPY <<'EOF' /usr/local/bin/entrypoint.sh
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONFIG_DIR=/root/.config/obsidian-headless
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG_DIR/auth_token" ]; then
|
||||||
|
echo "[obsidian-headless] not logged in — run:"
|
||||||
|
echo " docker exec -it beaver-obsidian ob login"
|
||||||
|
echo "then restart this container."
|
||||||
|
exec sleep infinity
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ls "$CONFIG_DIR"/sync/*/config.json >/dev/null 2>&1; then
|
||||||
|
echo "[obsidian-headless] vault not configured — run:"
|
||||||
|
echo " docker exec -it beaver-obsidian ob sync-setup --vault '<vault name>'"
|
||||||
|
echo "then restart this container."
|
||||||
|
exec sleep infinity
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec ob sync --continuous
|
||||||
|
EOF
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<role>
|
||||||
|
You are the Chief Beaver Officer (Менеджер Бобрения) - an AI agent powering the LifeOS personal operating system.
|
||||||
|
|
||||||
|
Your purpose: Help the user operate their life with maximum efficiency. You are not a therapist, not a friend, not a motivational coach. You are a COO - you manage operations, planning, and execution.
|
||||||
|
|
||||||
|
Core identity:
|
||||||
|
- The USER is "The Beaver" (Бобёр) - a builder who operates through action
|
||||||
|
- YOU are the Chief Beaver Officer - managing the beavering process
|
||||||
|
- "Beavering" (Бобрение) = state of focused, productive work. Hard processing. Building.
|
||||||
|
- Your job: keep The Beaver in beavering mode, remove obstacles, maintain momentum
|
||||||
|
|
||||||
|
You exist inside the user's knowledge management system (Obsidian vault) - their second brain containing projects, people, tasks, daily logs, knowledge, and life documentation.
|
||||||
|
</role>
|
||||||
|
|
||||||
|
<philosophy>
|
||||||
|
CORE PRINCIPLE: "Action cures fear"
|
||||||
|
|
||||||
|
Derivatives:
|
||||||
|
- Overthinking is the enemy. Movement creates clarity.
|
||||||
|
- A bad plan executed today beats a perfect plan next week.
|
||||||
|
- When stuck → one small action → momentum → unstuck.
|
||||||
|
- Analysis paralysis is a bug. You are the debugger.
|
||||||
|
|
||||||
|
You embody this philosophy in every interaction. No coddling, no endless reflection loops, no "have you considered how you feel about this?" - instead: "Here's what to do. Go."
|
||||||
|
</philosophy>
|
||||||
|
|
||||||
|
<user-profile>
|
||||||
|
The Beaver is a builder. Direct, action-oriented, allergic to fluff.
|
||||||
|
|
||||||
|
You can: be blunt, push when stuck, use stoic-style humour.
|
||||||
|
You should not: moralize, hedge, add caveats, treat them as fragile.
|
||||||
|
</user-profile>
|
||||||
|
|
||||||
|
<operating-modes>
|
||||||
|
You auto-detect the appropriate mode from context. No need to announce it.
|
||||||
|
|
||||||
|
### Axis 1: DEPTH
|
||||||
|
|
||||||
|
**Quick Mode**
|
||||||
|
- User asks something general or wants a fast answer
|
||||||
|
- Respond from your knowledge, your style, any length appropriate
|
||||||
|
- Do NOT dive into vault research unless clearly needed
|
||||||
|
- Examples: coding questions, recipes, facts, casual chat, opinions
|
||||||
|
|
||||||
|
**Deep Mode**
|
||||||
|
- Topic touches user's personal system/life
|
||||||
|
- Switch to "gather context first" approach
|
||||||
|
- Ask clarifying questions if needed
|
||||||
|
- Go into vault: check roadmap, boards, relevant notes
|
||||||
|
- Structure and plan before executing
|
||||||
|
- Examples: planning, projects, people in their life, tasks, studying, decisions
|
||||||
|
|
||||||
|
**Trigger for Deep Mode - topic involves:**
|
||||||
|
- People (relationships, contacts, social)
|
||||||
|
- Projects (work, side projects, creative)
|
||||||
|
- Tasks and planning (what to do, priorities)
|
||||||
|
- Study/education (exams, courses, materials)
|
||||||
|
- Personal items (belongings, tools, places)
|
||||||
|
- Events (trips, experiences, logs)
|
||||||
|
- Reflections (thoughts, journaling, life decisions)
|
||||||
|
|
||||||
|
If unsure → start Quick, switch to Deep if you realize vault context would help.
|
||||||
|
|
||||||
|
### Axis 2: CONTEXT
|
||||||
|
|
||||||
|
**Operational**
|
||||||
|
- User is functional, working on something
|
||||||
|
- Normal mode: help with the task
|
||||||
|
- Can push, challenge, be demanding
|
||||||
|
- Focus on results and execution
|
||||||
|
|
||||||
|
**Crisis**
|
||||||
|
- User is overwhelmed, burned out, or having a rough time
|
||||||
|
- Be a calm, grounded presence
|
||||||
|
- Offer one small concrete step (not a plan)
|
||||||
|
- Match their pace - no rushing
|
||||||
|
- Listen more, fix less
|
||||||
|
</operating-modes>
|
||||||
|
|
||||||
|
<vault-access>
|
||||||
|
You are running inside the user's Obsidian vault, mounted at /vault.
|
||||||
|
Read AGENTS.md at the vault root before doing anything substantive -
|
||||||
|
it has the directory map and conventions.
|
||||||
|
|
||||||
|
`[[wikilinks]]` are first-class. Use them when you reference notes;
|
||||||
|
follow them by reading the target file when you see them.
|
||||||
|
</vault-access>
|
||||||
|
|
||||||
|
<vault-awareness>
|
||||||
|
The vault typically contains these domains (triggers for Deep mode):
|
||||||
|
|
||||||
|
- **People** - personal/professional contacts, relationship history
|
||||||
|
- **Projects** - active work, archives, materials
|
||||||
|
- **Tasks** - kanban boards, lists, scheduled items
|
||||||
|
- **Daily logs** - journal entries, timestamps
|
||||||
|
- **Knowledge** - skills, problem→solution notes, cheatsheets
|
||||||
|
- **Education** - courses, study materials
|
||||||
|
- **Research** - deep dives, investigations
|
||||||
|
- **Objects** - belongings, tools, software
|
||||||
|
- **Places** - locations, bookmarks
|
||||||
|
- **Events** - trips, experiences, trip reports
|
||||||
|
- **Thoughts** - manifestos, philosophy, identity-level ideas
|
||||||
|
- **Media** - books, shows, music consumed
|
||||||
|
|
||||||
|
When user mentions something from these domains → consider going into vault for context.
|
||||||
|
When topic is general/external → respond from your knowledge.
|
||||||
|
</vault-awareness>
|
||||||
|
|
||||||
|
<interaction-guidelines>
|
||||||
|
**Language:** Russian
|
||||||
|
|
||||||
|
**Tone:**
|
||||||
|
- Professional but not corporate
|
||||||
|
- Direct but not cold
|
||||||
|
- Can use humor, sarcasm, light roasts (stoic style)
|
||||||
|
- High energy when pushing, calm when supporting
|
||||||
|
- No empty filler phrases, no over-apologizing
|
||||||
|
|
||||||
|
**Style:**
|
||||||
|
- Get to the point fast
|
||||||
|
- Structure when helpful, prose when natural
|
||||||
|
- Use their terminology and references naturally
|
||||||
|
- Match their energy level
|
||||||
|
|
||||||
|
**Naming/Branding (use naturally, not forced):**
|
||||||
|
- "Beavering" (Бобрение) / "Beaver mode" - productive state
|
||||||
|
- "Action cures fear" - when they're stuck
|
||||||
|
- "Chief Beaver Officer" - your role (sparingly)
|
||||||
|
- Can create derivatives and variations
|
||||||
|
</interaction-guidelines>
|
||||||
|
|
||||||
|
<constraints>
|
||||||
|
- Don't invent vault content you haven't read
|
||||||
|
- Don't write to vault without permission (ask first)
|
||||||
|
- Don't create files/folders unless explicitly requested
|
||||||
|
- Don't announce your mode ("switching to Deep mode...") - just do it
|
||||||
|
- Don't fake emotions or pretend to be human
|
||||||
|
- Don't break character into generic assistant mode
|
||||||
|
- Chats themselves live in vault. Wikilinks you write resolve in Obsidian; broken links show up as dead links to the user. Only `[[link]]` to notes you've verified exist.
|
||||||
|
</constraints>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# only for config.py development
|
||||||
|
[project]
|
||||||
|
name = "beaver-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"beaver-gateway[prod]",
|
||||||
|
"raycast-api[discovery]",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
beaver-gateway = { git = "https://git.kotikot.com/beaver/beaver-gateway.git", branch = "main" }
|
||||||
|
raycast-api = { git = "https://git.kotikot.com/beaver/raycast-api" }
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"venvPath": ".",
|
||||||
|
"venv": ".venv",
|
||||||
|
"pythonVersion": "3.13",
|
||||||
|
"include": ["config.py"],
|
||||||
|
"exclude": [".venv", "**/__pycache__"],
|
||||||
|
"reportMissingImports": "error",
|
||||||
|
"reportMissingTypeStubs": "none"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user