diff --git a/family/how-to/claude-python-cli-proxy.md b/family/how-to/claude-python-cli-proxy.md deleted file mode 100644 index 3a840971..00000000 --- a/family/how-to/claude-python-cli-proxy.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: claude-code-openai-wrapper — Python Claude CLI Proxy -type: reference -namespace: work -tags: - - hermes - - mac - - eagle - - claude-proxy - - python - - llm-backend - - pitfalls -created: 2026-06-03 -updated: 2026-06-03 -confidence: 0.95 ---- - -# claude-code-openai-wrapper — Python Claude CLI Proxy - -OpenAI-compatible FastAPI server that wraps the `claude` CLI via the official -`claude-agent-sdk`. Runs at **port 8090** on Eagle. Hermes uses it as its LLM -backend (`base_url: http://localhost:8090/v1`). - -## Why this exists - -Hermes needs an OpenAI-compatible endpoint that routes calls through the local -`claude` CLI session (Max/OAuth subscription, no API key). Two wrappers were -evaluated — see below. The Python one is currently active. - -### Node.js wrapper (port 3456) — status: broken streaming, kept as fallback - -**Package:** `openclaw-claude-proxy` v1.0.8 (npm) -**Repo:** [mehdic/openclaw-claude-proxy](https://github.com/mehdic/openclaw-claude-proxy) -**LaunchAgent:** `ai.claude-proxy.plist` → `~/.local/bin/claude-proxy-start.sh` - -**Issue:** uses a reverse-engineered `stream-json` protocol to talk to `claude` -as a long-lived subprocess. This protocol broke with claude CLI ≥ 2.1.141. -Streaming requests stall after the initial role chunk — only keepalives come -through. Non-streaming (`--print` fallback path) still works. - -The proxy has `CLAUDE_PROXY_RUNTIME=stream-json` (default) and -`CLAUDE_PROXY_RUNTIME=print` (fallback). Switching to `print` fixes -hangs but removes true incremental streaming. - ---- - -### Python wrapper (port 8090) — status: **active** - -**Repo:** [RichardAtCT/claude-code-openai-wrapper](https://github.com/RichardAtCT/claude-code-openai-wrapper) -**Local path:** `/Users/admin/openclaw/claude-proxy/` -**Version (pyproject.toml):** 2.2.0 -**SDK:** `claude-agent-sdk` 0.2.88 (updated June 2026 from 0.1.56) -**Bundled CLI:** 2.1.161 (inside SDK venv, used instead of system CLI — see pitfalls) -**LaunchAgent:** `com.openclaw.claude-proxy.plist` → `~/.local/bin/claude-python-proxy-start.sh` - -#### How it works - -``` -Hermes → POST /v1/chat/completions (port 8090) - └── FastAPI (uvicorn) - └── claude-agent-sdk query() - └── spawns .venv/…/_bundled/claude 2.1.161 - --output-format stream-json --verbose - (reads CLAUDE_CODE_OAUTH_TOKEN from env) - → streams AssistantMessage events → SSE chunks -``` - -Key: the SDK uses its own **bundled** `claude` binary (not the system one at -`/opt/homebrew/bin/claude`) because the SDK's stream-json protocol must match -the exact CLI version it was built against. - -#### Hermes config - -`~/.hermes/config.yaml`: -```yaml -model: - default: claude-sonnet-4-6 - provider: custom - base_url: 'http://localhost:8090/v1' -``` - ---- - -## What was fixed (June 2026) - -### 1. Venv Python version mismatch - -Venv was created with Python 3.14.3, Homebrew had upgraded to 3.14.5. -Compiled C extensions (`.so` files) were incompatible → `pip install` crashed -with `ImportError: Symbol not found: _XML_SetAllocTrackerActivationThreshold`. - -**Fix:** rebuilt venv with `uv`: -```bash -cd /Users/admin/openclaw/claude-proxy -uv venv .venv --python python3.14 --clear -uv pip install fastapi "uvicorn[standard]" pydantic python-dotenv httpx \ - sse-starlette python-multipart claude-agent-sdk slowapi -``` - -### 2. SDK too old — bundled CLI mismatch - -`claude-agent-sdk` 0.1.56 bundled `claude` 2.1.92. Between 2.1.92 and -2.1.141+, Anthropic changed how the stream-json protocol is initiated: -- Old: inferred from `--output-format stream-json` -- New: requires `--input-format stream-json` flag explicitly - -Result: SDK sent `control_request` on stdin; new CLI ignored it (no output, -exited code 1). - -**Fix:** `uv pip install claude-agent-sdk` → upgraded to 0.2.88, bundled CLI -2.1.161 which matches the updated SDK protocol. - -### 3. `cli_path=SYSTEM_CLAUDE_PATH` override - -The wrapper hard-coded `cli_path = shutil.which("claude")` (system 2.1.145) -in `ClaudeAgentOptions`. This forced the SDK to use the system CLI instead -of its own bundled binary, breaking the protocol handshake. - -**Fix:** set `SYSTEM_CLAUDE_PATH = None` in `src/claude_cli.py` so the SDK -auto-discovers and uses its bundled CLI. - -### 4. LaunchAgent missing auth token - -The original plist ran `uvicorn` directly and only set `CLAUDE_AUTH_METHOD=cli`. -The `claude` subprocess (spawned by the SDK) inherits the process env, which -had no `CLAUDE_CODE_OAUTH_TOKEN`. CLI responded: *"Not logged in"*. - -**Fix:** added wrapper script `~/.local/bin/claude-python-proxy-start.sh` that -sources `~/.hermes/.env` before starting uvicorn (same pattern as the Node.js -proxy). Updated plist `ProgramArguments` to call the wrapper instead of -uvicorn directly. - -### 5. Expired OAuth token (root cause of 401s) - -`CLAUDE_CODE_OAUTH_TOKEN` in `~/.hermes/.env` expires (access tokens are -short-lived). When the stored token is expired and passed to the CLI -explicitly via env, the CLI uses it as-is → 401 from Anthropic API. The -CLI does **not** auto-refresh when `CLAUDE_CODE_OAUTH_TOKEN` is set to an -expired value in the env. - -**Symptom:** both proxies return `401 Invalid authentication credentials` -even though `claude` works fine in the terminal (terminal has a fresh token -from a previous session). - -**Immediate fix:** copy fresh token from terminal into `~/.hermes/.env`: -```bash -# In the terminal that has a working claude session: -grep CLAUDE_CODE_OAUTH_TOKEN <(env) -# Then update: -sed -i '' "s|CLAUDE_CODE_OAUTH_TOKEN=.*|CLAUDE_CODE_OAUTH_TOKEN=|" \ - ~/.hermes/.env -# Restart both proxies: -launchctl unload ~/Library/LaunchAgents/ai.claude-proxy.plist -launchctl load ~/Library/LaunchAgents/ai.claude-proxy.plist -launchctl unload ~/Library/LaunchAgents/com.openclaw.claude-proxy.plist -launchctl load ~/Library/LaunchAgents/com.openclaw.claude-proxy.plist -``` - -**Long-term:** a token-refresh cron/daemon that keeps `~/.hermes/.env` updated -is needed. Not yet implemented. - ---- - -## Wrapper script - -`~/.local/bin/claude-python-proxy-start.sh`: -```bash -#!/bin/zsh -set -a -source /Users/admin/.hermes/.env 2>/dev/null -set +a -export PATH="/Users/admin/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" -cd /Users/admin/openclaw/claude-proxy -exec .venv/bin/python -m uvicorn src.main:app --host 127.0.0.1 --port 8090 -``` - -## LaunchAgent - -`~/Library/LaunchAgents/com.openclaw.claude-proxy.plist`: -```xml -ProgramArguments - - /bin/zsh - /Users/admin/.local/bin/claude-python-proxy-start.sh - -WorkingDirectory -/Users/admin/openclaw/claude-proxy -StandardOutPath/tmp/claude-proxy.log -StandardErrorPath/tmp/claude-proxy.err -``` - -## Pitfall summary - -| Pitfall | Symptom | Fix | -|---------|---------|-----| -| Token expired in `.env` | 401 from both proxies, terminal claude works | Copy fresh token from terminal to `~/.hermes/.env`, restart proxies | -| Venv Python version mismatch | `pip` crashes, `ImportError` on `.so` | Rebuild with `uv venv --clear`, reinstall deps | -| `cli_path` set to system claude | SDK exits code 1, no response | Set `SYSTEM_CLAUDE_PATH = None` in `src/claude_cli.py` | -| LaunchAgent has no OAuth token | "Not logged in" | Wrapper script must `source ~/.hermes/.env` | -| SDK version too old | CLI protocol mismatch, exit code 1 | `uv pip install claude-agent-sdk` to update | -| Node.js proxy stream-json | Streaming hangs after role chunk | Use Python proxy on 8090 instead | - -## Health check - -```bash -curl http://localhost:8090/health -curl http://localhost:8090/v1/auth/status -# Test streaming: -curl -s -X POST http://localhost:8090/v1/chat/completions \ - -H 'Content-Type: application/json' \ - -d '{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"hi"}],"stream":true}' -``` - ---- - -## Related - -- [[hermes-eagle-mac]] — full Eagle setup including Zulip, MCP, launchd -- [[hermes-deployment-patterns]] — comparison of Eagle/Kraken deployment models diff --git a/personal/projects/balda.md b/personal/projects/balda.md deleted file mode 100644 index 80adbf55..00000000 --- a/personal/projects/balda.md +++ /dev/null @@ -1,99 +0,0 @@ -# Balda (Валера) — AI Worker Bot - -Аналог Hermes. Go-сервис: берёт задачи из чата (Telegram или Zulip), запускает AI CLI и отвечает. - -## Репо - -- Upstream: https://github.com/normahq/balda -- Fork: https://github.com/mallexxx/balda -- Local: ~/Developer/balda -- Ветка: `feat/zulip-transport` - -## Архитектура - -- NATS JetStream — event bus (embedded) -- SQLite — состояние (owner, sessions, tasks) -- Telegram — через polling или webhook -- Zulip — через outgoing webhook bot -- Providers: codex, opencode, copilot, gemini, claude (ACP protocol) - -## Zulip Integration - -Полностью реализована. Webhook-based (не polling). -Тип бота: **Outgoing webhook** (не Generic bot). - -### Credentials (zulip.qentra.top) - -- Bot email: `balda-bot@zulip.qentra.top` -- API key: `AtC9FypdIl6F0hNICW0lcOzehrVrWhPr` -- Server URL: `https://zulip.qentra.top` -- Webhook token: берётся из Settings → Bots → balda-bot → Edit → Token - -### Config (env vars) - -``` -BALDA_ZULIP_BOT_EMAIL=balda-bot@zulip.qentra.top -BALDA_ZULIP_API_KEY=AtC9FypdIl6F0hNICW0lcOzehrVrWhPr -BALDA_ZULIP_SERVER_URL=https://zulip.qentra.top -BALDA_ZULIP_WEBHOOK_TOKEN=<из настроек бота> -BALDA_ZULIP_WEBHOOK_ENABLED=true -``` - -### Webhook setup в Zulip - -1. Settings → Bots → balda-bot → Edit -2. Bot type: Outgoing webhook -3. Endpoint URL: `http://:8090/zulip/webhook` -4. Скопировать token → `BALDA_ZULIP_WEBHOOK_TOKEN` - -### Авторизация - -После запуска написать боту в DM: -``` -/start owner= -``` - -## Реализованные команды (Telegram parity) - -| Команда | Статус | -|---------|--------| -| `/start owner=` | ✅ | -| `/start invite=` | ✅ | -| `/goal ` / `/goal clear` | ✅ | -| `/topic` | ✅ (создаёт сессию для нового топика) | -| `/user add ` | ✅ | -| `/user list` | ✅ (с инвайтами + expiry) | -| Typing indicator | ✅ (isDM-aware: true в DM, false в стримах) | -| plan_updates / SendDraftPlain | ➖ no-op (нет аналога в Zulip) | - -## Upcoming: mention-based thread takeover - -Когда `@balda-bot` упоминается в топике без активной сессии — бот автоматически берёт топик под контроль. - -Механизм (аналог whale-thread-guard в Hermes): -- Webhook payload поле `trigger == "mention"` → auto-create session -- `allowed_owners` список в конфиге: Zulip email без токена -- После захвата топик отвечает на все сообщения (обычная сессия) - -## Конфиг и инструкции - -- [`balda/soul.md`](balda/soul.md) — soul (global_instruction) — редактировать здесь, синхронизировать в config.yaml -- [`balda/workspace-context.md`](balda/workspace-context.md) — per-provider system_instructions (рабочий проект/стек) -- `~/Developer/balda/.config/balda/config.yaml` — основной конфиг (runtime + balda) -- `~/Developer/balda/.env` — секреты (Zulip API key, webhook token) - -### Запуск - -```bash -cd ~/Developer/balda && ~/.local/bin/balda start -``` - -После первого запуска: из логов взять `owner_token` и отправить боту в Zulip DM: -``` -/start owner= -``` - -## Docs - -- `docs/zulip-webhook.md` — полное руководство по настройке -- `docs/balda.md` — техническая спецификация diff --git a/personal/projects/eagle-dashboard.md b/personal/projects/eagle-dashboard.md deleted file mode 100644 index 8c9cb8c3..00000000 --- a/personal/projects/eagle-dashboard.md +++ /dev/null @@ -1,166 +0,0 @@ -# Eagle Dashboard - -Process supervisor and control panel for all Eagle local services. - -**URL:** `http://localhost:8880` (internal) · `https://dashboard.qentra.top` (external, token-required) -**Source:** `~/Developer/eagle-dash/` -**LaunchAgent:** `com.eagle.dashboard` (single Login Item, KeepAlive=true) - ---- - -## Architecture - -FastAPI + HTMX + Tailwind CDN + Alpine.js. No build step, CDN-only frontend. - -``` -~/Developer/eagle-dash/ -├── main.py # FastAPI app, lifespan, SSE -├── auth.py # Origin-aware auth: 127.0.0.1 → free; cloudflared → token -├── supervisor.py # PID-file-based process supervisor (asyncio, setsid) -├── registry.py # Service registry + YAML loader -├── health.py # Health checks: http / port / process / launchctl -├── control.py # /restart /stop /start endpoints -├── memory.py # RSS tracking (main process + children) -├── loader.py # services.yaml loader -├── services.yaml # Service definitions (source of truth) -├── .env # EAGLE_TOKEN= (not committed) -├── templates/ -│ └── index.html # Dashboard UI — 3 tabs: Services, Pages, Files -└── com.eagle.dashboard.plist -``` - -### Auth - -- `127.0.0.1` direct access → no auth -- Requests via cloudflared (have `X-Forwarded-For`) → require `Authorization: Bearer ` - or `eagle_token` cookie (set on first login, 30-day expiry) -- Token stored in `.env` - -### Supervisor (supervisor.py) - -PID-file-based — survives Dashboard restarts without crashing. - -- Services launched with `start_new_session=True` (setsid) → run in own session -- PID written to `~/.eagle-dash/pids/.pid` immediately on launch -- On Dashboard restart: reads PID file → if process alive → adopts without re-launching -- If process dead → launches fresh -- Auto-restart with exponential backoff (2s → 60s max) - -### macOS integration - -- `pf` anchor: `localhost:80` → `127.0.0.1:8880` (setup-pf-redirect.sh) -- cloudflared tunnel: `dashboard.qentra.top` → `localhost:8880` -- FileBrowser: `http://127.0.0.1:8181` (FB_NOAUTH=true, auth delegated to Dashboard) - ---- - -## Services - -24 services in `services.yaml`, 10 with `autostart: true`. Секция **Docker** — сервисы, запускающиеся через docker-compose — отображается первой в дашборде. - -### Docker - -| ID | Name | Access | Type | -|----|------|--------|------| -| zulip | Zulip Connector | localhost | container | -| filebrowser | FileBrowser | localhost | container | - -### AI - -| ID | Name | Access | Type | -|----|------|--------|------| -| hermes | Hermes Eagle | localhost | daemon | -| hermes_whale | Hermes Whale | localhost | daemon | -| claude_proxy_node | Claude Proxy (node) | localhost | daemon | -| claude_proxy_python | Claude Proxy (python) | localhost | daemon | -| asana_mcp | Asana MCP | localhost | daemon | -| virfield | Virfield | localhost | daemon | - -### ML - -| ID | Name | Access | Type | -|----|------|--------|------| -| mlx_lm | MLX LM Server | lan (0.0.0.0:8080) | on-demand | -| ollama | Ollama | lan (0.0.0.0:11434) | on-demand | - -### Reflect - -| ID | Name | Access | Type | -|----|------|--------|------| -| reflect_proxy | Reflect Proxy | lan | daemon | -| reflect_fdroid | Reflect F-Droid Server | lan | daemon | - -### Monitoring - -| ID | Name | Access | Type | -|----|------|--------|------| -| aw_ddg | AW Watcher DDG | localhost | daemon | -| aw_xcode | AW Watcher Xcode | localhost | daemon | - -### Orchestration - -| ID | Name | Access | Type | -|----|------|--------|------| -| wiki_ingest | Wiki Ingest | localhost | cron | - -**Remaining LaunchAgents (not under Dashboard):** -- `com.colima.start` — Homebrew managed -- `homebrew.mxcl.postgresql@17` — Homebrew managed -- `personal.os.wiki-ingest` — cron schedule -- `com.personalos.heartbeat` — cron schedule - ---- - -## Network Access Badges - -- 🟢 `localhost` — bound to 127.0.0.1, safe -- 🟡 `lan` — bound to 0.0.0.0, accessible on home network (mlx_lm, ollama) -- 🔵 `tunnel` — cloudflared-managed, token-protected - ---- - -## Tabs - -**Services** — health cards with: status badge, PID, uptime, memory (RSS + children), start/stop/restart controls, log tail drawer (last 200 lines). - -**Pages** — URL input + iframe for quick local page testing. - -**Files** — FileBrowser iframe at `http://localhost:8181`. - ---- - -## Deployment - -```bash -cd ~/Developer/eagle-dash - -# Install deps -uv sync - -# Generate token -python3 -c "import secrets; print('EAGLE_TOKEN='+secrets.token_hex(32))" > .env - -# Setup pf redirect (localhost:80 → :8880) -sudo bash setup-pf-redirect.sh - -# Load LaunchAgent -launchctl load ~/Library/LaunchAgents/com.eagle.dashboard.plist -``` - ---- - -## Troubleshooting - -**Services show `running: null`** — check `supervisor.supervisor_status` field in `/api/services` (not top-level `running`). The supervisor status is in the nested `supervisor` object. - -**PID files stale** — `ls ~/.eagle-dash/pids/` and compare with `ps`. Supervisor adopts automatically on next poll. - -**Ghost launchctl entries** — after removing plists, use `launchctl remove