Skip to main content
Glama
Luissalet

EchoHoard

by Luissalet

Echo's Hoard

Everything you copy, kept on your own PC, searchable, with the things you copy often pinned. The app watches your clipboard in the background; an assistant reaches the same history through MCP, so it can answer "what was that URL I copied an hour ago", paste back the last thing you copied, or put new text on your clipboard for you.

Everything stays on the machine: SQLite for the clip history, PNG files for images, no accounts, no network.

Part of the Hoard family (see faustus-plugin.json).

What it does

  • Clips = whatever you copy: text, or an image stored as a PNG. With optional local OCR, image text becomes searchable and available to Faustus as clearly marked derived text; without OCR, the clip keeps a placeholder like "[imagen 1254×1254]". Each clip tracks kind, a one-line preview, the source app/window when known, first/last seen, how many times the same content was copied, pinned, label, tags.

  • Dedupe: copying the same content again bumps last_seen_at/times on the existing clip instead of creating a new one.

  • Kind detection: url, email, path, code, number, image, or plain text — from the content alone (see echo/detect.py).

  • Sensitive content (privacy first): a clip that looks like a password, an API key/token, a credit card number (Luhn-checked) or an IBAN is flagged sensitive and its real content is replaced by a placeholder like [oculto: contraseña] before it ever reaches the database — not hidden in the UI, never stored. The assistant never receives it, not even partially. A per-app exclusion list (ECHO_EXCLUDE_APPS, default KeePass,1Password,Bitwarden,keepassxc) skips capture entirely for password managers. A pause switch stops capture (UI + tool).

  • Retention: everything is kept for ECHO_RETENTION_DAYS (default 30) unless pinned; capped at ECHO_MAX_CLIPS (default 5000), oldest unpinned dropped first; images capped at ECHO_MAX_IMAGE_MB (default 200) total. A soft delete keeps a 24h undo window before the janitor purges it for good. Housekeeping runs at startup and every 10 minutes.

  • Search: FTS5 over text/label/tags/source window title, diacritics-insensitive, prefix match, filterable by kind/pinned/time/app.

  • Capture backend (echo/backends/): a small interface (read(), write(), foreground()) with three implementations — Windows (ctypes against user32/kernel32, no pywin32, polls GetClipboardSequenceNumber cheaply and only reads on change; retries a locked clipboard a few times, then skips that change rather than crashing), generic/cross-platform (pyperclip, polling text by comparing sha256; no images, no source app), and a fake backend used by tests. Selected by ECHO_BACKEND (auto/windows/generic/fake).

Related MCP server: mcp-clipboardify

Requirements

  • Windows 10/11 (also runs on Linux/macOS with the generic backend, without images or source-app detection), Python 3.11+ (3.13 fine), Node 22 only to build the client.

  • Python's sqlite3 must have FTS5 (the official Windows builds do). The app fails loudly at startup otherwise.

Install and run (Windows)

git clone <this repo> echo-hoard
cd echo-hoard
python -m venv venv
venv\Scripts\pip install -r requirements.txt
npm install
npm run build
venv\Scripts\python -m echo

Open http://127.0.0.1:5188, go to Historial to see and search what you've copied, Fijados for your pinned clips, and Estado to pause/resume watching and purge old clips.

  • python scripts/launch.py starts the app on a free port and opens the browser.

  • python scripts/dev.py runs uvicorn --reload + the Vite dev server (proxying /api).

Configuration (environment)

Variable

Default

Meaning

ECHO_PORT / PORT

5188

Preferred port; PORT_STRICT=1 pins it, otherwise the first free port from there.

ECHO_DATA_DIR

<repo>/data

Database (echo-hoard.db), images/, mcp-token.

ECHO_BACKEND

auto

auto | windows | generic | fake.

ECHO_AUTOSTART

1

0 disables starting the watcher automatically with the app.

ECHO_RETENTION_DAYS

30

Unpinned clips older than this are purged; 0 keeps forever.

ECHO_MAX_CLIPS

5000

Cap on unpinned clips; oldest dropped first.

ECHO_MAX_IMAGE_MB

200

Cap on total image storage; oldest images dropped first.

ECHO_OCR_ENABLED

0

Set to 1 to read the text of newly copied images. The family's OCR engine (Kafka's Hoard, through the hub: fam_docs.ocr_image from Hoard Link) is used when it is running, otherwise a local Tesseract (PATH, ECHO_TESSERACT, or the standard Windows install folders). Images are still saved if neither is available or it fails.

ECHO_OCR_LANG

spa+eng

Language(s) as Tesseract spells them, for example eng+spa; the first one is the language asked of the family service. A missing pack makes the local engine retry with eng.

ECHO_EXCLUDE_APPS

KeePass,1Password,Bitwarden,keepassxc

Comma-separated process names never captured.

ECHO_ALLOWED_HOSTS

Extra host names accepted behind a tunnel (see below).

Access from your phone (behind a tunnel)

The server binds 127.0.0.1 and only answers requests whose Host is localhost, 127.0.0.1 or [::1]. To reach it from your phone through a tunnel that fronts the app, list the extra host names in ECHO_ALLOWED_HOSTS, comma-separated, exact names or *.suffix: ECHO_ALLOWED_HOSTS=my-pc.example,*.ts.net. Port and letter case are ignored, and the Origin of API calls must resolve to one of those hosts too (any scheme or port). Cross-site fetches are still refused; opening the app from another page (a link, a bookmarklet, the share sheet) is a normal navigation and works.

