{
  "markdown": "# chatmux\n\nLocal-first personal chat data layer daemon. Connects IM platforms (v0.1: LINE) via child-process adapters, stores messages to JSONL + SQLite/FTS5, exposes MCP tools for AI clients.\n\n## The three repos\n\nchatmux is the core. Platforms plug in below it, consumers sit above it, and both sides of that\nboundary live in their own repos:\n\n| Repo | Role |\n|------|------|\n| **chatmux** (this one) | Core daemon: storage, safety rail, MCP server, LINE adapter |\n| [chatmux-adapter-telegram](https://github.com/echoedinvoker/chatmux-adapter-telegram) | Second platform adapter (Telegram, MTProto user session) |\n| [chat.nvim](https://github.com/echoedinvoker/chat.nvim) | Reference consumer: read and reply to chats inside Neovim |\n\nAdapters speak the [adapter protocol](docs/adapter-protocol.md); consumers speak\n[MCP](docs/mcp-interface.md). Either side can be replaced without touching the other.\n\n## Quickstart\n\n### 1. Install\n\n```bash\ngit clone https://github.com/echoedinvoker/chatmux.git\ncd chatmux\nbun install\n```\n\n### 2. Decide whether to connect an account yet\n\nWith no `adapters.json`, `bun run start` launches the **LINE adapter**, which means step 3 puts\nyour LINE account on the line — read [Account Risk Warning](#️-account-risk-warning) before you\nrun it. If you would rather look around first, start with no adapter at all:\n\n```bash\nmkdir -p ~/.local/share/chatmux\ncat > ~/.local/share/chatmux/adapters.json <<'JSON'\n{\n  \"adapters\": [],\n  \"mcp\": { \"port\": 7717 }\n}\nJSON\nbun run start\n```\n\nThe daemon comes up with storage and the full MCP interface — you can `initialize`, list tools,\nand read resources. There is simply no chat data behind them until an adapter is connected. Set\n`CHATMUX_DATA_DIR` to keep this trial run out of your real data directory:\n\n```bash\nCHATMUX_DATA_DIR=/tmp/chatmux-trial bun run start\n```\n\nEach entry in `adapters` takes `platform`, a `command` **string**, and an `args` **array**\n(plus optional `cwd` and `env`):\n\n```json\n{ \"platform\": \"telegram\", \"command\": \"python\", \"args\": [\"-m\", \"chatmux_adapter_telegram\"] }\n```\n\nFor Telegram, follow the setup in\n[chatmux-adapter-telegram](https://github.com/echoedinvoker/chatmux-adapter-telegram) — it has its\nown credentials and login flow, and does not involve LINE.\n\n### 3. First login (QR code)\n\n```bash\nbun run start\n# A QR code will appear in the terminal\n# Open LINE on your phone → open the QR scanner → scan\n#   iOS:     Home → the scan icon\n#   Android: Home → Add friends → QR code\n# After successful login, authToken is saved for future auto-login\n```\n\n### 4. Connect Claude Code\n\nRegister the daemon's MCP endpoint with Claude Code:\n\n```bash\nclaude mcp add --transport http chatmux http://127.0.0.1:7717/mcp\nclaude mcp list   # chatmux: ... - ✔ Connected\n```\n\nThe daemon listens on two transports at once: a **TCP port on `127.0.0.1`** (default `7717`) for\nstandard MCP clients like Claude Code, and a **unix socket** for same-host sidecar consumers like\n[chat.nvim](https://github.com/echoedinvoker/chat.nvim). Use the TCP url for Claude Code — the MCP\nspec only defines stdio and streamable HTTP transports, so no MCP client accepts a unix socket path.\n\nPort is configurable via `CHATMUX_MCP_PORT`, or `mcp.port` in `adapters.json`; set it to `0` to\ndisable the TCP listener. See [docs/mcp-interface.md](docs/mcp-interface.md).\n\n## Architecture\n\n```\nLINE adapter ←── stdio JSON-RPC ──→ core daemon ←── MCP Streamable HTTP ──→ Claude Code\n(Node+tsx)        (child process)    (Bun)         (127.0.0.1 TCP / unix)     (MCP client)\n                                     ├─ SafetyRail\n                                     ├─ Storage (JSONL → SQLite/FTS5)\n                                     ├─ Adapter Runner\n                                     └─ MCP Server\n```\n\n- **Core daemon** (Bun): central process managing storage, safety, and MCP server\n- **LINE adapter** (Node+tsx): child process connecting to LINE via IOSIPAD slot\n- **Storage**: JSONL append-only truth source + SQLite/FTS5 queryable view\n- **MCP server**: Streamable HTTP over TCP (standard MCP clients; loopback by default, settable for containers) + unix socket (same-host sidecars), 8 tools + 4 resources\n\n## MCP Tools\n\n| Tool | Description |\n|------|-------------|\n| `list_chats` | List chats with last message preview, search, pagination |\n| `read_messages` | Read messages from a chat, paginated by timestamp |\n| `read_events` | Tail the event log from an opaque cursor — resumable, survives backfill reordering, and re-delivers a message when it is edited or retracted |\n| `search_messages` | Full-text search (CJK supported via FTS5 trigram + LIKE fallback) |\n| `send_message` | Send message through SafetyRail (rate-limited, error-tracked) |\n| `get_media` | Local file path for a message's image or sticker; downloads and caches on first call |\n| `probe_latest` | Diagnostic, read-only: ask the adapter for a chat's newest N messages without landing them |\n| `get_status` | System status: adapter connection + storage stats |\n\n## MCP Resources\n\n| URI | Description |\n|-----|-------------|\n| `chat://chats` | All chat list |\n| `chat://chats/{id}/messages` | Recent messages for a chat |\n| `chat://chats/{id}/info` | Chat details with members |\n| `chat://status` | System status |\n\n## Writing a consumer\n\nCore exposes primitives, not policy. Anything that decides *what matters* — which chats\nare worth surfacing, where a notification goes, when to stay quiet — belongs in a\nconsumer, on the far side of the MCP boundary.\n\n[`examples/notifier/`](examples/notifier/) is a working reference: it tails the event\nlog with a persisted cursor and hands each message to a hook you fill in. Its\n`mcp-client.ts` uses raw `fetch` rather than the TypeScript SDK, so it doubles as a\nwire-protocol reference for consumers in any language.\n\n## systemd Service\n\n```bash\ncp config/chatmux.service ~/.config/systemd/user/\nsystemctl --user daemon-reload\nsystemctl --user enable --now chatmux\n```\n\nEdit `WorkingDirectory` to point at your clone before copying it.\n\n### How it comes back\n\nThe unit ships `Restart=always`, not `on-failure`. A chat backend is supposed to be there\nall day, and there are three ways it can stop being there — it crashes, something sends it\na signal, or it exits cleanly — of which `on-failure` only recovers from the first.\n`systemctl --user stop` still stops it: a stop you asked for is not a failure, under either\nsetting.\n\n> ⚠️ **If you are on an older unit with `Restart=on-failure`, `kill -TERM` will not bring\n> it back — and that is not a missing restart policy.** systemd counts SIGTERM, SIGHUP,\n> SIGINT and SIGPIPE as an intended stop, so `on-failure` leaves the service sitting in\n> `inactive` after any of them. Only `kill -9` (SIGKILL) counts as a failure there.\n>\n> With the `Restart=always` this unit now ships, **TERM comes back too** — measured\n> 2026-08-02: `kill -TERM $MainPID` moved `NRestarts` 1 → 2 and produced a new `MainPID`\n> within the 10s `RestartSec`. That makes TERM the useful test: `kill -9` restarts under\n> *either* setting, so it cannot tell you which one is in effect. If you want to confirm\n> `always` is live, send TERM and watch `systemctl --user show chatmux -p MainPID,NRestarts`\n> change.\n\n`StartLimitIntervalSec=300` / `StartLimitBurst=5` cap a crash loop: five starts inside five\nminutes and systemd stops trying, leaving the unit `failed` for you to look at rather than\nrestarting into the same wall forever. Clear it with `systemctl --user reset-failed chatmux`.\n\n## Containers\n\nA systemd user service is the intended way to run chatmux. If you want it in a\ncontainer instead, [`deploy/container/`](deploy/container/) is a reference that builds\nand answers — not an official image, and it runs **zero adapters**, because adapters\nhold logged-in sessions and a container you rebuild is the wrong home for those.\n\nThe one thing you cannot skip is `CHATMUX_MCP_HOST`. The daemon binds `127.0.0.1` by\ndefault, which inside a container is the container's own loopback — a published port\nthen maps to a socket nobody is listening on, and every connection is refused while the\nlogs look perfectly healthy. Read `deploy/container/README.md` before assuming your\nport mapping is broken.\n\n## Development\n\n```bash\nbun run dev     # Start with --watch (auto-reload)\nbun test        # Run all tests\nbun run start   # Start daemon\n```\n\nSee `docs/` for detailed architecture and protocol documentation.\n\n## Limitations\n\nKnown and accepted, with what would make each worth revisiting.\n\n- **The chat list caps at 1000, silently.** `chat://chats` is hard-coded to that limit. Consumers\n  can detect an overflow by comparing the `total` field against what arrived, so it will not bite\n  you without saying so. Worth raising once a vault approaches ~500 chats, or the first time that\n  completeness check fires.\n- **The JSONL log holds duplicate history.** Backfill re-ingested some messages many times over,\n  leaving the event log several times larger than the messages in it. This has stopped: recent\n  growth is almost entirely new distinct messages, and the worst-case duplicate count has been\n  frozen across repeated measurements. It is not a correctness problem — ingestion is idempotent\n  and the SQLite projection is unaffected — so the fix, if ever needed, is a one-off compaction\n  rather than a code change. Worth doing if the log passes ~500 MB, if the duplicate count starts\n  climbing again, or if cold start slows noticeably.\n- **Retractions in Telegram one-to-one chats are missed.** Group retractions land; direct ones do\n  not, because the adapter cannot recover the chat id for those events from its entity cache, and\n  core will not match a message on id alone — that ambiguity is exactly what the storage key was\n  widened to remove. So a message you retracted on your phone can stay visible here. Worth fixing\n  once the adapter can resolve the chat id itself, or as soon as retraction accuracy matters to a\n  consumer.\n- **Reactions are not stored at all.** The platforms send them; no layer reads them. Nothing in\n  core, the schema, or the MCP surface represents a reaction, so a consumer cannot show what a\n  phone shows. Worth building when reactions carry meaning you would otherwise miss — it is new\n  storage, not a display tweak.\n- **`read_receipt` is declared but never emitted.** The LINE adapter advertises the capability and\n  core is ready to ingest it; nothing constructs the event. Whether read state should reach a UI\n  at all is an open product question, not a pending bug — but the declaration is wrong today, so\n  do not branch on `supported_events` for this one. Worth fixing as soon as any consumer does\n  branch on it, or once that product question gets an answer.\n\n## ⚠️ Account Risk Warning\n\nThis project uses **@evex/linejs**, an unofficial LINE client library. Using unofficial APIs may violate LINE's Terms of Service. Your LINE account may be restricted, suspended, or permanently banned. **Use at your own risk.**\n\nThe IOSIPAD device slot is used to avoid interfering with your phone's LINE app, but LINE may change their multi-device policy at any time.\n\n## ⚠️ Legal Disclaimer\n\nThis software is provided \"as is\", without warranty of any kind. The author is not responsible for any consequences of using this software, including but not limited to account restrictions, data loss, or violations of third-party terms of service.\n\nThis is a personal tool for personal use. Do not use it for spam, harassment, unauthorized access to others' messages, or any illegal activity.\n\n## 🔒 Privacy Disclosure\n\nchatmux stores **decrypted message content in plaintext** on your local machine:\n- `~/.local/share/chatmux/events.jsonl` — all events (append-only)\n- `~/.local/share/chatmux/chatmux.db` — SQLite database with messages, contacts, chats\n- `~/.local/share/chatmux/adapters/line/auth.json` — LINE auth token\n- `~/.local/share/chatmux/adapters/line/storage.json` — E2EE key storage\n\nThese files are protected by filesystem permissions (owner-only). **Do not share these files.** The auth token grants full access to your LINE account. The E2EE keys can decrypt your messages.\n\nv0.1 does not encrypt the database. SQLCipher encryption is planned for v0.2.\n\n## License\n\nMIT\n",
  "bytes": 12235,
  "sha": "7d898abc4ab86be0fc8bf62a75ef06d73a758f253de3e4b170acfc93a58cbc2a",
  "repo_slug": "echoedinvoker/chatmux",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_echoedinvoker_chatmux_2e4cc51b/readme"
}