No description
  • Rust 99.3%
  • Shell 0.6%
Find a file
microchipster 426b64b034
Some checks are pending
Binaries / package (aarch64, macos-latest, apple-darwin) (push) Waiting to run
Binaries / package (aarch64, ubuntu-latest, unknown-linux-gnu) (push) Waiting to run
Binaries / package (x86_64, macos-latest, apple-darwin) (push) Waiting to run
Binaries / package (x86_64, ubuntu-latest, unknown-linux-musl) (push) Waiting to run
Binaries / package (x86_64, windows-latest, pc-windows-msvc) (push) Waiting to run
CI / test (macos-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / Coverage report (push) Waiting to run
CI / Flake checks ❄️ (push) Waiting to run
CI / Flake checks ❄️-1 (push) Waiting to run
sending queue during initial sync
2026-06-13 18:01:45 -07:00
.github completions, coverage 2026-05-09 17:05:14 -07:00
crates sending queue during initial sync 2026-06-13 18:01:45 -07:00
docs sending queue during initial sync 2026-06-13 18:01:45 -07:00
scripts real server scenarios 2026-05-12 10:53:11 -07:00
src issue 8 2026-06-10 22:34:14 -07:00
.envrc Enable direnv for Nix flakes (#183) 2023-12-19 00:53:17 +00:00
.gitattributes Add an icon for iamb (#232) 2024-03-28 16:20:27 +00:00
.gitignore pending send 2026-05-18 11:15:07 -07:00
.rustfmt.toml Fix rustfmt warning (#523) 2025-10-25 12:55:23 -07:00
AGENTS.md issues work 2026-05-12 21:57:06 -07:00
archive-issues.log fix drafts 2026-04-07 00:24:50 -07:00
backlog-methodology.md RAM introspection 2026-04-23 11:50:44 -07:00
backlog.md checkpoint 2026-04-11 01:40:00 -07:00
build.rs verification recovery tutorial 2026-04-19 05:28:08 -07:00
build.sh real server scenarios 2026-05-12 10:53:11 -07:00
Cargo.lock backlog update 2026-06-09 13:08:49 -07:00
Cargo.toml improve filtering on leave command 2026-06-08 17:12:15 -07:00
clippy.toml refactor for cli commands with ipc to tui 2026-01-23 11:41:29 -08:00
config.example.toml issue 8 2026-06-10 22:34:14 -07:00
contain.sh pinning and joining 2026-03-24 16:08:33 -07:00
CONTRIBUTING.md Support sending and displaying typing notifications (#9) 2023-01-03 13:57:28 -08:00
flake.lock Use cargo crane in Nix flake and set up cachix action (#539) 2025-10-25 22:44:19 +00:00
flake.nix backlog work 2026-04-30 15:45:48 -07:00
generic-prompt.txt issue 617 raw view of formatted messages 2026-06-10 15:55:57 -07:00
iamb.desktop Add an icon for iamb (#232) 2024-03-28 16:20:27 +00:00
LICENSE Fix LICENSE file (#274) 2024-04-24 06:59:00 +00:00
PACKAGING.md packaging 2026-05-01 19:06:45 -07:00
README.md backlog 2026-06-13 02:33:55 -07:00
ROADMAP.md roadmap 2026-03-24 16:08:25 -07:00
rust-toolchain.toml sending and backlog 2026-03-30 11:37:42 -07:00

Repository

About

iamb is a terminal Matrix client with Vim-style interaction, a full-screen TUI, and a growing automation-oriented CLI.

It supports:

  • E2EE, threads, spaces, replies, edits, reactions, read receipts, and mentions
  • Room, DM, invite, unread, and session workflows from both the UI and CLI
  • Archive/listen/download tooling for history, attachments, external media, hooks, and postback flows
  • Custom keybindings, multiple profiles, image previews, image reactions, notifications, and configurable sorting

This repository is maintained at https://codeberg.org/microchipster/iamb.

Documentation

Prefer the built-in help first: iamb --help and iamb <command> --help stay closest to the code.

Use this README for project overview, installation, and a few higher-level workflows. Use docs/iamb.1 and docs/iamb.5 for concise man-page style references.

IPC and daemon model

iamb can reuse an authenticated Matrix client over IPC so CLI commands do not need to perform a fresh login or sync every time.

  • The UI and iamb daemon expose a per-profile Unix socket unless --no-ipc is set.
  • CLI commands first try that socket; if a daemon is reachable, the command is executed there and reuses the warm session.
  • If no daemon is available, the command falls back to local execution.
  • Socket lookup order is $XDG_RUNTIME_DIR/iamb/<profile>.sock, then $TMPDIR/iamb/<profile>.sock, then /tmp/iamb/<profile>.sock.
  • --ipc-socket <PATH> overrides the resolved socket path for both client and server.

This makes one-off commands much faster in normal use and keeps helpfully stateful operations, such as room lookups and uploads, consistent with the running UI session.

Useful CLI workflows

The CLI is meant for scripting, maintenance, and headless usage, not just account bootstrap.

  • iamb rooms <query> fuzzy-searches joined rooms; use . to list all rooms and --user <USER> to require specific members.
  • iamb rooms, iamb public-rooms, iamb users, and iamb status all support --format json when you want a stable automation surface instead of text output.
  • Human-readable CLI reports now default to aligned tables and headings, while prompts stay off stdout so pipes and --format json output remain machine-safe. If a command needs confirmation or a password, rerun it interactively or provide the value via flags/stdin when the command supports that path.
  • iamb spaces <query> fuzzy-searches joined spaces; iamb spaces tree prints the joined space tree as text or JSON; iamb spaces organize opens a staged CLI organizer for moving rooms between spaces or Orphans; and iamb spaces merge --suggestions / iamb spaces merge <source> [--into <dest>] [--apply] review or apply duplicate-space consolidation plans.
  • iamb room info <room> prints room diagnostics; iamb room versions lists locally known rooms by version in TSV form; iamb room threads <room> lists thread roots; iamb room uploads <room> lists recent uploads.
  • iamb dm @user:server creates or reuses a direct message room, and iamb public-rooms <query> / iamb public-rooms --server <server> <query> / iamb public-rooms --known-servers <query> / iamb public-rooms --remembered-servers / iamb users complete <prefix> / iamb users discoverable <query> / iamb users shared <query> / iamb users rooms <@user:server> / iamb users members <!room:server> / iamb users whois <@user:server> expose directory and joined-room user queries without opening the UI. Use iamb users complete @ho for shell-friendly user-id suggestions when you only remember the start of an MXID or display name. Omit the discoverable-user query, or pass * / .*, to ask the homeserver for all discoverable users it is willing to return; if that endpoint returns nothing, iamb falls back to locally known shared-room members and reports that limitation. Passing a full MXID such as @user:server also tries the Matrix profile endpoint, which can verify exact users that are not prefix-discoverable. Explicit public-room --server queries are now remembered per profile, can be inspected without a network query, and are folded into later --known-servers runs. Shared-user queries now include cached presence when it is available locally, and users rooms includes per-room membership, power level, and a conservative can_ban power-level check for roomless moderation triage.
  • iamb invites lists pending invites; add --accept, --reject, or --picker for bulk handling.
  • iamb unreads lists unread rooms/messages and can clear them with public receipts, private receipts, or no receipt.
  • unread_suppression config rules can keep bridge/system noise visible in timelines without letting it mark rooms unread; mentions are preserved by default.
  • iamb mark-unread <room-id>... marks rooms unread locally; --picker opens an interactive selector.
  • iamb send, iamb upload, iamb create-room, iamb room-setting, iamb upgrade, iamb leave, iamb sessions, iamb setup, iamb reset-secure-backup, and iamb change-password cover common operational tasks without opening the TUI. Piping text into iamb send --room <room> sends that text as the message body when no explicit body, attachment, or reaction was provided; use --stdin when you want to force stdin or combine it with other inputs. Set message_send_format = "plain" or "html" to change the default send markup, while /markdown, /plain, and /html still override one message. CLI reactions sent with iamb send --react-to ... --text smile accept emoji shortcodes; add --reaction-literal to preserve an exact reaction key.
  • iamb media, iamb archive, iamb listen, iamb download, iamb postback, iamb search, and iamb redact support archival, media, and cleanup workflows.

TUI quick reference

The interactive : command set is broader than the short examples in this README. The most useful discovery-oriented commands are:

Press <Tab> in the command bar to complete command names, common subcommands, Matrix IDs, room aliases, file paths, and emoji shortcodes in the main flows such as :invite send, :join, :room ..., :space child ..., :upload, and :react. Command and search history are now persisted per profile too, so pressing Up/Down in the command bar can recall recent entries even after restart. In the message composer, typing a room alias like #room:server now sends a matrix.to permalink, and typing # then <Tab> inserts a known room alias without leaving the keyboard.

  • :rooms, :chats, and :dms accept optional sort modes, while :sort <mode> changes the active list ordering; the footer shows the current sort label.
  • :join <room> accepts room IDs, aliases, and matrix.to links; use :peek <room> to inspect a room before switching into a joined timeline. Shared matrix.to room/event links opened from messages now reuse the same internal join/navigation path instead of always launching a browser, and :open on Matrix location messages opens their geo: URI through the configured opener.
  • :public-rooms [query] and :users discoverable [query] [limit] / :users shared [query] / :users rooms <@user:server> / :users members <!room:server> / :users whois <@user:server> expose the public-room and user-query flows inside the TUI too. The command bar completes known user IDs by MXID prefix or display name for :dm, :invite send, :kick <@user:server>, :users rooms, :users whois, :users shared, and room moderation/role commands. Shared-user output and member lists now surface cached presence when it is available locally, and :kick works contextually from room scrollback or :members with confirmation while :users rooms/:users whois include roomless moderation context for shared rooms.
  • :info opens the room report, and :pins / :pins open <n> let you browse pinned events and jump back into the timeline.
  • :source opens an Element-style source view for the selected message, while :source PATH or :source save PATH writes the current/decrypted source JSON to disk; use :source! ... to overwrite an existing file.
  • :raw on / :raw off temporarily forces the selected message to render as raw body text instead of the configured HTML/plain renderer.
  • iamb message <room> <event-id> prints pretty event source JSON for scripts; add --out ./event.json to save it from the invoking CLI process, and --force to overwrite an existing file.
  • :markdown on enables one-message markdown authoring explicitly, while :markdown off reuses the plain-text send path for the next message only. The message_send_format setting changes the normal composer default to markdown, plain, or html; inline /markdown, /plain, and /html still override one message. Inline code and fenced code blocks now use stronger code styling so snippets stay visible.
  • :confetti and :snowfall now match the existing slash-command effects for the next message, so you can trigger those Matrix client-side visuals from the : command bar too.
  • :room show, :room avatar ..., :room encryption ..., :room retention ..., :room role ..., :room threshold ..., and :room admin show cover the room inspection and moderation surfaces that also exist under iamb room-setting, while :room mentions next|prev|previous and :room me last / :room own last add room-local navigation helpers in the focused room.
  • :spaces! opens the hierarchical space tree, :space tree move starts a staged move there, and :space tree review / :space tree apply let you inspect and commit the batch before mutating server state.
  • :help, :drafts, :status, :log, :reload, :initial-sync, :setup, :reset-secure-backup, and :perf are the main recovery/inspection commands when the UI needs a nudge or a quick health check. Drafts survive restart, rooms with saved drafts are marked with 📝 in list views, and :drafts opens the current saved-draft list. Use :help for an in-client quick manual and :log to see the active debug log path or flush it before grabbing logs for support.
  • The :help keys page and scrollback views include Vim-style scroll placements such as z+ and z^, which move the current line just outside the viewport.

If you need the full command inventory in one place, prefer docs/iamb.1 or man ./docs/iamb.1.

iamb create-room provides a straightforward way to spin up a room with name/topic, directory visibility, invites, encryption, guest access, and join/history rule tweaks without dropping into the UI. Example:

iamb create-room --name "Project Workspace" --topic "Planning notes" \
  --visibility private --preset private-chat --invite @alice:example.com --invite @bob:example.com \
  --encrypted --guest-access --join-rule invite --history-visibility shared

iamb upgrade <room> upgrades the named joined room to the latest stable Matrix room version advertised by the homeserver. The command automatically restores the saved session or reuses the IPC daemon client, verifies m.room.tombstone permissions, calls /rooms/<room>/upgrade, and prints the replacement room ID so you can immediately follow the successor chain (or fall back to :upgrade follow after the tombstone is emitted). Rooms already running the preferred version simply report their current version. In the interactive UI you can run :upgrade latest (alias :upgrade current) to perform the same flow, show an in-place InfoMessage, and jump into the upgraded room while leaving the old room tombstoned so others can follow with :upgrade follow. :upgrade follow now makes the expectation explicit: it switches to the replacement room immediately when you are already joined, or joins it first if needed. Use :upgrade old to view the old version of an upgraded room. iamb room versions now adds predecessor/successor status and action columns so you can review which upgrade relationships are already joined locally before deciding what to follow next, and --predecessor-action / --successor-action let you filter that review output down to the rooms that still need a specific follow action.

The CLI flags map directly to the Matrix room creation API (preset, visibility, room_alias_name, join_rule, history_visibility, initial_state). Any of those settings can later be adjusted via iamb room-setting <room-id>.

Shell completions

iamb now ships with generated completions for every flag and subcommand. The scripts are emitted from the live clap command tree, so they track the real CLI surface instead of a hand-maintained copy. Install them by generating the script and dropping it in the right directory for your shell:

  • bash: mkdir -p ~/.config/iamb && iamb --completions bash > ~/.config/iamb/iamb.bash-completion. Add source ~/.config/iamb/iamb.bash-completion to your ~/.bashrc or drop the file into /etc/bash_completion.d/ if you prefer a system-wide install.
  • zsh: mkdir -p ~/.config/iamb && iamb --completions zsh > ~/.config/iamb/_iamb. Ensure ~/.config/iamb is on your fpath (e.g., fpath=(~/.config/iamb $fpath) inside ~/.zshrc), then run autoload -U compinit && compinit (or restart the shell) so the new completion becomes available.
  • fish: mkdir -p ~/.config/fish/completions && iamb --completions fish > ~/.config/fish/completions/iamb.fish. The shell auto-loads ~/.config/fish/completions/*.fish, so either restart or source ~/.config/fish/completions/iamb.fish.

Regenerate the outputs (iamb --completions <shell>) whenever the CLI gains new flags or subcommands.

Development Coverage

Use scripts/coverage.sh for quick local coverage summaries while you are iterating, and scripts/coverage-report.sh for the routine artifact bundle that CI and ./build.sh now publish locally.

Quick local summary:

scripts/coverage.sh
scripts/coverage.sh --package iamb-core
scripts/coverage.sh --html

The default run covers deterministic workspace tests only. Ignored real-server tests are included only when explicitly requested with scripts/coverage.sh --include-real-server, using IAMB_TEST_CONFIG or ~/.config/iamb/test.toml.

Routine artifact bundle:

scripts/coverage-report.sh
scripts/coverage-report.sh --package iamb-core
scripts/coverage-report.sh --include-real-server

That routine path writes target/llvm-cov/summary.json, summary.md, gap-audit.md, lcov.info, and an inspectable HTML report under target/llvm-cov/html/. The default mode is explicitly deterministic-only. Real-server coverage remains opt-in and separately labeled.

GitHub Actions now also runs the deterministic scripts/coverage-report.sh path on Ubuntu and uploads the resulting artifact bundle for review, keeping coverage visible without turning it into a hard percentage gate.

./build.sh now runs that same deterministic coverage-report path by default before the release-profile test lane. Use ./build.sh --skip-coverage-tests or IAMB_SKIP_COVERAGE_TESTS=1 ./build.sh only when you intentionally want a faster local bypass.

Change-Aware Local Test Selection

Use scripts/test-changes.sh for a conservative local test suggestion based on changed paths. It is intentionally fail-open: docs-only changes select no cargo test command, package-scoped crate changes widen to known downstream packages, shared/tooling/root changes widen to cargo test --workspace --locked or all the way to ./build.sh.

Examples:

scripts/test-changes.sh --dry-run
scripts/test-changes.sh --base origin/main --dry-run
scripts/test-changes.sh --path crates/iamb-tui/src/main_application.rs --dry-run

This helper is only for fast local confidence while iterating. ./build.sh remains the canonical broad verification path before merge-quality handoff.

Real-Server Tests

Use scripts/test-real-server.sh to run opt-in integration tests against a real Matrix homeserver with normal test users. Add a local [test_server] section in ~/.config/iamb/test.toml, or set IAMB_TEST_CONFIG=/path/to/config.toml. Password environment variables and device names are inferred from each user key, such as alice -> IAMB_TEST_ALICE_PASSWORD and iamb-test-alice; password_env, device_name, and a direct local-only password remain optional overrides. Environment variables take precedence over direct passwords when both are present. A valid test.toml is enough to opt into real-server tests; the wrapper exports IAMB_TEST_SERVER=1 for Cargo. Admin reset tests are intentionally separate and require IAMB_TEST_ADMIN=1 plus IAMB_TEST_ADMIN_TOKEN.

Configuration

You can create a basic configuration in $CONFIG_DIR/iamb/config.toml that looks like:

[profiles."example.com"]
user_id = "@user:example.com"

If you homeserver is located on a different domain than the server part of the user_id and you don't have a /.well-known entry, then you can explicitly specify the homeserver URL to use:

[profiles."example.com"]
url = "https://example.com"
user_id = "@user:example.com"

If a homeserver exposes Matrix endpoints on a different host than the web UI you use to sign in (for example, chat.fedoraproject.org versus the m.homeserver base_url of https://fedoraproject.ems.host, or utwente.io versus https://matrix.utwente.io), retrieve /.well-known/matrix/client yourself and set profiles."name".url to the reported m.homeserver.base_url. The CLI relies on that explicit url to know which host actually answers the Matrix API, so pointing it at the shiny UI host will forever hand the SDK 404 HTML errors instead of JSON if the API lives somewhere else.

To automatically log in without interactive prompts, point password_file at a local file containing the account password (the file is read every startup). Keep the file permission-restricted so only the owning user can read it.

Startup windows and layout

Startup behavior comes from the layout section plus settings.default_room:

  • layout.style = "restore" restores the last saved layout
  • layout.style = "new" opens settings.default_room when set, otherwise the welcome page
  • layout.style = "config" opens the exact tabs and splits you describe under [[layout.tabs]]

settings.default_room can point at a room, alias, DM user, or a special iamb window URI such as iamb://welcome, iamb://rooms, iamb://chats, or iamb://dms. If the configured target cannot be opened, iamb falls back to the welcome page.

Split defaults

If you prefer cursor-file opens to use a vertical split, set:

[settings]
default_split = "vertical"

That changes <C-W>f and <C-W><C-F> from the default horizontal split to a vertical one.

To inspect the fully resolved configuration that the active profile is actually using, run iamb config current --format json or iamb config current --format toml.

Extra sender lines

If long sender names are making the room gutter feel cramped, you can move senders onto their own line above the message body:

[settings]
message_sender_extra_line = true

That gives the message body the full terminal width and makes narrow or long-name rooms easier to scan without shrinking the main text area.

Raw message bodies

If you want to see the raw Matrix message body instead of rendered formatted_body HTML, disable rich message rendering:

[settings]
message_html_display = false

This keeps the underlying event content intact but shows the literal body text in scrollback, thread previews, and other message views.

Watched keywords

If you want specific words or phrases to stand out in message bodies, add them to a watched keyword list:

[settings]
watched_keywords = ["matrix", "urgent"]

The keywords are highlighted case-insensitively wherever message bodies are rendered, and you can set them either globally or on a per-profile basis.

Example fixed startup layout:

[settings]
default_room = "iamb://welcome"

[layout]
style = "config"

[[layout.tabs]]
window = "iamb://rooms"

[[layout.tabs]]
window = "iamb://chats"

[[layout.tabs]]
window = "iamb://welcome"

[[layout.tabs]]
split = [
    { window = "#iamb-users:0x.badd.cafe" },
    { window = "#iamb-dev:0x.badd.cafe" },
]

Low-memory profile

For very large or high-activity accounts, iamb now performs much better than it used to, but you can still trade some cache depth and prefetching for lower RAM usage.

The most useful knobs are:

  • sliding_sync: keep enabled unless you are debugging sync behavior
  • sliding_sync_active_timeline_limit: lower the active-room timeline depth
  • sliding_sync_all_timeline_limit: lower the background-room timeline depth
  • event_cache: keep enabled unless you are isolating event-cache problems
  • event_cache_clear_interval: lower values clear cached event data more often, in minutes after an initial warmup period capped at 5 minutes
  • log_rotate_size: cap each active log file before rotating it, using values like 1G or 100M
  • log_rotate_policy: choose whether rotated logs are kept in full (backup), kept as a warning/error-focused minified backup (backup-minified), or discarded (delete)
  • prefetch_recent_minutes: lower values reduce how much recent history is prefetched
  • key_sequence_timeout_ms: resolve ambiguous multi-key bindings after this many milliseconds; set to 0 to keep waiting indefinitely

Example low-memory profile:

[settings]
sliding_sync = true
sliding_sync_active_timeline_limit = 10
sliding_sync_all_timeline_limit = 2
event_cache = true
event_cache_clear_interval = 25
prefetch_recent_minutes = 30

Tradeoffs:

  • lower event_cache_clear_interval can reduce memory growth, but may increase cache churn because the SDK event cache is cleared after at most 5 minutes, then again on that many minutes of uptime
  • lower sliding-sync timeline limits reduce cached per-room timeline depth, which helps on large accounts but may slightly reduce how much recent history is instantly warm
  • lower prefetch_recent_minutes can reduce RAM significantly on busy accounts, but may make upward scrolling or unread catch-up feel a bit less warm
  • disabling sliding_sync or event_cache is not recommended as a first step; prefer tuning the two values above first

Responsiveness checks

For large or busy accounts, use :perf in the TUI after a burst of activity to inspect the current responsiveness snapshot. The report includes p50/p95/p99 input-to-draw latency, frame timing, action/lock timing, worker-event counters, and recent slow events.

If a room report or pinned-event view looks incomplete after first opening it, keep the room focused for a moment or run :initial-sync to force a catch-up pass.

Empty room recovery

If a room suddenly shows up as Empty Room after an upgrade or stale sync, iamb usually is not missing a dedicated feature; it is waiting for fresh room state to arrive again.

Practical recovery steps, in order:

  • keep the room focused briefly, then try :initial-sync to request a fresh catch-up pass for the current view
  • if the UI still looks stale, run :reload to reload configuration and related runtime state; invalid config is now rejected instead of being normalized, so fix any validation error first, then revisit the room
  • if you can safely do so, sending a new message in the room often forces the room to become visibly active again
  • profile changes made from another Matrix client, such as updating your display name or avatar, can also cause many rooms to refresh their visible state

Important caveats:

  • iamb does not currently promise a one-shot “repair this room” button for every stale-room case, so do not assume :reload or :initial-sync can reconstruct history the homeserver has not resent yet
  • avoid manually deleting iamb profile data or cache directories unless you understand the impact on local state and encryption material; export keys first with iamb keys export ... if you are considering destructive cleanup
  • :retrydecrypt / :clearcache help with undecryptable-message recovery, not general room-state resync

When validating responsiveness-sensitive changes locally, a good stress pass is:

  • let sync settle on a large account for 30-60 seconds
  • switch between busy rooms and list views
  • scroll media-rich history upward quickly
  • send a text message plus a file or clipboard image upload
  • run :perf and confirm p95 input->draw stays under 50ms and dropped worker events stay at 0

Archival utilities

listen

iamb listen streams timeline events into an archive directory suitable for later processing and inspection. It writes per-room NDJSON event files under events/, stores downloaded attachments and cached assets under media/, and keeps bookkeeping and job metadata in meta/ (for example synced markers, postback and failed URL logs). The listener collects in-room attachments and classifies external URLs, queuing jobs to fetch external media when appropriate.

Use --only-print-changes when you want long-running listener output to stay focused on new events and material activity while still showing warnings, errors, and the final summary.

External failures are appended to meta/failed-urls.ndjson; successful downloads that should be posted back are queued in meta/postback-queue.ndjson.

Use iamb media-archive-output-path <event-id> --out ./archive to ask iamb for the canonical path(s) where media from an archived event should live. Add --room <room-id> to avoid scanning every event log, or --url <url> to print only the target path for one external URL in that event. This is intended for out-of-tree download helpers that want to place files in iamb's archive layout without adding downloader policy to iamb itself.

Usage example:

cargo run --bin iamb -- -P default listen --out ./archive

Custom bot commands (--command)

iamb listen can hand off each new message to an external helper by passing --command <program> alongside the rest of the listener invocation. The listener creates a small bot workspace inside the archive root (meta/bot/) and invokes your program like this for every decoded burst:

<program> <thread_id> <archive-root>/meta/bot/prompt.ndjson \
  --attachment <archive-root>/media/<room>/<file> --attachment <archive-root>/media/<room>/<file2> ...

thread_id is a deterministic alphanumeric identifier derived from the room plus the thread root event, so your helper can keep per-thread state in sync while it receives just the latest prompt. --prompt points at meta/bot/prompt.ndjson, which is newline-delimited JSON because the listener now waits roughly 2seconds after the first event before invoking your helper. This allows quick sequences (text after an image, multiple replies, etc.) to be bundled into one invocation. The shape of each entry is the same as before:

You can also pass --exclude-room <room_id> (repeatable) to keep specific rooms out of the listener and any bot invocations; persist the same list via archive.listener.excluded_rooms in your profile when you need the exclusion to survive across runs. Use --accept-invite-whitelist/--accept-invite-blacklist together with the archive.listener.invites.whitelist/archive.listener.invites.blacklist config arrays to teach the listener which invites should be accepted (room IDs and display names are matched against the supplied regexes).

{
  "room_id": "!room:example.com",
  "event_id": "$event:example.com",
  "sender": "@alice:example.com",
  "type": "m.room.message",
  "origin_server_ts": 1700000000000,
  "thread_root": "$thread:example.com",
  "content": {
    "msgtype": "m.text",
    "body": "Hello world"
  }
}

thread_root now always contains the root event that the conversation is happening in (it is only null for the very first message, which also becomes the thread root). The history files live under meta/bot/history and are named after the room and the percent-encoded thread root (<room>__main.ndjson vs <room>__thread__<event>.ndjson). Each history file is newline-delimited JSON that mixes previous incoming events and the replies that the helper already sent; the listener keeps that file trimmed to the most recent 512 lines so you can treat it as a rolling conversational context.

When a Matrix attachment is part of the batch, the helper receives one --attachment argument for each downloaded file (e.g. media/<room>/<file>). Any caption on that attachment already appears in the matching JSON entry's content.body; if there is no caption, the JSON line may be empty while the file is still supplied via --attachment. Your helper should parse every line in --prompt (it can contain multiple events) and inspect every --attachment path it receives, then write the text that should be sent back. Empty stdout or a non-zero exit code suppresses a reply, so send logs and diagnostics to stderr.

The snippet below illustrates a tiny standalone helper that reads the NDJSON prompt, enumerates the rolling history, reports the attachment paths, and prints a brief summary. It does not need to depend on the rest of iamb and can live in its own crate.

use clap::Parser;
use serde::Deserialize;
use std::{fs, path::PathBuf};

#[derive(Parser)]
struct Args {
    #[arg(long)]
    prompt: PathBuf,
    #[arg(long)]
    history: PathBuf,
    #[arg(long)]
    attachments: Vec<PathBuf>,
}

#[derive(Deserialize)]
struct EventContent {
    msgtype: String,
    body: String,
}

#[derive(Deserialize)]
struct EventLine {
    room_id: String,
    event_id: String,
    sender: String,
    #[serde(rename = "type")]
    kind: String,
    origin_server_ts: u64,
    thread_root: Option<String>,
    content: EventContent,
}

fn read_history(path: &PathBuf) -> Vec<EventLine> {
    fs::read_to_string(path)
        .ok()
        .into_iter()
        .flat_map(|text| text.lines().map(|line| serde_json::from_str(line)))
        .filter_map(Result::ok)
        .collect()
}

fn read_prompt(path: &PathBuf) -> Vec<EventLine> {
    fs::read_to_string(path)
        .ok()
        .into_iter()
        .flat_map(|text| text.lines().map(|line| serde_json::from_str(line)))
        .filter_map(Result::ok)
        .collect()
}

fn summarize_prompt(lines: &[EventLine]) -> String {
    if lines.is_empty() {
        "no recent events".to_string()
    } else {
        lines
            .iter()
            .map(|entry| format!("{}: {}", entry.sender, entry.content.body.trim()))
            .collect::<Vec<_>>()
            .join(" ⇢ ")
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    let prompt = read_prompt(&args.prompt);
    let history = read_history(&args.history);
    let summary = summarize_prompt(&prompt);
    let attachments = args
        .attachments
        .iter()
        .map(|path| path.display().to_string())
        .collect::<Vec<_>>()
        .join(", ");
    println!(
        "Prompt {} entries · history {} entries · attachments [{}]\nSummary: {}",
        prompt.len(),
        history.len(),
        attachments,
        summary
    );
    Ok(())
}

Build this helper with cargo build --bin bot (or your preferred toolchain) and pass the resulting binary to iamb listen --command. Every time the listener encounters a fresh burst, the helper sees the NDJSON prompt, can reuse the rolling history for context, and prints whatever text should be sent back.

archive

iamb archive converts or bundles an existing event log into final artefacts such as HTML/Markdown exports or static copies. It consumes the files produced by iamb listen (notably the events/, media/, and meta/ hierarchies), and will read postback queue entries or hooks if present so cached external assets can be included or referenced in the generated outputs.

Use --only-print-changes when you want quieter archive runs that suppress unchanged-room chatter while still reporting real updates, warnings, errors, and the final summary. Use --continue to resume from the newest archived event recorded for each room, rather than replaying the entire history from scratch.

Handling encrypted events

If iamb archive encounters events it still cannot decrypt after a handful of retries, it now stops with an error, lists the affected rooms, and records each event ID under meta/undecryptable.txt. The undecryptable ledger is maintained automatically by the tools: entries will be added when events cannot be decrypted and will be removed automatically once keys become available.

Archive now persists a little more context for each unresolved event in that ledger, including repeated archive-run sightings and any sender / session / withheld metadata present on the raw encrypted event. Final archive errors also print a more human-readable summary per event, including room display names, sender, session/device hints, and whether the gap is still considered retryable or has become a conservative likely permanent classification.

To unblock the run you can:

  1. Export keys from another verified client (iamb keys export /path/to/keys mypass), copy the file, and import them on the archive machine (iamb keys import /path/to/keys mypass).
  2. Re-run iamb archive once the keys are present.
  3. If the room or event should be skipped, prefer using the user-maintained ignore list in meta/ignored-events.txt. This file accepts one event ID per line and may optionally include the room id and/or a friendly label after the event id, for example:
$EVENTID                       # just the event
$EVENTID !roomid:example.com   # event + room
$EVENTID !roomid:example.com A friendly name
  1. If an entire room or homeserver should be treated as an acceptable permanent hole, prefer meta/allowed-decryption-errors.txt over ignoring individual event IDs. That file accepts a room id or server name, plus an optional cutoff timestamp and label, using the same format as the other room-rule files.
  2. If you rerun iamb archive --continue, long-lived undecryptable events can eventually be auto-classified as likely permanent and the archive will continue past them instead of blocking forever. The current conservative thresholds are 5 archive runs in general, or 3 archive runs when the raw encrypted event already carries withheld metadata.

Notes:

  • meta/undecryptable.txt is managed by the archiver/listener and should not be edited by hand except for advanced use cases. The tools will remove entries from meta/undecryptable.txt once the missing keys are available and events can be decrypted.
  • Events explicitly listed in meta/ignored-events.txt will remain skipped even after keys are imported; use the ignored-events list to permanently suppress specific events from processing.
  • likely permanent is a heuristic, not certainty. iamb can report the sender device that originally created an encrypted event, but it cannot prove whether your missing room key copy lived only on a device that has already been logged out of or lost.

The live listener (iamb listen) never blocks on encrypted events, but it warns in the log and appends each unresolved event to the same meta/undecryptable.txt file so you know which rooms still need attention.

Decryption diagnostics

Pass the global --log-decrypt-errors flag when running the interactive UI or any CLI command that receives messages (the default launcher, iamb archive, iamb listen, and iamb unreads). When enabled, every event the client still cannot decrypt is appended as a newline-delimited JSON record to ~/.local/share/iamb/profiles/<profile>/encryption-errors.log. Each entry captures the room/event identifiers, sender, encryption metadata (algorithm/session_id/sender_key/device_id/withheld), the stage at which it was observed, and the full raw payload so you can investigate missing keys or bridging issues.

Retention cleanup (iamb redact)

iamb redact executes explicitly configured retention/cleanup policies from your profile's delete section. It does not create new policies from CLI flags — the flags only select which pre-defined policies to apply or temporarily override specific policy fields for that run.

How policy resolution works

  • The effective policy for a given room and store is resolved in this order (later items override earlier ones):
    1. Global store defaults: delete.stores.<Store> (lowest precedence)
    2. Named room sets: delete.room_sets.<Name> (applied in deterministic order)
    3. Per-room overrides: delete.rooms."<room_id>"
    4. Top-level explicit excludes: delete.exclude_rooms (these opt rooms out of global store defaults)
  • There are two common store kinds:
    • remote — operations that redact events on the homeserver (SDK redactions).
    • archive — operations that edit the local on-disk archive (events NDJSON and local media files).
  • If a policy requires archive presence (require_archive_presence), the archive index (the events/ files) is consulted to decide whether a remote deletion should be applied to a particular event.
  • Candidate discovery defaults to live Matrix history; pass --candidate-source archive when you want iamb redact to seed the plan from the local archive NDJSON instead.
  • If you want a copy-and-edit replay artifact, add --save-script path/to/redact.sh and review the generated bash script. Each selected event gets its own executable iamb redact command, with the surrounding metadata and body preview kept as comments.
  • To narrow a generated or replayed plan to explicit event IDs, pass repeatable --event <event_id> arguments.

Minimal example configuration

[delete]
allow_cli = true
default_rooms = ["!cleanup:matrix.org"]

# Global remote (server-side) policy
[delete.stores.remote]
enabled = true
include_media = true        # include non-message media events as candidates
skip_peer_events = true     # only affect messages sent by the profile user
older_than = "30d"         # target events older than 30 days

# Global archive (local) policy — operate on the archive files & media
[delete.stores.archive]
enabled = true
include_media = true
older_than = "30d"

Both remote and archive can be configured independently; per-room overrides are used when you need a different retention rule for a particular room.

Practical scenarios and configuration hints

  1. Cleaning old data from the server
  • Goal: redact messages older than N days from the remote homeserver.
  • Config: set delete.stores.remote.enabled = true and older_than = "30d" (or desired window).
  • Run: iamb -P profile redact --remote --dry-run
  • Once satisfied, re-run without --dry-run to perform remote redactions.
  1. Purging something you didnt want saved anywhere (remote + archive)
  • Goal: remove events both from the server and your local archive.
  • Config: enable and tune both stores (archive + remote) with matching older_than and include_media as needed.
  • Run: iamb -P profile redact --remote --archive --dry-run
  • Confirm and re-run without --dry-run to redact remotely and prune local NDJSON + media files.
  1. Configuring disappearing messages for a room
  • Goal: make a room automatically cleaned up after a time window.
  • Config: add a per-room entry under delete.rooms."<room_id>".stores.<Store> and set older_than and enabled = true. Optionally set leave_empty_rooms if you want the client to leave/forget empty rooms.
  • Use delete.room_sets to apply the same disappearing rule to many rooms.
  1. Automating periodic cleanup (cron)
  • Put a safe cron job that runs with --dry-run first when testing, then without once validated.
  • Example cron invocation (monthly run):
    • 0 3 1 * * /usr/bin/iamb -P myprofile redact --archive --remote --dry-run >> /var/log/iamb-delete.log 2>&1
    • After validating outputs, remove --dry-run.
  1. Fully purging a room (include leave + media removal)
  • Goal: remove archived events, delete associated media files, optionally leave the room when empty.
  • Config: per-room delete.rooms."<room_id>".stores.archive with enabled = true, include_media = true, and leave_empty_rooms = true.
  • Run: iamb -P profile redact --archive --dry-run (inspect) then re-run without --dry-run.

Important operational notes

  • CLI flags like --older-than and --media temporarily override policy fields for that invocation only; they do not create or persist a policy. To make changes persistent, edit your config.

  • Start with --dry-run every time and inspect results carefully.

  • Inspect resolved policies before running destructive operations: iamb -P profile config current --format toml This prints the resolved configuration (including merged defaults/overrides) so you can confirm what will be applied.

  • If you rely on archive checks (require_archive_presence or archive_before) ensure the archive root exists and is readable by the process that runs iamb redact.

  • Faster defaults are always active; the archiver keeps meta/room-sync.json up to date with the last archived event per room and skips rooms whose remote history has not advanced since the previous run. Use iamb archive --full (or delete that metadata file) to force a complete rescan if you suspect new events were missed, then tune archive.archiver.fetch_limit, archive.archiver.message_fetch_retry_delay_secs, archive.archiver.message_fetch_timeout_secs, archive.listener.heartbeat_interval_secs, archive.listener.idle_sleep_millis, and the archive.archiver.* / archive.listener.* external rate-limit entries when you need more conservative pacing. See docs/addressed/notes/archiver.md for guidance.

  • Keep rooms out of the listener by configuring archive.listener.excluded_rooms in your profile; the same list can be supplied temporarily using --exclude-room on the CLI. You can also use archive.listener.invites.whitelist/archive.listener.invites.blacklist (or the CLI equivalents) to control which invites are automatically accepted.

Sender and homeserver-aware cleanups

You can layer the --sender, --homeserver, and --homeserver-other flags on your remote policy runs to only target specific users or domains. Keep named room sets handy so you can easily point a cleanup at public rooms, encrypted rooms, or other slices of your archive.

[delete.room_sets.public]
rooms = ["!public1:example.com", "!public2:example.com"]

[delete.room_sets.unencrypted]
rooms = ["!open1:example.com", "!open2:example.com"]

[delete.room_sets.encrypted]
rooms = ["!secret1:example.com", "!secret2:example.com"]
  1. Redact only your own messages in the public room set between 3 and 12 months old:

iamb -P public redact --remote --sender @user:example.com --older-than "90d" --newer-than "365d"
--room !public1:example.com --room !public2:example.com --dry-run


2. Target non-encrypted rooms only when removing your own activity from the last 3090 days:

```bash
iamb -P public redact --remote --sender @user:example.com --older-than "30d" --newer-than "90d" \
--room !open1:example.com --room !open2:example.com --dry-run
  1. Repeat the same clean-up for encrypted rooms that match your delete.room_sets.encrypted list:

iamb -P public redact --remote --sender @user:example.com --older-than "30d" --newer-than "90d"
--room !secret1:example.com --room !secret2:example.com --dry-run


4. Redact every event originating from a specific home server (or servers) regardless of the sender:

```bash
iamb -P public redact --remote --homeserver matrix.org --homeserver example.org --older-than "180d" --newer-than "720d" --dry-run
  1. Clean up content from homeservers that are not your own (useful when bridging or roaming):

iamb -P public redact --remote --homeserver-other --older-than "90d" --newer-than "365d" --dry-run


These combinations give you the expressive filter power to redact messages by sender, room type, or server while still respecting your configured retention policies.

### mentions

By default, `@all` and `@channel` will send mentions to every member of the room (@room is specially handled by some clients so it is skipped). To opt out, set:

```toml
[settings]
expand_everyone_mentions = false

room-space context in lists

If you want room, chat, DM, and invite lists to show the parent space for each room, enable:

[settings]
show_room_space_context = true

This is off by default so compact list views stay unchanged unless you explicitly opt in.

verify

iamb verify logs in and runs a guided self-verification flow on the command line in isolation.

Inside the TUI:

  • :verify opens the active SAS verification list.
  • :verify login starts the same guided self-verification flow for the current session.

During a fresh interactive login, iamb now requires verification before finishing bootstrap. You can choose a recovery/security key, another signed-in device, or both. If verification does not complete, iamb falls back to the secure-backup reset flow so the new login can become trusted.

If the login is fresh and secure backup is not yet configured, iamb can also offer setup on the CLI or :setup in the TUI. That path bootstraps secure backup on a trusted device without asking for a local room-key export first, so the first-login flow stays explainable instead of failing on a missing export path.

setup

Bootstrap secure backup on a freshly trusted device without exporting local room keys first. This is the explainable first-login path when you have just logged in and want iamb to set up recovery for you immediately.

iamb -P profilename setup --password <pw>
:setup

What it does

  • Restores the session if available; otherwise prompts for password or SSO, then performs an initial sync so encryption state is current.
  • Generates or accepts a recovery passphrase, prompts you to retype it unless --yes is set, and enables secure backup on the trusted device without requiring a local room-key export.
  • Prints the recovery key and passphrase; store them immediately in a password manager or secure medium.

reset-secure-backup

Enable or rotate secure backup on a trusted device and generate a fresh recovery passphrase and recovery key. Before any server-side change, iamb requires a local encrypted room-key export as a rollback copy unless you intentionally use --skip-keys-export for a trusted first-login bootstrap or emergency recovery.

If you only want a passphrase to stash first and use later, you can generate one without touching server state:

iamb -P profilename reset-secure-backup --generate-passphrase-only
iamb -P profilename reset-secure-backup

What it does

  • Restores the session if available; otherwise prompts for password or SSO, then performs an initial sync so encryption state is current.
  • Requires --keys-export <path> and --keys-export-passphrase <value> (unless --generate-passphrase-only is used) and writes a local encrypted room-key export before changing recovery state.
  • Use --skip-keys-export only when you intentionally want the trusted-device bootstrap path without exporting local room keys first.
  • Generates a high-entropy passphrase (you can supply --passphrase <value>), prompts you to retype it unless --yes is set, then either:
    • enables secure backup via the SDKs recovery.enable().wait_for_backups_to_upload() flow when recovery is not enabled yet, or
    • rotates the recovery key via recovery.reset_key() when recovery is already enabled and --force is supplied.
  • Prints both the recovery key and passphrase; store them immediately in a password manager or secure medium.

What does not change

  • This device keeps access to encrypted messages/media it can already read.
  • Other already signed-in devices keep access to encrypted messages/media they can already read.
  • No room history, messages, or media are deleted.

What does change

  • New devices must use the new recovery key or passphrase after rotation.
  • The old recovery key/passphrase should be treated as obsolete once the new one works.

Flags

  • --passphrase <string> — supply your own passphrase instead of auto-generating one.
  • --keys-export <path> — required local encrypted room-key export path before changing secure backup.
  • --keys-export-passphrase <string> — passphrase used to encrypt the required local room-key export.
  • --generate-passphrase-only — print a secure-backup passphrase and exit without logging in or changing any backup state.
  • --skip-keys-export — skip the local key export before changing secure backup; use only for trusted first-login bootstrap or emergency recovery.
  • --force — recreate recovery even if already enabled.
  • --yes — skip the confirmation prompt (non-interactive / automation).
  • --password <pw>, --sso — optional login inputs, same as iamb verify.
  • --timeout <secs> — limit how long login and verification can take before the command gives up.

Hooks, postback, and media configuration

External media jobs are classified and processed according to the MediaHandling configuration in your config.toml. Two related configuration areas control behavior:

  • media.hooks — fine-grained enable/disable and include/exclude pattern rules for each handler, either globally (media.hooks.default) or per-room (media.hooks.rooms). Rules are simple substring matches: an include list requires a substring to appear, and exclude prevents matches containing a substring. Rules also have an enabled flag to turn handlers on/off for matching URLs.

  • media.post_back — controls which fetched assets should be queued for postback. Legacy list-style options (default and rooms) continue to work, but you can now specify default_rules and room_rules to apply the same pattern-based controls used by media.hooks.

A minimal example config.toml excerpt that demonstrates enabling the single-file browser, restricting yt-dlp globally to example.com links, and enabling postback only for a special room:

[media]
single_file_browser = "/usr/bin/chromium"

# Hook rules: enable yt-dlp only when the URL contains "example.com"
[media.hooks.default]
# Handler keys map to MediaHandlingKind names (Direct, YtDlp, SingleFile, MatrixAttachment)
YtDlp = { enabled = true, include = ["example.com"], exclude = [] }
Direct = { enabled = true }

# Postback: legacy list is still supported, but you can use rule-based controls
[media.post_back]
default = [] # no legacy global postback

# Enable postback for yt-dlp only in a specific room via room_rules
[media.post_back.room_rules."!special:example.com".YtDlp]
enabled = true
include = []
exclude = []

Note: enabling the optional Cargo feature yt-dlp-python allows iamb to query yt-dlp's Python extractors at runtime to determine whether yt-dlp can handle particular URLs. This can improve handler classification for many streaming or platform URLs. Enable it by building with --features "yt-dlp-python".

Command prefixes in messages (for example !yt-dlp ...) can still be used to force YtDlp handling for a URL. Fetched assets now land under the canonical per-room media directory media/<sanitized-room>/, with filenames that include the handler label (for example 2024-01-01@12:00:00__alice__yt-dlp__some-title.mp4). Hook scripts or automation can consume meta/postback-queue.ndjson to act on queued items (for example sending follow-up messages that include cached assets).

Example workflows

# setup
mkdir -p ~/.config/iamb
$EDITOR ~/.config/iamb/config.toml

# authenticate once and complete guided verification if needed
iamb -P profilename verify

# run a reusable background session for fast CLI commands
iamb -P profilename daemon

# archive everything once
iamb -P profilename archive --verbose --out ~/.local/share/iamb-archive/profilename

# continue archiving from the newest saved event per room
iamb -P profilename archive --verbose --out ~/.local/share/iamb-archive/profilename --continue

# listen only for selected rooms
iamb -P profilename listen --verbose --out ~/.local/share/iamb-archive/profilename \
  --room <room-id0> --room <room-id1>

# clear unread markers without sending public receipts
iamb -P profilename unreads clear --no-receipt --all

Installation (from source)

Install Rust and Cargo using rustup, and then run from the directory containing the sources (ie: from a git clone):

cargo install --locked --path .

Installation (via crates.io)

Install Rust (1.83.0 or above) and Cargo, and then run:

cargo install --locked iamb

See Configuration for getting a profile set up.

Installation (via package managers)

Arch Linux

On Arch Linux a package is available in the Arch User Repositories (AUR). To install it simply run with your favorite AUR helper:

paru iamb-git

FreeBSD

On FreeBSD a package is available from the official repositories. To install it simply run:

pkg install iamb

Gentoo

On Gentoo, an ebuild is available from the community-managed GURU overlay.

You can enable the GURU overlay with:

eselect repository enable guru
emerge --sync guru

And then install iamb with:

emerge --ask iamb

macOS

On macOS a package is available in Homebrew's repository. To install it simply run:

brew install iamb

NetBSD

On NetBSD a package is available from the official repositories. To install it simply run:

pkgin install iamb

Conda / Pixi

On platforms supported by conda-forge, iamb can be installed with Conda-compatible tools:

conda install conda-forge::iamb

Or with pixi:

pixi global install iamb --channel conda-forge

Nix / NixOS (flake)

nix profile install "git+https://codeberg.org/microchipster/iamb.git"

openSUSE Tumbleweed

On openSUSE Tumbleweed a package is available from the official repositories. To install it simply run:

zypper install iamb

Snap

A snap for Linux distributions which support the packaging system.

snap install iamb

License

iamb is released under the Apache License, Version 2.0.