Once opened through the tunnel, the browser offers to install it (PWA).

API

All JSON; errors are { "error": "..." } (plus a machine-readable code, and issues for bad input). Agent-tool answers are capped at about 20 KB (truncated says what was cut). The request guard, the error envelope, the PWA files, the single-page-app server (index.html is never cached), the MCP bridge, the agent routes, the database class and the search-query builder are the shared Hoard Link commons (echo/hoard_link/), not code of this app. A second python -m echo reports the running instance and exits without touching the token or the database; the token in data/mcp-token is created once and stays.

  • GET /api/health → { service: "echo-hoard", version, dataDirConfigured, hoard_link } (the shared hoard_link.service.health_router); GET /api/status

  • POST /api/pause, POST /api/resume

  • GET /api/clips?kind&pinned&since&until&app&limit&offset, POST /api/clips {text} (sets the clipboard AND stores it), GET/PATCH/DELETE /api/clips/{id}, POST /api/clips/{id}/restore, POST /api/clips/{id}/copy (put it back on the clipboard), GET /api/clips/{id}/image

  • DELETE /api/clips?before=<iso> (bulk purge, irreversible; UI confirms)

  • GET /api/search?q&kind&pinned&since&until&app&limit

  • GET /api/agent/tools (catalog + instructions), POST /api/agent/call (Bearer token from data/mcp-token)

Faustus assistant

Once the shared Hoard Link launcher is installed, the app shows the family's Faustus panel (a floating button). The conversation, the model and the tools live in Faustus; this app only tells the panel what is open.

  • Context per view. The view reports the page (history, pinned, status), the active filters (kind, search text) and the ids of the clips on screen (up to 20). A clip whose label is being edited is sent as kind clip with its real id, clip kind, source app, last seen, times, pinned, label, tags, size and a preview of at most 200 characters. The full clip text is never sent, and sensitive or deleted clips are left out entirely, also from the visible ids. Nothing is sent as an object where no clip is being edited.

  • Unsaved fields. The panel asks for them with the echo:context-request window event. The open label editor (clip-label) answers with the label typed so far and a dirty flag.

  • Refresh. When the assistant finishes a turn the open view reloads in place (hoard:copilot-turn-end / window.hoardCopilot.onTurnEnd, merged into one reload). A label you are typing keeps your text; an untouched one follows the new value.

  • Saving. The label editor sends only the changed field in PATCH /api/clips/{id}, which merges just the fields it carries (null and unknown fields are ignored, an empty label clears it).

  • Tests. node --test client/copilot-context.test.mjs and venv\Scripts\python -m pytest -q tests/test_api.py.

MCP tools

mcp_server.py is a stdio bridge: it fetches the tool list from the running app and proxies every call to POST /api/agent/call with the token from <DATA_DIR>/mcp-token. It never opens the database. Env: ECHO_URL, ECHO_TOKEN_FILE (or ECHO_TOKEN).

Tool

What it does

clip_recent

The most recently copied clips. Sensitive ones come back as [oculto].

clip_search

Full-text search over the clipboard history.

clip_get

The full text of one clip (paginated); refuses sensitive clips outright.

clip_bundle

Read up to 20 selected clips together, in order, with source and date for a comparison or summary in Faustus.

clip_set

Put text on the clipboard and store it as a clip (write, idempotent by content).

clip_copy

Put an existing clip back on the clipboard (write; refuses sensitive unless allow_sensitive, and even then never returns the content).

clip_pin

Pin/unpin and set label/tags (write).

clip_delete

Soft-delete a clip (write, destructive; 24h undo window).

clip_status

Whether Echo is watching, counts, retention config.

clip_capture

Pause/resume watching (write).

The shipped instructions tell the assistant: this is the user's own data, quote it only when asked; a sensitive clip is never returned, not even partially, and never ask the user to unhide one; clip_set changes what the user will paste next, so say what was put there; prefer clip_search with a time window over listing everything; never read the data folder or database directly.

Tests

venv\Scripts\python -m pytest -q

Covers kind detection (URL/email/path/code/number/text table), sensitive detection (passwords by window title and by length, API key prefixes, JWT-like strings, Luhn-checked card numbers, IBAN checksum, and the negatives: a normal sentence, a plain 16-char word, a URL), dedupe bumping times, retention/cap/janitor with an injected clock, FTS with accents, pin/label/tags, soft delete + restore + purge, the fake backend driving the watcher end to end (push three contents, one a duplicate, → two clips, one with times: 2), pause/resume, the HTTP API, agent tools through /api/agent/call with the token (including the sensitive refusals), the request guard, the PWA endpoints, and a subprocess end-to-end test through the MCP stdio client. No test touches the real clipboard.

License

MIT — Luissalet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Gives AI assistants direct access to macOS clipboard - letting Claude copy generated code, text, or files straight to your clipboard for pasting anywhere on Mac.
    251
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides clipboard access tools (get and set) for AI assistants via the Model Context Protocol, enabling seamless clipboard operations across platforms.
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to read and write the system clipboard in native app formats such as Slack, Mural, Figma, and Outlook without API keys. It decodes clipboard content into structured data and generates app-ready clipboard payloads.
    MIT