Single-workspace Zulip CLI toolkit for humans and AI agents — read, write, archive, and grep one Zulip workspace from the shell.
Project description
zlp-cli
Single-workspace Zulip CLI for humans and AI agents. zlp is a small,
scriptable command-line toolkit that wraps one Zulip account: read streams,
post and edit messages, upload files, and keep a lossless local Markdown
archive that's easy to grep and easy for an LLM to read.
It deliberately knows nothing about which Zulip workspace it's talking to —
one process, one zuliprc, one archive directory. Driving multiple workspaces
is the job of an outer layer that keeps per-workspace credentials and cds into
each before invoking zlp.
Why
- Agent-friendly. Predictable subcommands, machine-parseable output
(Markdown with YAML frontmatter, JSON via
--format json, TSV for status). An agent shell can callzlp messages --stream Xand get usable text back without parsing HTML. - Local-first archive. Every message lands as its own
.mdfile with attachments next to it, making the chat corpus another grep-able knowledge base alongside your code repo. - Real-time. A background daemon tails Zulip's event queue (no polling), so the local archive stays current automatically.
- One zuliprc, one job. No multi-tenant logic, no implicit globals — easy to drop into containers, CI, or a per-agent sandbox.
Install
pip install zlp-cli # or: uv pip install zlp-cli
This installs the zlp console script. You'll also need a zuliprc file —
create one at https://YOUR-ZULIP-SERVER/api/api-keys and save it.
Quick start
export ZULIP_CONFIG_FILE=/path/to/zuliprc
zlp whoami
zlp streams
zlp messages --stream general --limit 10
zlp send --stream general --topic test --msg 'hello from zlp'
# run one incremental pass over subscribed workspace stream messages
# prints archived file paths for messages written in this pass
zlp pull
# keep the local mirror current in the foreground (Ctrl-C to stop)
zlp sync
# ...or in the background as a daemon
zlp sync --daemon
tail -f .run/_workspace.log # daemon log with archived file paths
# or narrow sync to one stream
zlp pull --stream general --import-history
zlp pull --stream general # one incremental stream pass
zlp sync --daemon --stream general # background stream daemon
tail -f .run/general__*.log # stream daemon log
For multi-line message bodies pipe stdin: zlp send ... --msg-file -.
Configuration
Three settings, with this precedence: flag > env > default.
| Setting | Flag | Env var | Default |
|---|---|---|---|
| zuliprc path | --config |
ZULIP_CONFIG_FILE |
./zuliprc |
| archive root | --archive-root |
ZLP_ARCHIVE_ROOT |
. |
| daemon pid/log root | --run-root |
ZLP_RUN_ROOT |
./.run |
Defaults are CWD-relative. An outer "workspace manager" can cd into a
per-workspace directory or set the env vars to point at workspace-specific
locations — zlp itself stays workspace-agnostic.
Commands
| Command | What it does |
|---|---|
zlp whoami |
Print server URL, account email, and full name. |
zlp streams |
List subscribed streams, one per line. |
zlp topics --stream S |
List topics in a stream. |
zlp messages --stream S [--topic T] [--limit N] [--format md|json] |
Fetch recent messages. |
zlp search --query Q [--stream S] [--limit N] [--format md|json] |
Server-side full-text search. |
zlp send --stream S --topic T (--msg M | --msg-file F) |
Send a stream message. |
zlp dm --to EMAIL[,EMAIL] (--msg | --msg-file) |
Send a direct message. |
zlp edit --id N (--msg | --msg-file) |
Edit your own message. |
zlp delete --id N |
Delete your own message. |
zlp upload --file F --stream S --topic T [--msg M | --msg-file F] |
Upload a file and post the link, with an optional message body. |
zlp pull [--stream S [--topic T]] [--all-public] [--import-history] [--no-attachments] [--silent] |
One-shot archive catchup; defaults to subscribed workspace streams, or narrows to one stream. Prints archived file paths unless --silent is set. |
zlp sync [--daemon] [--stream S [--topic T]] [--all-public] [--no-attachments] [--silent] |
Live event-queue sync. Foreground by default (Ctrl-C to stop); pass --daemon to run in the background. Defaults to subscribed workspace streams, or narrows to one stream. Prints archived file paths unless --silent is set. |
zlp unsync [--stream S [--topic T]] |
Stop the workspace sync daemon, or a stream daemon when --stream is set. |
zlp sync-status |
TSV of archive targets and daemon state. |
zlp reconcile --stream S [--topic T] [--since 24h] |
Re-fetch recent archived messages to catch edits and deletes the daemon may have missed. |
A Makefile is included for convenience (make whoami, make send STREAM=..., etc.).
Archive layout
Under --archive-root:
<archive-root>/
├── .sync-state.json # workspace cursor
└── <stream-slug>/
└── <topic-slug | _all>/
├── 2026-04-26T02-30-00_alice-chen_id147641.md # one file per message
├── _files/<sha-prefix>__attachment.pdf # downloaded attachments
└── .sync-state.json # per-stream/topic cursor
Workspace-wide zlp pull and zlp sync --daemon write a workspace-level
.sync-state.json at the archive root; individual messages still land under
their normal stream/topic directories. By default they follow the account's
subscribed stream message feed, including subscribed private streams, and
ignore direct messages. --all-public is an advanced mode for public channels
beyond the account's subscriptions.
Each .md file has YAML frontmatter (sender, ids, timestamp, permalink,
attachments) followed by the message body. Edits rewrite in place; deletions
move the file to <name>.md.deleted with _archive.deleted: true.
Archive-writing commands emit stable TSV lines by default:
archived <stream-slug>/<topic-slug>/<message-file>.md
deleted <stream-slug>/<topic-slug>/<message-file>.md.deleted
For background daemons, these lines are written to .run/*.log (workspace daemon
to .run/_workspace.log, stream daemons to .run/<stream-slug>__<topic-slug>.log).
Tail those files directly. Pass --silent to suppress them.
For agents, zlp pull is the normal "what changed since the last pull?" command:
each archived\t... line is a file to read for that pass, followed by
ok archived=N. The cursor is the archive's .sync-state.json; it tracks last
pulled, not a separate per-agent read receipt. Use zlp sync --daemon only when
you want continuous background mirroring; tail .run/_workspace.log to inspect
that daemon's output.
For agent integrations
The CLI is designed to be the primitive layer below richer integrations:
- Outer workspace manager. Keep
zuliprcfiles, archive roots, and run dirs outside this package and dispatchzlpper account by settingZULIP_CONFIG_FILE,ZLP_ARCHIVE_ROOT,ZLP_RUN_ROOT. - MCP / function-calling shells. Subcommands map cleanly onto tool schemas; outputs are stable enough to feed back into a model.
- CI / cron jobs.
pullfor snapshots and one-shot incremental catchup,sync --daemonfor live mirrors,reconcilefor catching missed edits.
Development
git clone https://github.com/GiggleLiu/zlp-cli
cd zlp-cli
uv sync
make test
Useful developer targets:
make fmt # format Python with ruff
make fmt-check # check formatting
make lint # run ruff checks
make check # fmt-check + lint + tests
make build # build sdist and wheel artifacts
Source layout uses src/zlp/. Agent-facing workflow notes live in
AGENTS.md and .claude/CLAUDE.md.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zlp_cli-0.1.0.tar.gz.
File metadata
- Download URL: zlp_cli-0.1.0.tar.gz
- Upload date:
- Size: 19.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21304c68cff63cfffc4123ad2ba27aa2848ebda938e4dcec8c5beff2987041cb
|
|
| MD5 |
42f6122a0960aa864bf749b59d972580
|
|
| BLAKE2b-256 |
a7269c2bdb05a29e9487203f52ce176b49bcc0adf143715039f5ff8dd95be4c3
|
Provenance
The following attestation bundles were made for zlp_cli-0.1.0.tar.gz:
Publisher:
publish.yml on GiggleLiu/zlp-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zlp_cli-0.1.0.tar.gz -
Subject digest:
21304c68cff63cfffc4123ad2ba27aa2848ebda938e4dcec8c5beff2987041cb - Sigstore transparency entry: 1391087857
- Sigstore integration time:
-
Permalink:
GiggleLiu/zlp-cli@ebc86c691f7dd6d6df55bcc7fc93466ca6fb024c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GiggleLiu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ebc86c691f7dd6d6df55bcc7fc93466ca6fb024c -
Trigger Event:
push
-
Statement type:
File details
Details for the file zlp_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: zlp_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf268caf55af17a8895939e92577d3b908bd370d1bd19375e9c6ab9b99fd1a69
|
|
| MD5 |
29f1286b699cfe3ad6e07a9d69e802a5
|
|
| BLAKE2b-256 |
38ae9f5712b6e0047a32bbfd17230d5b3768ab524343bde39aca9e619bbc61bc
|
Provenance
The following attestation bundles were made for zlp_cli-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on GiggleLiu/zlp-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zlp_cli-0.1.0-py3-none-any.whl -
Subject digest:
cf268caf55af17a8895939e92577d3b908bd370d1bd19375e9c6ab9b99fd1a69 - Sigstore transparency entry: 1391087941
- Sigstore integration time:
-
Permalink:
GiggleLiu/zlp-cli@ebc86c691f7dd6d6df55bcc7fc93466ca6fb024c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GiggleLiu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ebc86c691f7dd6d6df55bcc7fc93466ca6fb024c -
Trigger Event:
push
-
Statement type: