himalaya

---
name: himalaya
description: "Himalaya CLI: IMAP/SMTP email from terminal."
version: 1.1.0
author: community
license: MIT
metadata:
  hermes:
    tags: [Email, IMAP, SMTP, CLI, Communication]
    homepage: https://github.com/pimalaya/himalaya
prerequisites:
  commands: [himalaya]
---

# Himalaya Email CLI

Himalaya is a CLI email client that lets you manage emails from the terminal using IMAP, SMTP, Notmuch, or Sendmail backends.

## References

- `references/configuration.md` (config file setup + IMAP/SMTP authentication)
- `references/message-composition.md` (MML syntax for composing emails)

## Prerequisites

1. Himalaya CLI installed (`himalaya --version` to verify)
2. A configuration file at `~/.config/himalaya/config.toml`
3. IMAP/SMTP credentials configured (password stored securely)

### Non-standard config paths

On systems where `$HOME/.config` doesn't exist or is read-only (ZimaOS, containers), use `-c` to point directly at the config:

```bash
himalaya -c /path/to/config.toml envelope list
```

The `-c` flag also accepts colon-delimited paths for merging multiple config files (e.g., public + private).

On this ZimaOS instance: binary at `/DATA/AppData/hermes/bin/himalaya`, config at `/DATA/AppData/hermes/himalaya.toml`.

### GMX.de folder names (German)

GMX uses German folder names — they do NOT match himalaya's English canonical names. You MUST set folder aliases to match the server:

```toml
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Gesendet"    # NOT "Sent"
folder.aliases.drafts = "Entwürfe"  # NOT "Drafts"
folder.aliases.trash = "Gelöscht"   # NOT "Trash"
```

Always run `himalaya folder list` first to discover the real folder names before setting aliases. A wrong alias causes `--folder "Sent"` to return empty results silently because the real folder is `Gesendet`.

**Critical pitfall with `template send`**: When sending with MML attachments (`<#part filename=...>`), himalaya copies the sent message to the Sent folder after SMTP delivery. If the `folder.aliases.sent` alias points to a non-existent folder (e.g., English `"Sent"` instead of GMX German `"Gesendet"`), himalaya fails with `"cannot add IMAP message / unexpected NO response: folder does not exist"` — **after** SMTP delivery succeeds. The email is sent but himalaya exits non-zero. Do NOT retry on this error without fixing the alias first, or you'll send duplicates.

### Installation

```bash
# Pre-built binary (Linux/macOS — recommended)
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh

# macOS via Homebrew
brew install himalaya

# Or via cargo (any platform with Rust)
cargo install himalaya --locked
```

## Configuration Setup

Run the interactive wizard to set up an account:

```bash
himalaya account configure
```

Or create `~/.config/himalaya/config.toml` manually:

```toml
[accounts.personal]
email = "you@example.com"
display-name = "Your Name"
default = true

backend.type = "imap"
backend.host = "imap.example.com"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "you@example.com"
backend.auth.type = "password"
backend.auth.cmd = "pass show email/imap"  # or use keyring

message.send.backend.type = "smtp"
message.send.backend.host = "smtp.example.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "you@example.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "pass show email/smtp"

# Folder aliases (himalaya v1.2.0+ syntax). Required whenever the
# server's folder names don't match himalaya's canonical names
# (inbox/sent/drafts/trash). Gmail is the common case — see
# `references/configuration.md` for the `[Gmail]/Sent Mail` mapping.
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Sent"
folder.aliases.drafts = "Drafts"
folder.aliases.trash = "Trash"
```

> **Heads up on the alias syntax.** Pre-v1.2.0 docs used a
> `[accounts.NAME.folder.alias]` sub-section (singular `alias`).
> v1.2.0 silently ignores that form — TOML parses fine, but the
> alias resolver never reads it, so every lookup falls through to
> the canonical name. On Gmail this means save-to-Sent fails *after*
> SMTP delivery succeeds, and `himalaya message send` exits non-zero.
> Any caller (agent, script, user) that retries on that exit code
> will re-run the entire send — including SMTP — producing duplicate
> emails to recipients. Always use `folder.aliases.X` (plural, dotted
> keys, directly under `[accounts.NAME]`).

## Hermes Integration Notes

- **Reading, listing, searching, moving, deleting** all work directly through the terminal tool
- **Composing/replying/forwarding** — piped input (`cat << EOF | himalaya template send`) is recommended for reliability. Interactive `$EDITOR` mode works with `pty=true` + background + process tool, but requires knowing the editor and its commands
- Use `--output json` for structured output that's easier to parse programmatically
- The `himalaya account configure` wizard requires interactive input — use PTY mode: `terminal(command="himalaya account configure", pty=true)`

## Common Operations

### List Folders

```bash
himalaya folder list
```

### List Emails

List emails in INBOX (default):

```bash
himalaya envelope list
```

List emails in a specific folder:

```bash
himalaya envelope list --folder "Sent"
```

List with pagination:

```bash
himalaya envelope list --page 1 --page-size 20
```

### Search Emails

```bash
himalaya envelope list from john@example.com subject meeting
```

### Read an Email

Read email by ID (shows plain text):

```bash
himalaya message read 42
```

Export raw MIME:

```bash
himalaya message export 42 --full
```

### Reply to an Email

To reply non-interactively from Hermes, read the original message, compose a reply, and pipe it:

```bash
# Get the reply template, edit it, and send
himalaya template reply 42 | sed 's/^$/\nYour reply text here\n/' | himalaya template send
```

Or build the reply manually:

```bash
cat << 'EOF' | himalaya template send
From: you@example.com
To: sender@example.com
Subject: Re: Original Subject
In-Reply-To: <original-message-id>

Your reply here.
EOF
```

Reply-all (interactive — needs $EDITOR, use template approach above instead):

```bash
himalaya message reply 42 --all
```

### Forward an Email

```bash
# Get forward template and pipe with modifications
himalaya template forward 42 | sed 's/^To:.*/To: newrecipient@example.com/' | himalaya template send
```

### Write a New Email

**Non-interactive (use this from Hermes)** — pipe the message via stdin:

```bash
cat << 'EOF' | himalaya template send
From: you@example.com
To: recipient@example.com
Subject: Test Message

Hello from Himalaya!
EOF
```

Or with headers flag:

```bash
himalaya message write -H "To:recipient@example.com" -H "Subject:Test" "Message body here"
```

Note: `himalaya message write` without piped input opens `$EDITOR`. This works with `pty=true` + background mode, but piping is simpler and more reliable.

### Move/Copy Emails

Move to folder:

```bash
himalaya message move 42 "Archive"
```

Copy to folder:

```bash
himalaya message copy 42 "Important"
```

### Delete an Email

```bash
himalaya message delete 42
```

### Manage Flags

Add flag:

```bash
himalaya flag add 42 --flag seen
```

Remove flag:

```bash
himalaya flag remove 42 --flag seen
```

## Multiple Accounts

List accounts:

```bash
himalaya account list
```

Use a specific account:

```bash
himalaya --account work envelope list
```

## Attachments

Save attachments from a message:

```bash
himalaya attachment download 42
```

Save to specific directory:

```bash
himalaya attachment download 42 --dir ~/Downloads
```

## Output Formats

Most commands support `--output` for structured output:

```bash
himalaya envelope list --output json
himalaya envelope list --output plain
```

## Debugging

Enable debug logging:

```bash
RUST_LOG=debug himalaya envelope list
```

Full trace with backtrace:

```bash
RUST_LOG=trace RUST_BACKTRACE=1 himalaya envelope list
```

## Tips

- Use `himalaya --help` or `himalaya <command> --help` for detailed usage.
- Message IDs are relative to the current folder; re-list after folder changes.
- For composing rich emails with attachments, use MML syntax (see `references/message-composition.md`).
- Store passwords securely using `pass`, system keyring, or a command that outputs the password.

To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'


zimaos-camofox-browser

---
name: zimaos-camofox-browser
description: "Camofox-Browser als persistenten Docker-Dienst auf ZimaOS für Hermes-Browser-Automation einrichten und betreiben."
version: 1.0.0
author: Kiwi (Ahmed Zeyd Aytac)
tags: [zimaos, camofox, browser, docker, compose, hermes, anti-detection, persistence]
---

# ZimaOS Camofox Browser

Ziel: Auf dem ZimaOS-Server soll Browser-Automation für Hermes **immer** über einen lokalen Camofox-Browser-Container laufen — nicht über Webtop, nicht primär über linuxserver/chromium, nicht primär über Kasm und nicht primär über Browserless.

## Architekturstandard

1. **Camofox local mode** als Docker/Compose-Dienst auf ZimaOS bevorzugen.
2. Hermes verwendet seine nativen Browser-Tools über `CAMOFOX_URL`.
3. Hermes direkt auf ZimaOS-Host → `CAMOFOX_URL=http://127.0.0.1:9377`.
4. Hermes selbst in Docker → gemeinsames Docker-Netzwerk und `CAMOFOX_URL=http://camofox-browser:9377`.
5. **Persistence aktivieren**, damit Cookies, Logins, LocalStorage und Sessions über Neustarts erhalten bleiben.
6. noVNC nur zur Beobachtung oder manuellen MFA/CAPTCHA/Login-Hilfe; noVNC ist **nicht** die primäre Agentensteuerung.
7. Camofox-API nicht öffentlich ins WAN exponieren. Port 9377 an localhost oder nur an internes Docker-Netzwerk binden.
8. **Keinen `CAMOFOX_API_KEY` setzen**, solange Hermes' Camofox-Integration bei gesetztem Key 403-Probleme haben kann. Sicherheit stattdessen über Bind-Adresse, Firewall, Docker-Netzwerk, VPN oder SSH-Tunnel.
9. Für ZimaOS persistente Pfade unter `/DATA/AppData/camofox` verwenden. Keine fragilen Container ohne Volume.
10. Sessions nach Aufgaben: erst `browser.camofox.managed_persistence: true` prüfen, optional extern gemanagte Sessions mit `CAMOFOX_USER_ID`, `CAMOFOX_SESSION_KEY` und `CAMOFOX_ADOPT_EXISTING_TAB=true`.

## Referenz-Compose für ZimaOS

```yaml
services:
  camofox-browser:
    image: camofox-browser:local
    container_name: camofox-browser
    restart: unless-stopped
    ports:
      - "127.0.0.1:9377:9377"
      - "127.0.0.1:6080:6080"
    environment:
      CAMOFOX_PORT: "9377"
      ENABLE_VNC: "1"
      VNC_BIND: "0.0.0.0"
      VNC_RESOLUTION: "1920x1080"
      MAX_OLD_SPACE_SIZE: "2048"
    volumes:
      - /DATA/AppData/camofox:/root/.camofox
```

## Hermes-Konfiguration

### 1. `.env` — CAMOFOX_URL setzen

```bash
# In ~/.hermes/.env:
CAMOFOX_URL=http://127.0.0.1:9377
```

Bei Hermes-in-Docker stattdessen:
```bash
CAMOFOX_URL=http://camofox-browser:9377
```

### 2. `config.yaml` — managed persistence

```yaml
browser:
  camofox:
    managed_persistence: true
```

### 3. Browser-Toolset aktivieren

```bash
hermes tools enable browser
```

### 4. Hermes vollständig neu starten

Nach allen Änderungen muss Hermes komplett neu gestartet werden (Gateway-Restart oder CLI-Neustart).

## Verifikation

### Schritt 1: Camofox-Erreichbarkeit prüfen

```bash
curl -fsS http://127.0.0.1:9377/health
# oder einen anderen API-Endpunkt des Camofox-Servers
```

### Schritt 2: CAMOFOX_URL in .env prüfen

```bash
grep CAMOFOX_URL ~/.hermes/.env
```

### Schritt 3: Browser-Aufgabe + noVNC-Beobachtung

```bash
# noVNC über SSH-Tunnel:
ssh -L 6080:127.0.0.1:6080 user@zimaos
# Dann im Browser: http://localhost:6080
```

Starte eine Browser-Aufgabe in Hermes und beobachte über noVNC.

### Schritt 4: Persistenz-Test

1. Logge dich manuell auf einer Website ein.
2. Beende die Browser-Aufgabe.
3. Starte eine neue Browser-Aufgabe.
4. Prüfe, ob der Login erhalten bleibt.

### Schritt 5: Docker-Netzwerk-Fallback

Wenn Hermes in Docker läuft und `127.0.0.1` nicht funktioniert:
```bash
CAMOFOX_URL=http://camofox-browser:9377
```
im gemeinsamen Docker-Netzwerk verwenden.

## Build-Prozess (ZimaOS)

Da `jo-inc/camofox-browser` kein öffentliches Docker-Image hat, muss **lokal gebaut** werden.
ZimaOS hat kein `make` — die Schritte sind manuell:

### 1. Repo clonen + Assets downloaden

```bash
cd /tmp
git clone https://github.com/jo-inc/camofox-browser.git
cd camofox-browser
mkdir -p dist
curl -fSL "https://github.com/daijro/camoufox/releases/download/v135.0.1-beta.24/camoufox-135.0.1-beta.24-lin.x86_64.zip" -o dist/camoufox-x86_64.zip
curl -fSL "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux" -o dist/yt-dlp-x86_64
```

### 2. Image bauen (mit VNC-Support)

Der Standard-Dockerfile hat **kein** `x11vnc`, `noVNC` oder `websockify`.
Ein angepasster Dockerfile wird benötigt. Siehe `references/build-dockerfile.md` für das vollständige Dockerfile.

```bash
DOCKER_BUILDKIT=1 docker build --no-cache \
  --build-arg ARCH=x86_64 \
  --build-arg CAMOUFOX_VERSION=135.0.1 \
  --build-arg CAMOUFOX_RELEASE=beta.24 \
  -t camofox-browser:135.0.1-x86_64 \
  -f /path/to/custom.Dockerfile .
```

### 3. VNC-Config für Camofox

VNC ist im Standard-Image **deaktiviert**. Config mit aktiviertem VNC als Volume mounten:

```json
{
  "id": "camofox-browser",
  "name": "Camofox Browser",
  "version": "1.6.0",
  "plugins": {
    "youtube": { "enabled": true },
    "persistence": { "enabled": true },
    "vnc": { "enabled": true, "resolution": "1920x1080" }
  }
}
```

Die Datei ins Compose-File einbinden (siehe Referenz-Compose).

### 4. Websockify nachinstallieren (im laufenden Container)

`websockify` fehlt im Python-Path des Containers und muss nachinstalliert werden:

```bash
docker exec camofox-browser pip3 install --break-system-packages websockify
# Oder im Dockerfile ergänzen: RUN pip3 install --break-system-packages websockify
```

### Compose-File starten

```bash
docker --config /tmp/docker-config compose -f /DATA/AppData/camofox-compose/docker-compose.yml up -d
```

**Wichtig:** Auf ZimaOS immer `DOCKER_CONFIG=/tmp/docker-config` setzen — `/root/.docker` ist read-only.

## Web-Research Patterns (new in this session)

When doing ad-hoc web research through Camofox, use this ordered fallback chain:

1. **Search with DuckDuckGo first.** Google frequently blocks Camofox/jina.ai with 429/CAPTCHA. DuckDuckGo (`https://duckduckgo.com/?q=...`) is far more tolerant of automated text extraction and usually returns enough link text via the `/tabs/:tabId/links` endpoint.
2. **Extract article text with r.jina.ai.** `curl -sL "https://r.jina.ai/<URL>" -H "Accept: text/plain"` renders JS and returns clean markdown. Many news sites and blogs work; some block it (403). Use it before falling back to screenshots.
3. **Use Camofox only when the above is insufficient.** Good for: JavaScript-heavy sites, pages that block jina.ai, consent popups, or when you need a screenshot for visual confirmation.
4. **Avoid Fandom, Reddit, and X for anonymous extraction.** All three actively block anonymous/automated access with Cloudflare, JS challenges, or login walls. Use them only as a last resort and expect failures.

### Content Extraction from SPAs (JS-heavy sites)

Many modern sites are React/SPA — `curl` gets empty `<div id="root">`. This includes Austrian government sites: **RIS** (Rechtsinformationssystem), **Findok** (BMF Steuerdokumente), WU Wien, ÖH WU. For legal research on RIS, see `references/ris-legal-research.md` for the proven workflow.

General extraction options for SPAs:

1. **links endpoint** (fast, no JS needed): `GET /tabs/:tabId/links?userId=kiwi` — returns link texts, but hrefs may be empty (JS-routed).
2. **screenshot + vision** (reliable): Take screenshot, `vision_analyze` it for visual content.
3. **jina.ai reader** (best for text extraction): `curl -sL "https://r.jina.ai/<URL>" -H "Accept: text/plain"` — renders JS and returns clean markdown. Use when browser-extracted content is insufficient.
4. **Hidden JSON APIs** (fastest, no browser needed): Many JS SPAs have backend API endpoints. Look for them in page source (`grep -oP 'api|endpoint|json|type=\d+'`), or try common patterns like `?type=INTEGER` on the page URL. Example: `ihr-notariat.at/notarfinder/?type=1602231006` returned the full Austrian notary database as JSON — avoiding the entire SPA. Always try this before launching browser automation for content extraction.
5. **evaluate endpoint**: `POST /tabs/:tabId/evaluate` — requires `CAMOFOX_API_KEY` unless loopback. For localhost calls without API key, prefer links + screenshot.

### Cookie / Consent Popups

Many EU sites (WU, ÖH) show cookie banners that overlay content. Dismiss via:
- `POST /tabs/:tabId/click` with selector `"button:has-text('Alle akzeptieren')"` or similar
- If clicking times out (popup blocks interaction), try waiting 2-3s first, then click
- Take screenshot after dismissal to verify

### Auth / sessionKey Convention

- **POST endpoints**: `userId` and `sessionKey` go in the **JSON body**, NOT as query params. Query params are ignored.
  ```bash
  # ✅ CORRECT
  curl -X POST "http://127.0.0.1:9377/tabs" \
    -d '{"userId":"kiwi","sessionKey":"default","url":"https://example.com"}'

  # ❌ WRONG — query params ignored
  curl -X POST "http://127.0.0.1:9377/tabs?userId=kiwi&sessionKey=default" \
    -d '{"url":"https://example.com"}'
  ```
- **GET endpoints** (screenshot, links): use **query params**.
  ```bash
  curl "http://127.0.0.1:9377/tabs/$TAB/screenshot?userId=kiwi&sessionKey=default"
  ```
- The `sessionKey` value is arbitrary — `"default"`, `"tuya"`, etc. all work as long as it's present.

### API Endpoint Quirks

| Endpoint | Method | Notes |
|----------|--------|-------|
| `/tabs` | POST | Create tab — `{userId, sessionKey, url}` in body → returns `tabId` |
| `/tabs?userId=kiwi` | GET | List all tabs for user — works |
| `/tabs/:tabId?userId=kiwi` | GET | May return 404 with current version — use listing instead |
| `/tabs/:tabId/links?userId=kiwi&sessionKey=X` | GET | Link extraction — JS-routed sites have empty hrefs (React SPA). Returns `{links, pagination}`. Buttons/submit inputs inside forms are NOT listed (they're not `<a>` tags) |
| `/tabs/:tabId/screenshot?userId=kiwi&sessionKey=X` | GET | Screenshot — works, reliable. Returns PNG via stdout |
| `/tabs/:tabId/evaluate` | POST | **BLOCKED without CAMOFOX_API_KEY** even on loopback. Returns `{"error":"This endpoint requires CAMOFOX_API_KEY except for loopback requests in non-production environments."}`. Workaround: use `/type` for form filling + `/click` for interaction |
| `/tabs/:tabId/type` | POST | Type text into an input field. Body: `{userId, sessionKey, selector, text}`. Returns `{"ok":true}`. Works for both text and password fields. Chain `/type` + `/click` as a substitute for `/evaluate` when API key is absent |
| `/tabs/:tabId/click` | POST | CSS selector click. Body: `{userId, sessionKey, selector}`. May return `{"error":"Internal server error"}` on submit buttons inside forms — `/type` + form interaction may be needed instead |
| `/tabs/:tabId/navigate` | POST | Set URL. Body: `{userId, sessionKey, url}`. **Cannot navigate to `javascript:` URLs** — returns "Bad Request". Only works with `http(s)://` URLs |
| `/health` | GET | Health check — `browserConnected` goes true lazily on first tab |

## Troubleshooting

- **Google returns 429/CAPTCHA during research:** Switch to DuckDuckGo or use r.jina.ai on the target URL directly. Do not keep retrying Google from the same IP.
- **Camofox nicht erreichbar:** `docker logs camofox-browser` prüfen.
- **403 bei gesetztem CAMOFOX_API_KEY:** Key entfernen, Sicherheit über Netzwerk-Binding sicherstellen.
- **Cookies/Logins nicht persistent:** Prüfen ob `/DATA/AppData/camofox` gemountet ist und `managed_persistence: true` in config.yaml steht.
- **noVNC nicht erreichbar:** Port 6080 nur an localhost gebunden? SSH-Tunnel nutzen: `ssh -L 6080:127.0.0.1:6080 user@zimaos`
- **`/root/.docker` read-only:** Auf ZimaOS `DOCKER_CONFIG=/tmp/docker-config` vor allen Docker-Befehlen setzen.
- **`make: command not found`:** ZimaOS hat kein make. Build-Schritte manuell ausführen (curl für Assets, docker build direkt).
- **`websockify: not found`:** Im Container `pip3 install --break-system-packages websockify` ausführen, dann `websockify --web /usr/share/novnc 0.0.0.0:6080 127.0.0.1:5900` starten.
- **`browserConnected: false` in Health:** Normal — Camoufox startet lazily erst beim ersten Tab-Request. Einen Tab öffnen, dann prüft Health auf `true`.
- **VNC-Plugin deaktiviert:** In `camofox.config.json` muss `"vnc": { "enabled": true, "resolution": "1920x1080" }` stehen. Per Volume-Mount überschreiben.


Ich lade alle verlinkten Camofox-Referenzen und durchsuche E-Mail + lokale Dateien.

Explanation:
<think>Ich habe die Camofox-Skill-Doku geladen, die sehr detailliert ist. Der User sagt "Lies alle smit Camofox aus bitte" - das ist ein Typo, wahrscheinlich meint er "alle E-Mails" oder "alle Sachen" mit Camofox. Er will wohl alle relevanten Dokumente/Referenzen zu Camofox.

Ich sollte:
1. Die linked_files der Camofox-Skill lesen
2. Eventuell E-Mails nach Camofox durchsuchen via Himalaya
3. Lokale Dateien im /DATA/AppData/camofox Verzeichnis prüfen
4. Zusammenfassung geben

Lass mich die linked files lesen und das Camofox-Verzeichnis durchsuchen.

terminal

ls -la /DATA/AppData/camofox* 2>/dev/null; find /DATA/AppData -maxdepth 3 -iname '*camofox*' -type f 2>/dev/null | head -50


zimaos-camofox-browser

# Camofox Research Patterns

Condensed recipes for common web-research tasks using the local Camofox container plus lightweight fallbacks.

## Pattern 1: YouTube playlist / channel extraction

Goal: find the latest episode, video list, or metadata without logging in.

1. Create a tab with the playlist URL:
   ```bash
   curl -s -X POST http://127.0.0.1:9377/tabs \
     -H "Content-Type: application/json" \
     -d '{"userId":"kiwi","sessionKey":"yt-research","url":"https://www.youtube.com/playlist?list=PLAYLIST_ID"}'
   ```
2. After 3–4 s, call the `/links` endpoint. YouTube renders playlist entries as links with index and title.
   ```bash
   curl -s "http://127.0.0.1:9377/tabs/$TAB/links?userId=kiwi&sessionKey=yt-research" | jq
   ```
3. Look for the highest `index=N` link. The title usually includes episode number and duration.
4. Navigate to the last video URL to confirm length, title, etc.
5. YouTube hides exact upload dates behind a login wall. If the date is required, check the public video page while logged in, or cross-reference social media / fandom wikis.

## Pattern 2: Product page research (Ikea, Amazon, etc.)

Goal: extract product specs, care instructions, and official claims.

1. Navigate to the product URL with Camofox.
2. Take a screenshot for visual verification.
3. Use `r.jina.ai/<URL>` to pull clean text from the rendered SPA:
   ```bash
   curl -sL "https://r.jina.ai/<URL>" -H "Accept: text/plain"
   ```
   This works even when the `/links` endpoint only returns navigation links because the page is mostly SPA-rendered.
4. Quote the official text directly in the answer and note the source.

## Pattern 3: Help-center / about-page fact extraction

Goal: answer "Where does X get its data?"-style questions authoritatively.

1. Search the target site for an about/help page (e.g. `/help/woher-stammen-die-daten`).
2. Use `r.jina.ai/<URL>` to get the full article text.
3. Cite the exact phrasing and source URL.
4. If the site has no direct help article, fall back to `/links` + screenshot analysis.

## Pattern 4: SPA fallback chain

When `curl` returns only an empty `<div id="root">`:

1. Try `r.jina.ai/<URL>` first — it often renders JS and returns markdown.
2. If jina.ai is blocked or insufficient, open the page in Camofox, wait, and use `/links`.
3. If links are too sparse, take a screenshot and analyze with `vision_analyze`.
4. Only use `/evaluate` if `CAMOFOX_API_KEY` is set and the endpoint is not loopback-only; otherwise prefer `/type` + `/click` or jina.ai.

## Pitfalls

- YouTube exact dates require login; quote only what is public.
- Some SPAs (e.g. YouTube watch page without login) return bot-check walls. Use playlists and jina.ai to bypass where possible.
- `/links` returns `<a>` tags only; buttons inside forms (submit, load more) are not listed.


FROM node:22-slim AS base

ARG CAMOUFOX_VERSION=135.0.1
ARG CAMOUFOX_RELEASE=beta.24
ARG ARCH=x86_64

RUN apt-get update && apt-get install -y \
    libgtk-3-0 libdbus-glib-1-2 libxt6 libasound2 libx11-xcb1 libxcomposite1 \
    libxcursor1 libxdamage1 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
    libxtst6 libegl1-mesa libgl1-mesa-dri libgbm1 xvfb x11vnc \
    fonts-liberation fonts-noto-color-emoji fontconfig ca-certificates curl unzip \
    python3-minimal python3-pip python3-venv \
    && rm -rf /var/lib/apt/lists/*

# noVNC web client
RUN mkdir -p /usr/share/novnc && \
    curl -fsSL https://github.com/novnc/noVNC/archive/refs/tags/v1.5.0.tar.gz | \
    tar xz -C /usr/share/novnc --strip-components=1 && \
    curl -fsSL https://github.com/novnc/websockify/archive/refs/tags/v0.12.0.tar.gz | \
    tar xz -C /usr/share/novnc/utils --strip-components=1

# websockify CLI (pip install required — the bundled version needs it as a module)
RUN pip3 install --break-system-packages websockify

RUN --mount=type=bind,source=dist,target=/dist \
    mkdir -p /root/.cache/camoufox && \
    (unzip -q /dist/camoufox-${ARCH}.zip -d /root/.cache/camoufox || true) && \
    chmod -R 755 /root/.cache/camoufox && \
    echo "{\"version\":\"${CAMOUFOX_VERSION}\",\"release\":\"${CAMOUFOX_RELEASE}\"}" > /root/.cache/camoufox/version.json && \
    test -f /root/.cache/camoufox/camoufox-bin

RUN --mount=type=bind,source=dist,target=/dist \
    install -m 755 /dist/yt-dlp-${ARCH} /usr/local/bin/yt-dlp

WORKDIR /app
COPY package.json ./
COPY scripts/ ./scripts/
RUN npm install --production
COPY server.js ./
COPY camofox.config.json ./
COPY lib/ ./lib/
COPY plugins/ ./plugins/
COPY scripts/ ./scripts/

RUN sh scripts/install-plugin-deps.sh

ENV NODE_ENV=production
ENV CAMOFOX_PORT=9377
EXPOSE 9377
CMD ["sh", "-c", "node --max-old-space-size=${MAX_OLD_SPACE_SIZE:-128} server.js"]


# RIS (Rechtsinformationssystem) Legal Research Workflow

Proven multi-tool approach for researching Austrian laws, DBA treaties, and tax regulations on JS-heavy government sites (RIS, Findok). Developed during DBA Österreich-Türkei pension research, 05.06.2026.

## The Problem

Austrian government legal databases (RIS, Findok) are JavaScript-rendered SPAs. Direct `curl` returns empty shells or HTML framework. Some RIS endpoints use WebForms-style session tokens and stateful page rendering that server-side readers can't replay — but NOT all. See "When each tool works" below for the current state as of 08.06.2026.

## When Each Tool Works (Empirical, 08.06.2026)

**jina.ai ✓ (works):**
- NormDokument pages with full query params: `?Abfrage=Bundesnormen&Gesetzesnummer=10001622&Paragraf=364c` — full statutory text extracted cleanly including metadata (Kurztitel, Kundmachungsorgan, Inkrafttretensdatum, Schlagworte)
- Justiz search results WITH Norm filter: `?Abfrage=Justiz&Suchworte=Veräußerungsverbot+364c&Norm=ABGB+§364c` — returns clean list of Rechtssätze with summaries, links, and NOR numbers
- Individual Justiz Rechtssatz/Entscheidung pages (`/Dokumente/Justiz/JJR_.../JJR_....html`) — clean markdown extraction
- RIS-Ergebnis search pages that resolve statically (without ASP.NET postback)
- DBA overview pages (NOR40110804)

**jina.ai ✗ (fails):**
- Seiten die ASP.NET postback + session state verwenden (z.B. manche "Ergebnis.wxe" Such-Ergebnisseiten) → "nicht mehr in Kraft" / 404
- Google Searches → 429 rate-limiting bei jina (nicht verwenden)

**Camofox (always works)**: Full JS rendering, handles all session state. Use when jina fails, or for initial exploration of RIS search results to discover document links/patterns. The `/links` endpoint is unreliable for Justiz search result pages (returns only navigation links with empty hrefs — SPA JS-routed).

## The Fix: Three-Tool Chain

### Tool 1: Camofox Browser (stateful rendering)
Use when jina.ai returns empty pages or 404s. The browser runs the JS, handles sessions, renders the actual document.

```bash
# Check Camofox is running
curl -fsS http://127.0.0.1:9377/health

# Open a tab with the legal document
curl -s -X POST "http://127.0.0.1:9377/tabs" \
  -H "Content-Type: application/json" \
  -d '{"userId":"kiwi","sessionKey":"default","url":"https://www.ris.bka.gv.at/..."}'

# Wait for JS rendering (5s minimum for RIS)
sleep 5

# Take screenshot
curl -s "http://127.0.0.1:9377/tabs/$TAB/screenshot?userId=kiwi&sessionKey=default" \
  -o /tmp/legal_page.png

# Extract links (for finding sub-articles, related docs)
curl -s "http://127.0.0.1:9377/tabs/$TAB/links?userId=kiwi&sessionKey=default"
```

### Tool 2: jina.ai reader (fast, try first)

```bash
curl -sL "https://r.jina.ai/<RIS_DOCUMENT_URL>" \
  -H "Accept: text/plain"
```

Best for: statutory text from NormDokument pages, Justiz Rechtssatz/Entscheidung pages, and search results that resolve without ASP.NET postback. See "When Each Tool Works" above for the current success/failure map.

### Tool 3: vision_analyze (reads screenshots)
When jina fails and browser screenshots are the only rendered content:

```
vision_analyze(image_url=/tmp/ris_page.png, question="What document titles and numbers are listed?")
```

**Critical pitfall:** vision_analyze can hallucinate legal text. It's good for:
- Extracting structured elements (table columns, document numbers, dates)
- Reading short labels and navigation items
- Identifying page status (e.g., "nicht mehr in Kraft")

It's NOT reliable for:
- Long paragraphs of legal prose (will paraphrase/hallucinate)
- Precise statutory language (invents plausible-sounding but wrong text)

## RIS Specific Patterns

### Finding a DBA (Double Taxation Treaty)

1. Search RIS: `https://www.ris.bka.gv.at/Ergebnis.wxe?Abfrage=Bundesnormen&Suchworte=T%C3%BCrkei+Doppelbesteuerung&...`
2. Open in Camofox, screenshot the results table
3. Extract document links from `/links` endpoint — each article row links to `.../eli/bgbl/iii/2009/96/A18/NOR40110822`
4. The NOR number increments: Art 1 = NOR40110805, Art 18 = NOR40110822, Art 23 = NOR40110826
5. Load individual article pages via jina.ai: `https://r.jina.ai/https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40110822/NOR40110822.html`

### Finding OGH Judikatur (Case Law) to a specific statute

**Goal:** Find all OGH Rechtssätze citing a specific ABGB paragraph (or any statute) and drill into individual decisions.

**Step 1 — jina.ai on Justiz search with Norm filter** (works reliably):
```bash
curl -sL "https://r.jina.ai/https://www.ris.bka.gv.at/Ergebnis.wxe?Abfrage=Justiz&Suchworte=Ver%C3%A4u%C3%9Ferungsverbot+364c&Norm=ABGB+%C2%A7364c" \
  -H "Accept: text/plain"
```
Key parameters:
- `Abfrage=Justiz` — the Justiz (courts) database
- `Suchworte=<URL-encoded keywords>` — simple, broad keywords work best; overly complex queries return empty
- `Norm=ABGB+§364c` — the Norm filter is powerful, restricts results to decisions citing that statute
- Returns: clean list of numbered Rechtssätze with summaries, GZ numbers, dates, and Document links

**Step 2 — jina.ai on individual decisions** (works reliably):
```bash
curl -sL "https://r.jina.ai/https://www.ris.bka.gv.at/Dokumente/Justiz/JJR_19961008_OGH0002_0050OB02243_96F0000_001/JJR_19961008_OGH0002_0050OB02243_96F0000_001.html" \
  -H "Accept: text/plain"
```
Returns the Rechtssatz text plus any Beisatz (annotations) and list of citing decisions.

**Step 3 — Camofox fallback** (when jina fails):
If the search results page uses ASP.NET postback and jina returns empty/404, open the search URL in Camofox:
```bash
curl -s -X POST "http://127.0.0.1:9377/tabs" \
  -H "Content-Type: application/json" \
  -d '{"userId":"kiwi","sessionKey":"default","url":"https://www.ris.bka.gv.at/Ergebnis.wxe?Abfrage=Justiz&Suchworte=Ver%C3%A4u%C3%9Ferungsverbot+Miteigentumsanteil"}'
sleep 6
curl -s "http://127.0.0.1:9377/tabs/$TAB/screenshot?userId=kiwi&sessionKey=default" -o /tmp/ogh_search.png
```
Note: The Camofox `/links` endpoint returns only navigation links (with empty hrefs) on Justiz search result pages — use screenshot + vision_analyze for structure, then jina.ai on individual decision URLs.

**Step 4 — RIS Justiz URL pattern for Norm-filtered search:**
```
https://www.ris.bka.gv.at/Ergebnis.wxe?Abfrage=Justiz&Suchworte=<keywords>&Norm=<statute>&ImRisSeit=Undefined
```
Example for § 364c ABGB:
```
?Abfrage=Justiz&Suchworte=Veräußerungsverbot+364c&Norm=ABGB+§364c
```
The `Norm` parameter is the secret weapon — it filters OGH decisions by the specific statute they cite, dramatically improving result relevance over keyword-only searches.

### Finding EStG Paragraphs

RIS URL pattern for EStG 1988 (Gesetzesnummer 10004570):
```
https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10004570&FassungVom=05.06.2026&Artikel=&Paragraf=2&Anlage=
```

### Findok (BMF Tax Rulings)

URL pattern: `https://findok.bmf.gv.at/findok/search/show?q=<searchterm>`
- Uses JavaScript search — direct curl/jina rarely works
- For EStR 2000 (Einkommensteuerrichtlinien): use the "Richtlinien" navigation link in Camofox

## Worked Example: DBA Austria-Turkey Art 18 (Pensions)

1. **jina.ai on RIS search results** → 404 (ASP.NET session state)
2. **Camofox to RIS search** → Screenshot showed 10 results: "Doppelbesteuerung – Einkommensteuer (Türkei)" BGBl III 96/2009
3. **links endpoint** → Extracted NOR pattern: A1=NOR40110805
4. **jina.ai on individual articles** → Clean markdown for each article:
   - Art 18 (NOR40110822): "Ruhegehälter ... nur in dem Staat besteuert werden, in dem der Empfänger ansässig ist"
   - Art 19 (NOR40110823): "ÖFFENTLICHER DIENST" with citizenship exceptions
   - Art 23 (NOR40110826): Method article — lit. a Befreiungsmethode, lit. d Progressionsvorbehalt
5. **Camofox for EStG § 25, FinStrG § 29** → Screenshots for document status verification

## Lessons Learned

- **Don't fight RIS with curl alone.** The session token model defeats stateless HTTP. Accept that browser automation is the primary path for RIS.
- **jina.ai is the first resort, not the only resort.** Try it, but when it 404s on an obviously valid page, switch to Camofox immediately.
- **vision_analyze is the last resort for text.** Use it for structure/scaffolding, then use jina.ai on discovered URLs for the actual text.
- **NOR number patterns let you skip pagination.** Once you find the base NOR for Art 1, you can probe Art 18 by adding 17 to the number.
- **For § 364c ABGB domain knowledge (Belastungs- und Veräußerungsverbot)** — key OGH decisions, Familienkreis, Teilbarkeit, letztwilliges BuV, Alternativen — see `references/abgb-364c-veraeusserungsverbot.md`.


# Multi-Source Web Research via Camofox

Pattern for researching a topic comprehensively across multiple sites simultaneously — used for gift ideas, product comparisons, travel planning, etc.

## Workflow

### Phase 1: Parallel Search Tabs
Open 4–6 Google search tabs simultaneously with different query angles (different keywords, languages, facets):

```bash
for name_url in "query1|url1" "query2|url2"; do
  name="${name_url%|*}"
  url="${name_url#*|}"
  curl -sS -X POST http://127.0.0.1:9377/tabs \
    -H "Content-Type: application/json" \
    -d "{\"userId\":\"kiwi\",\"sessionKey\":\"default\",\"url\":\"$url\"}"
done
```

Save tab IDs for later use in a temp file (`/tmp/hermes_gifts/tabs.json`).

### Phase 2: Dismiss Cookie Banners
Google and many EU sites show cookie consent overlays. Dismiss with:

```bash
curl -sS -X POST "http://127.0.0.1:9377/tabs/$TAB/click" \
  -H "Content-Type: application/json" \
  -d '{"userId":"kiwi","sessionKey":"default","selector":"button:has-text(\"Alle akzeptieren\")"}'
```

Wait 2–3 seconds after clicking, then screenshot.

### Phase 3: Extract Search Results
- **Google results pages**: Screenshot + vision_analyze (links endpoint only returns ads/shopping, not organic results)
- **Known-good content sites**: Use Jina.ai Reader directly (faster, cleaner markdown, no browser needed)
- **Shopping-heavy queries**: Google increasingly shows ads over organic results. If vision shows mainly shopping ads, switch to Jina on known gift-guide sites instead.

### Phase 4: Jina.ai for Deep Content
For known content sites (The Knot, Vogue, Geschenkidee.de, etc.), skip the browser entirely:

```bash
curl -sL --max-time 20 "https://r.jina.ai/https://target-site.com/article" \
  -H "Accept: text/plain" -o /tmp/content.txt
```

### Phase 5: Aggregate & Curate
Read Jina outputs with read_file, cross-reference ideas across sources, produce curated list.

## Pitfalls

- **Jina 404/empty responses**: Some sites return 404 even when valid. Try alternate URLs or fall back to browser+screenshot.
- **Google ad dominance**: "Gesponsert" (sponsored) results drown out organic. Try niche queries or skip Google for known content sites.
- **Paywalls/blockers**: Brides.com, Vogue, and some other sites may return "Just a moment..." (Cloudflare challenge) via Jina. Skip those, try alternatives.
- **Parallel tabs timeout**: Opening too many tabs at once can be slow. 5–6 is the sweet spot.
- **Tab ID tracking**: Always save tab IDs. Camofox doesn't have named tabs, only UUIDs.

## Proven Source Sites for Gift Research

| Site | Language | Quality | Notes |
|------|----------|---------|-------|
| theknot.com | EN | ★★★★★ | 45+ curated gifts, editor picks |
| geschenkidee.de | DE | ★★★★☆ | Experience gifts, German market |
| vogue.com | EN | ★★★★☆ | Luxury focus, may paywall |
| uncommon goods | EN | ★★★★☆ | Unique/personalized items |
| cartida.de | DE | ★★★☆☆ | Star maps, personalized prints |
| librio.com | DE | ★★★☆☆ | Personalized books |


terminal

Output:
total 16
drwxr-xr-x  3 az-a samba 4096 Jun  4 09:10 .
drwxrwxrwx 21 root root  4096 Jun 11 09:25 ..
-rw-r--r--  1 az-a samba  235 May 24 12:18 camofox.config.json
drwxr-xr-x  3 root root  4096 May 24 15:14 profiles
/DATA/AppData/hermes-agent/tools/browser_camofox.py
/DATA/AppData/hermes-agent/tools/browser_camofox_state.py
/DATA/AppData/camofox/camofox.config.json


Ich habe jetzt die wichtigsten Camofox-Dokumente gelesen. Du meintest aber „alle smit Camofox“ — ich prüfe noch E-Mail und weitere lokale Dateien.

himalaya -c /DATA/AppData/hermes/himalaya.toml envelope list camofox OR Camofox --output plain --page-size 50 2>/dev/null | head -100 || true


{"output": "", "exit_code": 0, "error": null}

[Subdirectory context discovered: AppData/hermes/AGENTS.md]
# Operating Rules — Kiwi

## Default operating rule
If the path is clear, do the work and report after.
Do not make the user keep re-approving momentum.

## Stop only for
- destructive actions with real blast radius
- real product decisions
- conflicting requirements
- missing context that tools cannot retrieve
- any deletion of files, branches, data, or code until the user explicitly says ok via y/n
- any config edit until the user explicitly says ok via y/n

## Execution rule
- Bundle obvious next steps.
- Prefer action over recap.
- Use tools instead of describing what you would do.

## Non-technical user rule
For anything affecting system resources (storage, RAM, installs, containers),
proactively explain in plain language what you're doing and why.
Do not silently consume disk space or RAM. Ask if a path is unclear.


{"total_count": 0}

[Subdirectory context discovered: AppData/hermes-agent/AGENTS.md]
# Hermes Agent - Development Guide

Instructions for AI coding assistants and developers working on the hermes-agent codebase.

## Development Environment

```bash
# Prefer .venv; fall back to venv if that's what your checkout has.
source .venv/bin/activate   # or: source venv/bin/activate
```

`scripts/run_tests.sh` probes `.venv` first, then `venv`, then
`$HOME/.hermes/hermes-agent/venv` (for worktrees that share a venv with the
main checkout).

## Project Structure

File counts shift constantly — don't treat the tree below as exhaustive.
The canonical source is the filesystem. The notes call out the load-bearing
entry points you'll actually edit.

```
hermes-agent/
├── run_agent.py          # AIAgent class — core conversation loop (~12k LOC)
├── model_tools.py        # Tool orchestration, discover_builtin_tools(), handle_function_call()
├── toolsets.py           # Toolset definitions, _HERMES_CORE_TOOLS list
├── cli.py                # HermesCLI class — interactive CLI orchestrator (~11k LOC)
├── hermes_state.py       # SessionDB — SQLite session store (FTS5 search)
├── hermes_constants.py   # get_hermes_home(), display_hermes_home() — profile-aware paths
├── hermes_logging.py     # setup_logging() — agent.log / errors.log / gateway.log (profile-aware)
├── batch_runner.py       # Parallel batch processing
├── agent/                # Agent internals (provider adapters, memory, caching, compression, etc.)
├── hermes_cli/           # CLI subcommands, setup wizard, plugins loader, skin engine
├── tools/                # Tool implementations — auto-discovered via tools/registry.py
│   └── environments/     # Terminal backends (local, docker, ssh, modal, daytona, singularity)
├── gateway/              # Messaging gateway — run.py + session.py + platforms/
│   ├── platforms/        # Adapter per platform (telegram, discord, slack, whatsapp,
│   │                     #   homeassistant, signal, matrix, mattermost, email, sms,
│   │                     #   dingtalk, wecom, weixin, feishu, qqbot, bluebubbles,
│   │                     #   yuanbao, webhook, api_server, ...). See ADDING_A_PLATFORM.md.
│   └── builtin_hooks/    # Extension point for always-registered gateway hooks (none shipped)
├── plugins/              # Plugin system (see "Plugins" section below)
│   ├── memory/           # Memory-provider plugins (honcho, mem0, supermemory, ...)
│   ├── context_engine/   # Context-engine plugins
│   ├── model-providers/  # Inference backend plugins (openrouter, anthropic, gmi, ...)
│   ├── kanban/           # Multi-agent board dispatcher + worker plugin
│   ├── hermes-achievements/  # Gamified achievement tracking
│   ├── observability/    # Metrics / traces / logs plugin
│   ├── image_gen/        # Image-generation providers
│   └── <others>/         # disk-cleanup, example-dashboard, google_meet, platforms,
│                         #   spotify, strike-freedom-cockpit, ...
├── optional-skills/      # Heavier/niche skills shipped but NOT active by default
├── skills/               # Built-in skills bundled with the repo
├── ui-tui/               # Ink (React) terminal UI — `hermes --tui`
│   └── src/              # entry.tsx, app.tsx, gatewayClient.ts + app/components/hooks/lib
├── tui_gateway/          # Python JSON-RPC backend for the TUI
├── acp_adapter/          # ACP server (VS Code / Zed / JetBrains integration)
├── cron/                 # Scheduler — jobs.py, scheduler.py
├── environments/         # RL training environments (Atropos)
├── scripts/              # run_tests.sh, release.py, auxiliary scripts
├── website/              # Docusaurus docs site
└── tests/                # Pytest suite (~17k tests across ~900 files as of May 2026)
```

**User config:** `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys only).
**Logs:** `~/.hermes/logs/` — `agent.log` (INFO+), `errors.log` (WARNING+),
`gateway.log` when running the gateway. Profile-aware via `get_hermes_home()`.
Browse with `hermes logs [--follow] [--level ...] [--session ...]`.

## File Dependency Chain

```
tools/registry.py  (no deps — imported by all tool files)
       ↑
tools/*.py  (each calls registry.register() at import time)
       ↑
model_tools.py  (imports tools/registry + triggers tool discovery)
       ↑
run_agent.py, cli.py, batch_runner.py, environments/
```

---

## AIAgent Class (run_agent.py)

The real `AIAgent.__init__` takes ~60 parameters (credentials, routing, callbacks,
session context, budget, credential pool, etc.). The signature below is the
minimum subset you'll usually touch — read `run_agent.py` for the full list.

```python
class AIAgent:
    def __init__(self,
        base_url: str = None,
        api_key: str = None,
        provider: str = None,
        api_mode: str = None,              # "chat_completions" | "codex_responses" | ...
        model: str = "",                   # empty → resolved from config/provider later
        max_iterations: int = 90,          # tool-calling iterations (shared with subagents)
        enabled_toolsets: list = None,
        disabled_toolsets: list = None,
        quiet_mode: bool = False,
        save_trajectories: bool = False,
        platform: str = None,              # "cli", "telegram", etc.
        session_id: str = None,
        skip_context_files: bool = False,
        skip_memory: bool = False,
        credential_pool=None,
        # ... plus callbacks, thread/user/chat IDs, iteration_budget, fallback_model,
        # checkpoints config, prefill_messages, service_tier, reasoning_config, etc.
    ): ...

    def chat(self, message: str) -> str:
        """Simple interface — returns final response string."""

    def run_conversation(self, user_message: str, system_message: str = None,
                         conversation_history: list = None, task_id: str = None) -> dict:
        """Full interface — returns dict with final_response + messages."""
```

### Agent Loop

The core loop is inside `run_conversation()` — entirely synchronous, with
interrupt checks, budget tracking, and a one-turn grace call:

```python
while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) \
        or self._budget_grace_call:
    if self._interrupt_requested: break
    response = client.chat.completions.create(model=model, messages=messages, tools=tool_schemas)
    if response.tool_calls:
        for tool_call in response.tool_calls:
            result = handle_function_call(tool_call.name, tool_call.args, task_id)
            messages.append(tool_result_message(result))
        api_call_count += 1
    else:
        return response.content
```

Messages follow OpenAI format: `{"role": "system/user/assistant/tool", ...}`.
Reasoning content is stored in `assistant_msg["reasoning"]`.

---

## CLI Architecture (cli.py)

- **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete
- **KawaiiSpinner** (`agent/display.py`) — animated faces during API calls, `┊` activity feed for tool results
- `load_cli_config()` in cli.py merges hardcoded defaults + user config YAML
- **Skin engine** (`hermes_cli/skin_engine.py`) — data-driven CLI theming; initialized from `display.skin` config key at startup; skins customize banner colors, spinner faces/verbs/wings, tool prefix, response box, branding text
- `process_command()` is a method on `HermesCLI` — dispatches on canonical command name resolved via `resolve_command()` from the central registry
- Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching

### Slash Command Registry (`hermes_cli/commands.py`)

All slash commands are defined in a central `COMMAND_REGISTRY` list of `CommandDef` objects. Every downstream consumer derives from this registry automatically:

- **CLI** — `process_command()` resolves aliases via `resolve_command()`, dispatches on canonical name
- **Gateway** — `GATEWAY_KNOWN_COMMANDS` frozenset for hook emission, `resolve_command()` f

[...truncated AGENTS.md: 45,236 chars total]


Explanation:
<think>User asked: "Lies alle smit Camofox aus bitte" - likely meant "alle Mails mit Camofox" (all emails with Camofox). I attempted Himalaya search but it returned empty. Let me try proper Himalaya search syntax, and also check for emails differently. Also search session history maybe. But user wants "auslesen" - read all emails with Camofox. Need to find emails containing "Camofox".

I should use himalaya envelope list with search terms correctly. The previous command may have wrong syntax. Let me try again. Also can search local mail files. Also could be "alle SMS mit Camofox"? But he said "smit" probably typo for "Mails". Let's do proper search.

Also check /DATA/AppData/hermes/himalaya.toml config exists. And try himalaya search.

cat /DATA/AppData/hermes/himalaya.toml 2>/dev/null | head -80

himalaya -c /DATA/AppData/hermes/himalaya.toml envelope list --output json --page-size 100 | python3 -c "import json,sys,subprocess,re; data=json.load(sys.stdin); msgs=[m for m in data if any(re.search('camofox', str(v), re.I) for v in m.values())]; print(len(msgs)); [print(m.get('id'), m.get('subject'), m.get('from'), m.get('date')) for m in msgs]" 2>/dev/null || true


Output:
# himalaya config.toml - GMX Konto (Kiwi)

[accounts.gmx]
email = "amadeus.bot@gmx.at"
display-name = "Kiwi"
default = true

backend.type = "imap"
backend.host = "imap.gmx.net"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "amadeus.bot@gmx.at"
backend.auth.type = "password"
backend.auth.cmd = "cat /DATA/AppData/.secrets/gmx_email.env | grep GMX_PASSWORD | cut -d= -f2 | tr -d '\n'"

message.send.backend.type = "smtp"
message.send.backend.host = "mail.gmx.net"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "amadeus.bot@gmx.at"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "cat /DATA/AppData/.secrets/gmx_email.env | grep GMX_PASSWORD | cut -d= -f2 | tr -d '\n'"

folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Gesendet"
folder.aliases.drafts = "Entwürfe"
folder.aliases.trash = "Gelöscht"


Output: /usr/bin/bash: line 3: himalaya: command not found


Ich finde den Himalaya-Binary nicht am erwarteten Pfad. Suche alternativen Pfad.

Explanation: <think>

find /DATA/AppData/hermes -type f -name himalaya 2>/dev/null | head -5; which himalaya 2>/dev/null || true; ls /DATA/AppData/hermes/bin/ 2>/dev/null || true


Output:
/DATA/AppData/hermes/bin/himalaya
himalaya
tirith
yt-dlp


Explanation: <think>

/DATA/AppData/hermes/bin/himalaya -c /DATA/AppData/hermes/himalaya.toml envelope list --output json --page-size 100 | python3 -c "
import json,sys,re
data=json.load(sys.stdin)
msgs=[m for m in data if any(re.search('camofox', str(v), re.I) for v in m.values())]
print('Camofox matches:', len(msgs))
for m in msgs:
    print(m.get('id'), m.get('subject'), m.get('from'), m.get('date'))
" 2>/dev/null || true


Output: Camofox matches: 0


Ich lese noch die beiden Hermes-Camofox-Tool-Dateien direkt.

Explanation:
Ich habe jetzt:
1. Die Camofox-Skill-Doku geladen
2. Alle verlinkten Referenzen gelesen
3. Lokale Dateien gesucht
4. E-Mail auf Camofox durchsucht (keine Treffer)

Der User wollte „alle smit Camofox aus bitte“. Das könnte „alles mit Camofox“ bedeuten. Ich habe alles Zugängliche gelesen. Soll ich noch die zwei Python-Tool-Dateien browser_camofox.py und browser_camofox_state.py lesen? Das wäre sinnvoll, weil sie direkt mit Camofox zu tun haben.


     1|"""Camofox browser backend — local anti-detection browser via REST API.
     2|
     3|Camofox-browser is a self-hosted Node.js server wrapping Camoufox (Firefox
     4|fork with C++ fingerprint spoofing).  It exposes a REST API that maps 1:1
     5|to our browser tool interface: accessibility snapshots with element refs,
     6|click/type/scroll by ref, screenshots, etc.
     7|
     8|When ``CAMOFOX_URL`` is set (e.g. ``http://localhost:9377``), the browser
     9|tools route through this module instead of the ``agent-browser`` CLI.
    10|
    11|Setup::
    12|
    13|    # Option 1: npm
    14|    git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
    15|    npm install && npm start   # downloads Camoufox (~300MB) on first run
    16|
    17|    # Option 2: Docker
    18|    docker run -p 9377:9377 -e CAMOFOX_PORT=9377 jo-inc/camofox-browser
    19|
    20|Then set ``CAMOFOX_URL=http://localhost:9377`` in ``~/.hermes/.env``.
    21|"""
    22|
    23|from __future__ import annotations
    24|
    25|import base64
    26|import json
    27|import logging
    28|import os
    29|import threading
    30|import uuid
    31|from typing import Any, Dict, Optional
    32|
    33|import requests
    34|
    35|from hermes_cli.config import cfg_get, load_config
    36|from tools.browser_camofox_state import get_camofox_identity
    37|from tools.registry import tool_error
    38|
    39|logger = logging.getLogger(__name__)
    40|
    41|# ---------------------------------------------------------------------------
    42|# Configuration
    43|# ---------------------------------------------------------------------------
    44|
    45|_DEFAULT_TIMEOUT = 30  # seconds per HTTP request
    46|_SNAPSHOT_MAX_CHARS = 80_000  # camofox paginates at this limit
    47|_vnc_url: Optional[str] = None  # cached from /health response
    48|_vnc_url_checked = False  # only probe once per process
    49|
    50|
    51|def get_camofox_url() -> str:
    52|    """Return the configured Camofox server URL, or empty string."""
    53|    return os.getenv("CAMOFOX_URL", "").rstrip("/")
    54|
    55|
    56|def is_camofox_mode() -> bool:
    57|    """True when Camofox backend is configured and no CDP override is active.
    58|
    59|    When the user has explicitly connected to a live Chrome instance via
    60|    ``/browser connect`` (which sets ``BROWSER_CDP_URL``), the CDP connection
    61|    takes priority over Camofox so the browser tools operate on the real
    62|    browser instead of being silently routed to the Camofox backend.
    63|    """
    64|    if os.getenv("BROWSER_CDP_URL", "").strip():
    65|        return False
    66|    return bool(get_camofox_url())
    67|
    68|
    69|def check_camofox_available() -> bool:
    70|    """Verify the Camofox server is reachable."""
    71|    global _vnc_url, _vnc_url_checked
    72|    url = get_camofox_url()
    73|    if not url:
    74|        return False
    75|    try:
    76|        resp = requests.get(f"{url}/health", timeout=5)
    77|        if resp.status_code == 200 and not _vnc_url_checked:
    78|            try:
    79|                data = resp.json()
    80|                vnc_port = data.get("vncPort")
    81|                if isinstance(vnc_port, int) and 1 <= vnc_port <= 65535:
    82|                    from urllib.parse import urlparse
    83|                    parsed = urlparse(url)
    84|                    host = parsed.hostname or "localhost"
    85|                    _vnc_url = f"http://{host}:{vnc_port}"
    86|            except (ValueError, KeyError):
    87|                pass
    88|            _vnc_url_checked = True
    89|        return resp.status_code == 200
    90|    except Exception:
    91|        return False
    92|
    93|
    94|def get_vnc_url() -> Optional[str]:
    95|    """Return the VNC URL if the Camofox server exposes one, or None."""
    96|    if not _vnc_url_checked:
    97|        check_camofox_available()
    98|    return _vnc_url
    99|
   100|
   101|def _managed_persistence_enabled() -> bool:
   102|    """Return whether Hermes-managed persistence is enabled for Camofox.
   103|
   104|    When enabled, sessions use a stable profile-scoped userId so the
   105|    Camofox server can map it to a persistent browser profile directory.
   106|    When disabled (default), each session gets a random userId (ephemeral).
   107|
   108|    Controlled by ``browser.camofox.managed_persistence`` in config.yaml.
   109|    """
   110|    try:
   111|        camofox_cfg = load_config().get("browser", {}).get("camofox", {})
   112|    except Exception as exc:
   113|        logger.warning("managed_persistence check failed, defaulting to disabled: %s", exc)
   114|        return False
   115|    return bool(camofox_cfg.get("managed_persistence"))
   116|
   117|
   118|# ---------------------------------------------------------------------------
   119|# Session management
   120|# ---------------------------------------------------------------------------
   121|# Maps task_id -> {"user_id": str, "tab_id": str|None}
   122|_sessions: Dict[str, Dict[str, Any]] = {}
   123|_sessions_lock = threading.Lock()
   124|
   125|
   126|def _get_session(task_id: Optional[str]) -> Dict[str, Any]:
   127|    """Get or create a camofox session for the given task.
   128|
   129|    When managed persistence is enabled, uses a deterministic userId
   130|    derived from the Hermes profile so the Camofox server can map it
   131|    to the same persistent browser profile across restarts.
   132|    """
   133|    task_id = task_id or "default"
   134|    with _sessions_lock:
   135|        if task_id in _sessions:
   136|            return _sessions[task_id]
   137|        if _managed_persistence_enabled():
   138|            identity = get_camofox_identity(task_id)
   139|            session = {
   140|                "user_id": identity["user_id"],
   141|                "tab_id": None,
   142|                "session_key": identity["session_key"],
   143|                "managed": True,
   144|            }
   145|        else:
   146|            session = {
   147|                "user_id": f"hermes_{uuid.uuid4().hex[:10]}",
   148|                "tab_id": None,
   149|                "session_key": f"task_{task_id[:16]}",
   150|                "managed": False,
   151|            }
   152|        _sessions[task_id] = session
   153|        return session
   154|
   155|
   156|def _ensure_tab(task_id: Optional[str], url: str = "about:blank") -> Dict[str, Any]:
   157|    """Ensure a tab exists for the session, creating one if needed."""
   158|    session = _get_session(task_id)
   159|    if session["tab_id"]:
   160|        return session
   161|    base = get_camofox_url()
   162|    resp = requests.post(
   163|        f"{base}/tabs",
   164|        json={
   165|            "userId": session["user_id"],
   166|            "sessionKey": session["session_key"],
   167|            "url": url,
   168|        },
   169|        timeout=_DEFAULT_TIMEOUT,
   170|    )
   171|    resp.raise_for_status()
   172|    data = resp.json()
   173|    session["tab_id"] = data.get("tabId")
   174|    return session
   175|
   176|
   177|def _drop_session(task_id: Optional[str]) -> Optional[Dict[str, Any]]:
   178|    """Remove and return session info."""
   179|    task_id = task_id or "default"
   180|    with _sessions_lock:
   181|        return _sessions.pop(task_id, None)
   182|
   183|
   184|def camofox_soft_cleanup(task_id: Optional[str] = None) -> bool:
   185|    """Release the in-memory session without destroying the server-side context.
   186|
   187|    When managed persistence is enabled the browser profile (and its cookies)
   188|    must survive across agent tasks.  This helper drops only the local tracking
   189|    entry and returns ``True``.  When managed persistence is *not* enabled it
   190|    does nothing and returns ``False`` so the caller can fall back to
   191|    :func:`camofox_close`.
   192|    """
   193|    if _managed_persistence_enabled():
   194|        _drop_session(task_id)
   195|        logger.debug("Camofox soft cleanup for task %s (managed persistence)", task_id)
   196|        return True
   197|    return False
   198|
   199|
   200|# ---------------------------------------------------------------------------
   201|# HTTP helpers
   202|# ---------------------------------------------------------------------------
   203|
   204|def _post(path: str, body: dict, timeout: int = _DEFAULT_TIMEOUT) -> dict:
   205|    """POST JSON to camofox and return parsed response."""
   206|    url = f"{get_camofox_url()}{path}"
   207|    resp = requests.post(url, json=body, timeout=timeout)
   208|    resp.raise_for_status()
   209|    return resp.json()
   210|
   211|
   212|def _get(path: str, params: dict = None, timeout: int = _DEFAULT_TIMEOUT) -> dict:
   213|    """GET from camofox and return parsed response."""
   214|    url = f"{get_camofox_url()}{path}"
   215|    resp = requests.get(url, params=params, timeout=timeout)
   216|    resp.raise_for_status()
   217|    return resp.json()
   218|
   219|
   220|def _get_raw(path: str, params: dict = None, timeout: int = _DEFAULT_TIMEOUT) -> requests.Response:
   221|    """GET from camofox and return raw response (for binary data)."""
   222|    url = f"{get_camofox_url()}{path}"
   223|    resp = requests.get(url, params=params, timeout=timeout)
   224|    resp.raise_for_status()
   225|    return resp
   226|
   227|
   228|def _delete(path: str, body: dict = None, timeout: int = _DEFAULT_TIMEOUT) -> dict:
   229|    """DELETE to camofox and return parsed response."""
   230|    url = f"{get_camofox_url()}{path}"
   231|    resp = requests.delete(url, json=body, timeout=timeout)
   232|    resp.raise_for_status()
   233|    return resp.json()
   234|
   235|
   236|# ---------------------------------------------------------------------------
   237|# Tool implementations
   238|# ---------------------------------------------------------------------------
   239|
   240|def camofox_navigate(url: str, task_id: Optional[str] = None) -> str:
   241|    """Navigate to a URL via Camofox."""
   242|    try:
   243|        session = _get_session(task_id)
   244|        if not session["tab_id"]:
   245|            # Create tab with the target URL directly
   246|            session = _ensure_tab(task_id, url)
   247|            data = {"ok": True, "url": url}
   248|        else:
   249|            # Navigate existing tab
   250|            data = _post(
   251|                f"/tabs/{session['tab_id']}/navigate",
   252|                {"userId": session["user_id"], "url": url},
   253|                timeout=60,
   254|            )
   255|        result = {
   256|            "success": True,
   257|            "url": data.get("url", url),
   258|            "title": data.get("title", ""),
   259|        }
   260|        vnc = get_vnc_url()
   261|        if vnc:
   262|            result["vnc_url"] = vnc
   263|            result["vnc_hint"] = (
   264|                "Browser is visible via VNC. "
   265|                "Share this link with the user so they can watch the browser live."
   266|            )
   267|
   268|        # Auto-take a compact snapshot so the model can act immediately
   269|        try:
   270|            snap_data = _get(
   271|                f"/tabs/{session['tab_id']}/snapshot",
   272|                params={"userId": session["user_id"]},
   273|            )
   274|            snapshot_text = snap_data.get("snapshot", "")
   275|            from tools.browser_tool import (
   276|                SNAPSHOT_SUMMARIZE_THRESHOLD,
   277|                _truncate_snapshot,
   278|            )
   279|            if len(snapshot_text) > SNAPSHOT_SUMMARIZE_THRESHOLD:
   280|                snapshot_text = _truncate_snapshot(snapshot_text)
   281|            result["snapshot"] = snapshot_text
   282|            result["element_count"] = snap_data.get("refsCount", 0)
   283|        except Exception:
   284|            pass  # Navigation succeeded; snapshot is a bonus
   285|
   286|        return json.dumps(result)
   287|    except requests.HTTPError as e:
   288|        return tool_error(f"Navigation failed: {e}", success=False)
   289|    except requests.ConnectionError:
   290|        return json.dumps({
   291|            "success": False,
   292|            "error": f"Cannot connect to Camofox at {get_camofox_url()}. "
   293|                     "Is the server running? Start with: npm start (in camofox-browser dir) "
   294|                     "or: docker run -p 9377:9377 -e CAMOFOX_PORT=9377 jo-inc/camofox-browser",
   295|        })
   296|    except Exception as e:
   297|        return tool_error(str(e), success=False)
   298|
   299|
   300|def camofox_snapshot(full: bool = False, task_id: Optional[str] = None,
   301|                     user_task: Optional[str] = None) -> str:
   302|    """Get accessibility tree snapshot from Camofox."""
   303|    try:
   304|        session = _get_session(task_id)
   305|        if not session["tab_id"]:
   306|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   307|
   308|        data = _get(
   309|            f"/tabs/{session['tab_id']}/snapshot",
   310|            params={"userId": session["user_id"]},
   311|        )
   312|
   313|        snapshot = data.get("snapshot", "")
   314|        refs_count = data.get("refsCount", 0)
   315|
   316|        # Apply same summarization logic as the main browser tool
   317|        from tools.browser_tool import (
   318|            SNAPSHOT_SUMMARIZE_THRESHOLD,
   319|            _extract_relevant_content,
   320|            _truncate_snapshot,
   321|        )
   322|
   323|        if len(snapshot) > SNAPSHOT_SUMMARIZE_THRESHOLD:
   324|            if user_task:
   325|                snapshot = _extract_relevant_content(snapshot, user_task)
   326|            else:
   327|                snapshot = _truncate_snapshot(snapshot)
   328|
   329|        return json.dumps({
   330|            "success": True,
   331|            "snapshot": snapshot,
   332|            "element_count": refs_count,
   333|        })
   334|    except Exception as e:
   335|        return tool_error(str(e), success=False)
   336|
   337|
   338|def camofox_click(ref: str, task_id: Optional[str] = None) -> str:
   339|    """Click an element by ref via Camofox."""
   340|    try:
   341|        session = _get_session(task_id)
   342|        if not session["tab_id"]:
   343|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   344|
   345|        # Strip @ prefix if present (our tool convention)
   346|        clean_ref = ref.lstrip("@")
   347|
   348|        data = _post(
   349|            f"/tabs/{session['tab_id']}/click",
   350|            {"userId": session["user_id"], "ref": clean_ref},
   351|        )
   352|        return json.dumps({
   353|            "success": True,
   354|            "clicked": clean_ref,
   355|            "url": data.get("url", ""),
   356|        })
   357|    except Exception as e:
   358|        return tool_error(str(e), success=False)
   359|
   360|
   361|def camofox_type(ref: str, text: str, task_id: Optional[str] = None) -> str:
   362|    """Type text into an element by ref via Camofox."""
   363|    try:
   364|        session = _get_session(task_id)
   365|        if not session["tab_id"]:
   366|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   367|
   368|        clean_ref = ref.lstrip("@")
   369|
   370|        _post(
   371|            f"/tabs/{session['tab_id']}/type",
   372|            {"userId": session["user_id"], "ref": clean_ref, "text": text},
   373|        )
   374|        return json.dumps({
   375|            "success": True,
   376|            "typed": text,
   377|            "element": clean_ref,
   378|        })
   379|    except Exception as e:
   380|        return tool_error(str(e), success=False)
   381|
   382|
   383|def camofox_scroll(direction: str, task_id: Optional[str] = None) -> str:
   384|    """Scroll the page via Camofox."""
   385|    try:
   386|        session = _get_session(task_id)
   387|        if not session["tab_id"]:
   388|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   389|
   390|        _post(
   391|            f"/tabs/{session['tab_id']}/scroll",
   392|            {"userId": session["user_id"], "direction": direction},
   393|        )
   394|        return json.dumps({"success": True, "scrolled": direction})
   395|    except Exception as e:
   396|        return tool_error(str(e), success=False)
   397|
   398|
   399|def camofox_back(task_id: Optional[str] = None) -> str:
   400|    """Navigate back via Camofox."""
   401|    try:
   402|        session = _get_session(task_id)
   403|        if not session["tab_id"]:
   404|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   405|
   406|        data = _post(
   407|            f"/tabs/{session['tab_id']}/back",
   408|            {"userId": session["user_id"]},
   409|        )
   410|        return json.dumps({"success": True, "url": data.get("url", "")})
   411|    except Exception as e:
   412|        return tool_error(str(e), success=False)
   413|
   414|
   415|def camofox_press(key: str, task_id: Optional[str] = None) -> str:
   416|    """Press a keyboard key via Camofox."""
   417|    try:
   418|        session = _get_session(task_id)
   419|        if not session["tab_id"]:
   420|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   421|
   422|        _post(
   423|            f"/tabs/{session['tab_id']}/press",
   424|            {"userId": session["user_id"], "key": key},
   425|        )
   426|        return json.dumps({"success": True, "pressed": key})
   427|    except Exception as e:
   428|        return tool_error(str(e), success=False)
   429|
   430|
   431|def camofox_close(task_id: Optional[str] = None) -> str:
   432|    """Close the browser session via Camofox."""
   433|    try:
   434|        session = _drop_session(task_id)
   435|        if not session:
   436|            return json.dumps({"success": True, "closed": True})
   437|
   438|        _delete(
   439|            f"/sessions/{session['user_id']}",
   440|        )
   441|        return json.dumps({"success": True, "closed": True})
   442|    except Exception as e:
   443|        return json.dumps({"success": True, "closed": True, "warning": str(e)})
   444|
   445|
   446|def camofox_get_images(task_id: Optional[str] = None) -> str:
   447|    """Get images on the current page via Camofox.
   448|
   449|    Extracts image information from the accessibility tree snapshot,
   450|    since Camofox does not expose a dedicated /images endpoint.
   451|    """
   452|    try:
   453|        session = _get_session(task_id)
   454|        if not session["tab_id"]:
   455|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   456|
   457|        import re
   458|
   459|        data = _get(
   460|            f"/tabs/{session['tab_id']}/snapshot",
   461|            params={"userId": session["user_id"]},
   462|        )
   463|        snapshot = data.get("snapshot", "")
   464|
   465|        # Parse img elements from the accessibility tree.
   466|        # Format: img "alt text" or img "alt text" [eN]
   467|        # URLs appear on /url: lines following img entries
   468|        images = []
   469|        lines = snapshot.split("\n")
   470|        for i, line in enumerate(lines):
   471|            stripped = line.strip()
   472|            if stripped.startswith(("- img ", "img ")):
   473|                alt_match = re.search(r'img\s+"([^"]*)"', stripped)
   474|                alt = alt_match.group(1) if alt_match else ""
   475|                # Look for URL on the next line
   476|                src = ""
   477|                if i + 1 < len(lines):
   478|                    url_match = re.search(r'/url:\s*(\S+)', lines[i + 1].strip())
   479|                    if url_match:
   480|                        src = url_match.group(1)
   481|                if alt or src:
   482|                    images.append({"src": src, "alt": alt})
   483|
   484|        return json.dumps({
   485|            "success": True,
   486|            "images": images,
   487|            "count": len(images),
   488|        })
   489|    except Exception as e:
   490|        return tool_error(str(e), success=False)
   491|
   492|
   493|def camofox_vision(question: str, annotate: bool = False,
   494|                   task_id: Optional[str] = None) -> str:
   495|    """Take a screenshot and analyze it with vision AI via Camofox."""
   496|    try:
   497|        session = _get_session(task_id)
   498|        if not session["tab_id"]:
   499|            return tool_error("No browser session. Call browser_navigate first.", success=False)
   500|
   501|

Use offset=501 to continue reading (showing 1-500 of 603 lines)


     1|"""Hermes-managed Camofox state helpers.
     2|
     3|Provides profile-scoped identity and state directory paths for Camofox
     4|persistent browser profiles.  When managed persistence is enabled, Hermes
     5|sends a deterministic userId derived from the active profile so that
     6|Camofox can map it to the same persistent browser profile directory
     7|across restarts.
     8|"""
     9|
    10|from __future__ import annotations
    11|
    12|import uuid
    13|from pathlib import Path
    14|from typing import Dict, Optional
    15|
    16|from hermes_constants import get_hermes_home
    17|
    18|CAMOFOX_STATE_DIR_NAME = "browser_auth"
    19|CAMOFOX_STATE_SUBDIR = "camofox"
    20|
    21|
    22|def get_camofox_state_dir() -> Path:
    23|    """Return the profile-scoped root directory for Camofox persistence."""
    24|    return get_hermes_home() / CAMOFOX_STATE_DIR_NAME / CAMOFOX_STATE_SUBDIR
    25|
    26|
    27|def get_camofox_identity(task_id: Optional[str] = None) -> Dict[str, str]:
    28|    """Return the stable Hermes-managed Camofox identity for this profile.
    29|
    30|    The user identity is profile-scoped (same Hermes profile = same userId).
    31|    The session key is scoped to the logical browser task so newly created
    32|    tabs within the same profile reuse the same identity contract.
    33|    """
    34|    scope_root = str(get_camofox_state_dir())
    35|    logical_scope = task_id or "default"
    36|    user_digest = uuid.uuid5(
    37|        uuid.NAMESPACE_URL,
    38|        f"camofox-user:{scope_root}",
    39|    ).hex[:10]
    40|    session_digest = uuid.uuid5(
    41|        uuid.NAMESPACE_URL,
    42|        f"camofox-session:{scope_root}:{logical_scope}",
    43|    ).hex[:16]
    44|    return {
    45|        "user_id": f"hermes_{user_digest}",
    46|        "session_key": f"task_{session_digest}",
    47|    }
    48|


     1|{
     2|  "id": "camofox-browser",
     3|  "name": "Camofox Browser",
     4|  "version": "1.6.0",
     5|  "plugins": {
     6|    "youtube": { "enabled": true },
     7|    "persistence": { "enabled": true },
     8|    "vnc": { "enabled": true, "resolution": "1920x1080" }
     9|  }
    10|}
    11|


Question: Camofox camofox browser anti-detection persistent profile


   501|        # Get screenshot as binary PNG
   502|        resp = _get_raw(
   503|            f"/tabs/{session['tab_id']}/screenshot",
   504|            params={"userId": session["user_id"]},
   505|        )
   506|
   507|        # Save screenshot to cache
   508|        from hermes_constants import get_hermes_home
   509|        screenshots_dir = get_hermes_home() / "browser_screenshots"
   510|        screenshots_dir.mkdir(parents=True, exist_ok=True)
   511|        screenshot_path = str(screenshots_dir / f"browser_screenshot_{uuid.uuid4().hex[:8]}.png")
   512|
   513|        with open(screenshot_path, "wb") as f:
   514|            f.write(resp.content)
   515|
   516|        # Encode for vision LLM
   517|        img_b64 = base64.b64encode(resp.content).decode("utf-8")
   518|
   519|        # Also get annotated snapshot if requested
   520|        annotation_context = ""
   521|        if annotate:
   522|            try:
   523|                snap_data = _get(
   524|                    f"/tabs/{session['tab_id']}/snapshot",
   525|                    params={"userId": session["user_id"]},
   526|                )
   527|                annotation_context = f"\n\nAccessibility tree (element refs for interaction):\n{snap_data.get('snapshot', '')[:3000]}"
   528|            except Exception:
   529|                pass
   530|
   531|        # Redact secrets from annotation context before sending to vision LLM.
   532|        # The screenshot image itself cannot be redacted, but at least the
   533|        # text-based accessibility tree snippet won't leak secret values.
   534|        from agent.redact import redact_sensitive_text
   535|        annotation_context = redact_sensitive_text(annotation_context)
   536|
   537|        # Send to vision LLM
   538|        from agent.auxiliary_client import call_llm
   539|
   540|        vision_prompt = (
   541|            f"Analyze this browser screenshot and answer: {question}"
   542|            f"{annotation_context}"
   543|        )
   544|
   545|        try:
   546|            _cfg = load_config()
   547|            _vision_cfg = cfg_get(_cfg, "auxiliary", "vision", default={})
   548|            _vision_timeout = float(_vision_cfg.get("timeout", 120))
   549|            _vision_temperature = float(_vision_cfg.get("temperature", 0.1))
   550|        except Exception:
   551|            _vision_timeout = 120.0
   552|            _vision_temperature = 0.1
   553|
   554|        response = call_llm(
   555|            messages=[{
   556|                "role": "user",
   557|                "content": [
   558|                    {"type": "text", "text": vision_prompt},
   559|                    {
   560|                        "type": "image_url",
   561|                        "image_url": {
   562|                            "url": f"data:image/png;base64,{img_b64}",
   563|                        },
   564|                    },
   565|                ],
   566|            }],
   567|            task="vision",
   568|            temperature=_vision_temperature,
   569|            timeout=_vision_timeout,
   570|        )
   571|        analysis = (response.choices[0].message.content or "").strip() if response.choices else ""
   572|
   573|        # Redact secrets the vision LLM may have read from the screenshot.
   574|        from agent.redact import redact_sensitive_text
   575|        analysis = redact_sensitive_text(analysis)
   576|
   577|        return json.dumps({
   578|            "success": True,
   579|            "analysis": analysis,
   580|            "screenshot_path": screenshot_path,
   581|        })
   582|    except Exception as e:
   583|        return tool_error(str(e), success=False)
   584|
   585|
   586|def camofox_console(clear: bool = False, task_id: Optional[str] = None) -> str:
   587|    """Get console output — limited support in Camofox.
   588|
   589|    Camofox does not expose browser console logs via its REST API.
   590|    Returns an empty result with a note.
   591|    """
   592|    return json.dumps({
   593|        "success": True,
   594|        "console_messages": [],
   595|        "js_errors": [],
   596|        "total_messages": 0,
   597|        "total_errors": 0,
   598|        "note": "Console log capture is not available with the Camofox backend. "
   599|                "Use browser_snapshot or browser_vision to inspect page state.",
   600|    })
   601|
   602|
   603|
   604|


Question: Camofox camofox browser anti-detection persistent profile

Summary:
**Summary: Camofox Anti-Detection Browser Setup on ZimaOS for Hermes**

**1. What the user wanted**
The user instructed the assistant to learn a permanent skill named **zimaos-camofox-browser**. The goal was to make Hermes always use a local **Camofox** (Camoufox-based) anti-detection browser running in a Docker container on ZimaOS, instead of relying on Webtop/Chromium, Kasm, Browserless, or cloud providers. Key requirements included:
- Persistent browser profiles (cookies, logins, LocalStorage across restarts).
- Hermes native browser tools routed via `CAMOFOX_URL`.
- noVNC enabled only for manual observation/MFA help, not for primary agent control.
- No public WAN exposure of the Camofox API (bind to localhost or internal Docker network).
- No `CAMOFOX_API_KEY` due to known 403 issues with Hermes’ integration.
- Persistent host paths under `/DATA/AppData/camofox`.

**2. Actions taken and outcomes**
- **Skill creation**: Created the skill `zimaos-camofox-browser` under `devops` at `/DATA/.hermes/skills/devops/zimaos-camofox-browser/SKILL.md` via `skill_manage`.
- **Source acquisition**: Cloned `https://github.com/jo-inc/camofox-browser` into `/tmp/camofox-browser`. The public image `jo-inc/camofox-browser` was not pullable (access denied), so a local build was required.
- **Asset downloads**: Because ZimaOS lacked the `make` command, the assistant manually downloaded the Camoufox binary (`camoufox-x86_64.zip`, ~679 MB) and `yt-dlp` (~34 MB) into `/tmp/camofox-browser/dist/` using the URLs defined in the upstream Makefile.
- **Docker image build**: Built the image locally with BuildKit using ZimaOS-specific workarounds:
  ```bash
  sg docker -c "DOCKER_CONFIG=/tmp/docker-config DOCKER_BUILDKIT=1 docker build ..."
  ```
  Image tag: `camofox-browser:135.0.1-x86_64`.
- **noVNC fix**: The default upstream image lacked `/usr/share/novnc`. The assistant added the `novnc` package to the Dockerfile and rebuilt the image so the VNC plugin could serve the web client.
- **Runtime configuration**: Wrote `/DATA/AppData/camofox-compose/camofox.config.json` with the `vnc` plugin explicitly enabled (it was disabled by default), then mounted it into the container.
- **Docker Compose**: Created `/DATA/AppData/camofox-compose/docker-compose.yml` with:
  - Ports bound to `127.0.0.1:9377` (API) and `127.0.0.1:6080` (noVNC).
  - Volume `/DATA/AppData/camofox:/root/.camofox` for persistent state.
  - Environment: `CAMOFOX_PORT=9377`, `ENABLE_VNC=1`, `VNC_BIND=0.0.0.0`, `MAX_OLD_SPACE_SIZE=2048`.
- **Hermes configuration updates**:
  - Added `CAMOFOX_URL=http://127.0.0.1:9377` to `/DATA/AppData/hermes/.env`.
  - Added the `browser` toolset to `/DATA/AppData/hermes/config.yaml` (only `hermes-cli` was active before).
  - Added the block:
    ```yaml
    browser:
      camofox:
        managed_persistence: true
    ```
    to `/DATA/AppData/hermes/config.yaml`.
- **Gateway restart**: Killed the running Hermes gateway process (PID `3302261`, `/DATA/AppData/hermes/venv/bin/python -m hermes_cli.main gateway run`) so the toolset and env changes would take effect.
- **Verification**:
  - Health endpoint responded correctly:
    ```bash
    curl http://127.0.0.1:9377/health
    # {"ok":true,"engine":"camoufox","browserConnected":true,"browserRunning":true,...}
    ```
  - Full API smoke test passed: created a tab via `POST /tabs?userId=test&sessionKey=smoke`, fetched a snapshot from `example.com`, and closed the tab successfully.

**3. Key decisions and solutions found**
- **Local build over public image**: The upstream Docker Hub image was unavailable, so the assistant built from source on the host.
- **Host-mode networking for Hermes**: Because Hermes runs directly on the ZimaOS host (not inside Docker), `CAMOFOX_URL=http://127.0.0.1:9377` was chosen over inter-container DNS names.
- **Managed persistence enabled**: `browser.camofox.managed_persistence: true` ensures Hermes reuses the same persistent profile directory across tasks, preserving sessions and cookies.
- **Security via bind address**: Port `9377` is bound strictly to `127.0.0.1`; no API key is used, relying on localhost binding and Docker network isolation instead.
- **ZimaOS-specific execution quirks**: Used `sg docker` to apply group membership in the same session and `DOCKER_CONFIG=/tmp/docker-config` to avoid read-only root filesystem issues under `/root/.docker`.

**4. Important commands, files, and technical details**
- **Skill path**: `/DATA/.hermes/skills/devops/zimaos-camofox-browser/SKILL.md`
- **Container image**: `camofox-browser:135.0.1-x86_64`
- **Compose file**: `/DATA/AppData/camofox-compose/docker-compose.yml`
- **Camofox config mount**: `/DATA/AppData/camofox-compose/camofox.config.json:/app/camofox.config.json`
- **Persistent data dir**: `/DATA/AppData/camofox` → container `/root/.camofox`
- **Hermes env**: `/DATA/AppData/hermes/.env` → `CAMOFOX_URL=http://127.0.0.1:9377`
- **Hermes config**: `/DATA/AppData/hermes/config.yaml` (added `browser` toolset + `browser.camofox.managed_persistence: true`)
- **Health check command**: `curl -fsS http://127.0.0.1:9377/health`
- **Build context**: `/tmp/camofox-browser` (cloned repo with downloaded `dist/camoufox-x86_64.zip` and `dist/yt-dlp-x86_64`)
- **noVNC port**: `127.0.0.1:6080` (intended for SSH-tunnel or localhost viewing)

**5. Unresolved / notable**
- **Truncated final VNC verification**: The transcript ends while testing noVNC on port `6080` after the image rebuild. The last terminal output showed `curl: (56) Recv failure: Connection reset by peer` immediately after the rebuilt container started, so it is **unclear whether noVNC was fully functional** by the end of the session, even though the VNC plugin loaded and the API remained healthy.
- **No live Hermes browser tool verification**: The assistant verified the Camofox REST API directly with `curl`, but the transcript does not include a final confirmation that Hermes’ own `browser_navigate` / `browser_snapshot` tools were invoked through the gateway after restart. The configuration was applied and the gateway restarted, but an end-to-end Hermes tool test was not shown.


Ich prüfe noch die übrige Referenzdatei und die Compose-Dateien.

Explanation:
Boss wants me to read everything about Camofox. I've already read:
1. The skill SKILL.md (zimaos-camofox-browser)
2. All linked reference files:
   - research-patterns.md
   - build-dockerfile.md
   - ris-legal-research.md
   - multi-source-web-research.md
3. The tool implementation files:
   - browser_camofox.py (full 603 lines)
   - browser_camofox_state.py
   - camofox.config.json
4. Email search - no Camofox-related emails found
5. Session search - found the original session where Camofox was set up

I should check if there are any remaining files. The skill also mentions there's an abgb-364c-veraeusserungsverbot.md reference file. Let me read that too. Also maybe search for any docker-compose files or other Camofox-related config files.


zimaos-camofox-browser

# ABGB § 364c — Belastungs- und Veräußerungsverbot

Domain knowledge from OGH-Recherche, Stand 08.06.2026. Für zukünftige Sitzungen zu österreichischem Sachen-/Erbrecht.

## Gesetzestext (konsolidierte Fassung)

§ 364c ABGB (JGS Nr. 946/1811, zuletzt BGBl. I Nr. 135/2009):

> Ein vertragsmäßiges oder letztwilliges Veräußerungs- oder Belastungsverbot hinsichtlich einer Sache oder eines dinglichen Rechtes verpflichtet nur den ersten Eigentümer, nicht aber seine Erben oder sonstigen Rechtsnachfolger. Gegen Dritte wirkt es dann, wenn es zwischen Ehegatten, eingetragenen Partnern, Eltern und Kindern, Wahl- oder Pflegekindern oder deren Ehegatten oder eingetragenen Partnern begründet und im öffentlichen Buche eingetragen wurde.

## Zwei-Stufen-Konstruktion (OGH RS0062140, 5 Ob 105/95)

| Stufe | Rechtsnatur | Zwischen wem |
|-------|------------|-------------|
| Satz 1 (obligatorisch) | Schuldrechtliche Vereinbarung | **Jeder** Eigentümer zugunsten **jedweder** Person möglich |
| Satz 2 (Drittwirkung) | Grundbuchseintragung mit Drittwirkung | Nur bei **qualifiziertem Familienverhältnis** |

Ohne Eintragung (weil Familienverhältnis fehlt): Nur obligatorische Wirkung. Bei Zuwiderhandlung → Schadenersatz, aber keine dingliche Wirkung gegen Dritte.

## Familienkreis (§ 364c Satz 2) — abschließend

Aufgezählte Verhältnisse (OGH RS0010723, 6 Ob 691/79):

- Ehegatten
- Eingetragene Partner
- Eltern und Kinder
- Wahl- oder Pflegekinder
- Deren Ehegatten oder eingetragene Partner

**Geschwister sind NICHT erfasst.** Keine Analogie. Einzige von der Rsp anerkannte Ausdehnung: Stiefkinder (5 Ob 104/98z).

## Eintragung auf Miteigentumsanteil: zulässig

OGH 5 Ob 111/91 (RS0010808):
> „Soll nicht die ganze Liegenschaft des Eigentümers, sondern nur ein ideeller Anteil belastet werden, **ein durchaus zulässiger Vorgang**, so ist damit notwendig die Aufspaltung des das Alleineigentumsrecht des Antragstellers ausdrückenden Eigentumsanteiles in mehrere, demselben Eigentümer gehörende Miteigentumsanteile verbunden."

OGH 5 Ob 2243/96f (RS0105682):
> „Der Verbücherung eines Veräußerungsverbotes und Belastungsverbotes steht nicht entgegen, **daß der nunmehrige Hälfteeigentümer einmal Alleineigentümer der Liegenschaft war**."

Die Eintragung scheitert nur bei **zwischenzeitigem Eigentumserwerb eines anderen** (weil BuV nur ersten Eigentümer verpflichtet, § 364c Satz 1). Zusammenziehung von Anteilen ist keine solche Rechtsnachfolge.

## Letztwilliges BuV: Auf welches Familienverhältnis kommt es an?

Der Schlüssel liegt im Wortlaut: „zwischen **begründet**". Wer begründet das BuV?

- **Vertragsmäßig**: Eigentümer und Berechtigter → Familienverhältnis **Eigentümer ↔ Berechtigter** erforderlich
- **Letztwillig**: Erblasser ordnet BuV an → Familienverhältnis **Erblasser ↔ Berechtigter** maßgeblich

**H.L. (Koziol/Bydlinski/Bollenberger, Spielbüchler in Rummel):** Es kommt auf das Verhältnis Erblasser ↔ Berechtigter an. Der Erbe ist nur passiver Verbotsträger ("erster Eigentümer" iSd § 364c Satz 1), nicht der Begründende.

**Mindermeinung (Hofmeister, NZ 1987):** Stellt auf Eigentümer ↔ Berechtigter ab. Hat sich nicht durchgesetzt, wird aber vereinzelt von Grundbuchsgerichten vertreten.

**Praxis-Empfehlung:** Bei letztwilligem BuV, wo Eigentümer und Berechtigter kein qualifiziertes Familienverhältnis haben, vor Testamentserrichtung beim zuständigen Grundbuchsgericht anfragen, welcher Lesart es folgt.

## Schlüssel-OGH-Entscheidungen (Rechtssätze)

| RS | Inhalt | Letzte Bestätigung |
|----|--------|-------------------|
| RS0010723 | BuV nur zwischen Familienmitgliedern; kein Vermögensobjekt; nicht übertragbar | 16.10.2025 (6 Ob 176/24m) |
| RS0010805 | BuV ist höchstpersönliches, nicht verwertbares Recht | 29.07.2025 (2 Ob 28/25v) |
| RS0062140 | Zwei-Stufen-Konstruktion: Obligation (jedermann) vs. Drittwirkung (nur Familie) | 01.06.2022 |
| RS0010808 | Belastung eines ideellen Anteils zulässig; Aufspaltung in Miteigentumsanteile | 08.10.1996 |
| RS0105682 | Früheres Alleineigentum hindert BuV-Eintragung auf Hälfteanteil nicht | 08.10.1996 |
| RS0010724 | BuV zwischen Ehegatten überlebt Scheidung | 27.09.2021 |
| RS0010739 | Belastungsverbot verbietet nur Pfandrechte + beschränkte dingliche Nutzungsrechte | 05.03.2024 |
| RS0010742 | BuV hindert jede Übertragung (auch Tausch) | 27.01.2023 |

## Alternativen zum BuV ohne qualifiziertes Familienverhältnis

Wenn Drittwirkung benötigt wird, aber kein § 364c-Familienverhältnis vorliegt:

- **Dingliches Vorkaufsrecht** (§ 1072 ABGB): Verbücherbar für jedermann, kein Familienverhältnis nötig. Verhindert nicht Veräußerung, aber gibt Eintrittsrecht in Kaufvertrag.
- **Pfandrecht**: Klassische Forderungssicherung, verbücherbar ohne Familienverhältnis.
- **Testamentarische Direktbegünstigung**: Statt Umweg über Erben direkt im Testament den gewünschten Berechtigten einsetzen (Eigentum oder Vermächtnis).


Path not found: /DATA/AppData/camofox-compose. Similar paths: /DATA/AppData/camofox


File not found: /DATA/AppData/camofox-compose/docker-compose.yml


Boss, ich habe alles mit Camofox-Bezug gelesen, was auf dem System verfügbar ist. Hier die Zusammenfassung:

## Was Camofox bei dir ist

Camofox ist ein **lokaler Anti-Detection-Browser** auf ZimaOS, basierend auf Camoufox (Firefox-Fork mit Fingerprint-Spoofing). Hermes nutzt ihn über `CAMOFOX_URL=http://127.0.0.1:9377` für Browser-Automation.

## Wo die wichtigen Dateien liegen

| Was | Pfad |
|---|---|
| Skill-Doku | `/DATA/.hermes/skills/devops/zimaos-camofox-browser/SKILL.md` |
| Build-Dockerfile | `/DATA/.hermes/skills/devops/zimaos-camofox-browser/references/build-dockerfile.md` |
| Research-Patterns | `/DATA/.hermes/skills/devops/zimaos-camofox-browser/references/research-patterns.md` |
| RIS-Recht-Recherche | `/DATA/.hermes/skills/devops/zimaos-camofox-browser/references/ris-legal-research.md` |
| Multi-Source-Web-Research | `/DATA/.hermes/skills/devops/zimaos-camofox-browser/references/multi-source-web-research.md` |
| ABGB § 364c Knowledge | `/DATA/.hermes/skills/devops/zimaos-camofox-browser/references/abgb-364c-veraeusserungsverbot.md` |
| Hermes Tool-Code | `/DATA/AppData/hermes-agent/tools/browser_camofox.py` |
| Hermes State-Helper | `/DATA/AppData/hermes-agent/tools/browser_camofox_state.py` |
| Camofox Config | `/DATA/AppData/camofox/camofox.config.json` |
| Persistente Profile | `/DATA/AppData/camofox/profiles/` |

## Aktueller Status

- Container-Image: `camofox-browser:135.0.1-x86_64` (lokal gebaut)
- API-Port: `127.0.0.1:9377`
- noVNC-Port: `127.0.0.1:6080`
- Volumes: `/DATA/AppData/camofox:/root/.camofox`
- Managed Persistence in Hermes aktiviert (`browser.camofox.managed_persistence: true`)
- `CAMOFOX_URL` in `/DATA/AppData/hermes/.env` gesetzt

## Wichtige technische Regeln aus der Doku

- **Kein `CAMOFOX_API_KEY`** — sonst gibt es 403-Probleme mit Hermes
- Sicherheit durch `127.0.0.1`-Bindung und Docker-Netzwerk
- `/evaluate` Endpoint **blockiert ohne API-Key** — Workaround: `/type` + `/click`
- POST-Endpunkte brauchen `userId` und `sessionKey` im **Body**, nicht als Query-Params
- GET-Endpunkte (screenshot, links) nutzen Query-Params
- `browserConnected` in `/health` wird erst `true`, nachdem der erste Tab geöffnet wurde

## Was ich nicht gefunden habe

- **Keine E-Mails** mit Camofox-Bezug im GMX-Postfach
- Das Docker-Compose-File `/DATA/AppData/camofox-compose/docker-compose.yml` existiert aktuell nicht mehr — nur `/DATA/AppData/camofox` mit Config und Profile

Soll ich jetzt etwas konkret mit Camofox machen — z. B. einen Research-Workflow laufen lassen, die Container-Logs prüfen, oder ein fehlendes Compose-File neu anlegen?

Explanation:
<think>Ich habe jetzt alles wichtige zu Camofox gelesen:
- Skill-Doku
- Referenzen (research patterns, build dockerfile, RIS legal research, multi-source web research, abgb-364c)
- Python-Tool-Implementierung
- Camofox config.json
- E-Mail-Suche ergab 0 Treffer
- Session-Search ergab den historischen Setup-Kontext

Ich sollte jetzt eine Zusammenfassung liefern, was ich gefunden habe. Aber zuerst: Der Nutzer sagt "Lies alle smit Camofox aus". "smit" ist wahrscheinlich "Sachen mit" oder "Schriften mit" - also alles was mit Camofox zusammenhängt. Ich habe das gemacht.

Ich sollte eine klare, übersichtliche Zusammenfassung geben:
1. Was ist Camofox bei dir
2. Wo liegt was
3. Aktueller Status
4. Wichtige technische Details
5. Was ich nicht gefunden habe (z.B. keine E-Mails)

Dann auf die nächste Frage warten bzw. fragen, was er damit machen will.


Ich wollte das du Camofox für die Recherche zur Herz Ablation benutzt


Boss, ich nutze Camofox, um seriöse medizinische Quellen zur Herzablation zu recherchieren.

Explanation:
Der User will, dass ich Camofox benutze, um Recherche zur Herzablation durchzuführen. Das ist eine medizinische Recherche-Aufgabe, die Web-Recherche erfordert. Camofox ist lokal auf ZimaOS verfügbar (Port 9377).

Ich sollte:
1. Camofox prüfen/verwenden
2. Seriöse medizinische Quellen recherchieren
3. Risiken einer Herzablation (Katheterablation) zusammenfassen

Ich muss vorsichtig sein: Medizinische Informationen, aber klarstellen, dass ich kein Arzt bin und der behandelnde Arzt/Cardiologe die maßgebliche Quelle ist.

Ich sollte Camofox nutzen, um z.B.:
- PubMed/Google Scholar/DuckDuckGo zu durchsuchen
- Fachartikel zu lesen
- Deutsche Patientenaufklärungsseiten der DGK/Herzstiftung

Ich starte Camofox und mache eine Recherche. Da Camofox anscheinend läuft, probiere ich die Endpoints.


terminal

Output: {"ok":true,"engine":"camoufox","browserConnected":false,"browserRunning":false,"activeTabs":0,"activeSessions":0,"consecutiveFailures":0,"memory":{"rssMb":169,"heapUsedMb":108,"nativeMemMb":61}}


Explanation: <think>Der Server läuft, browserConnected ist false weil noch kein Tab geöffnet wurde. Das ist normal. Ich muss einen Tab öffnen, um zu recherchieren.

import urllib.request, json, time, urllib.parse

BASE = "http://127.0.0.1:9377"
USER_ID = "hermes_research"
SESSION_KEY = "cardiac_ablation"

# Open tab with DuckDuckGo search
search_url = "https://duckduckgo.com/?q=Herzablation+Herzkatheter+ Risiken+Komplikationen+Herzstiftung+Aerzteblatt"
req = urllib.request.Request(
    f"{BASE}/tabs",
    data=json.dumps({"userId": USER_ID, "sessionKey": SESSION_KEY, "url": search_url}).encode(),
    headers={"Content-Type": "application/json"},
    method="POST"
)
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
tab_id = data.get("tabId")
print("tab_id:", tab_id)

time.sleep(4)

# Get links
req2 = urllib.request.Request(f"{BASE}/tabs/{tab_id}/links?userId={USER_ID}&sessionKey={SESSION_KEY}")
links_data = json.loads(urllib.request.urlopen(req2, timeout=30).read())
print("\nLinks found:", len(links_data.get("links", [])))
for link in links_data.get("links", [])[:30]:
    print(link.get("text", "")[:80], "->", link.get("href", "")[:120])


Output:
tab_id: ab2472b9-bcef-4cf6-85d8-cea126e0710f

Links found: 50
DuckDuckGo ->
Protection. Privacy. Peace of mind. ->
All ->
Images ->
Videos ->
News ->
Maps ->
Shopping ->
Search Assist ->
Duck.ai ->
Learn More ->
Search Settings ->
Only include results for this site ->
Redo search without this site ->
Block this site from all results ->
 ->
https://www.praktischarzt.de › behandlung › ablation-herz ->
Ablation Herz: Gründe, Ablauf, Risiken & Lebenserwartung ->
Only include results for this site ->
Redo search without this site ->
Block this site from all results ->
 ->
https://herzstiftung.de › infos-zu-herzerkrankungen › herzrhythmusstoerungen › v ->
Vorhofflimmern: Behandlung mit Katheterablation | Herzstiftung ->
Only include results for this site ->
Redo search without this site ->
Block this site from all results ->
 ->
https://herzmedizin.de › fuer-patienten-und-interessierte › behandlungsmethoden  ->
Katheterablation bei Herzrhythmusstörungen - Herzmedizin.de ->