maybe a prototype, maybe it will go in production
Zig 68%
TypeScript 27%
CSS 3%
Shell <1%
JavaScript <1%
<1%
Dockerfile <1%
HTML <1%

README.md

linji #

The CLI for 林集 (linji.at) — an atproto study-community client. One binary: multi-account login, spaces, threads, posting, watching, Markdown export. It is also the supported surface for building bots that read and post on app0.linji.at.

Build & install #

Requires Zig 0.16 (zvm/zigup recommended) and a C toolchain.

zig build                          # -> zig-out/bin/linji
zig build test                     # unit tests
zig build install --prefix ~/.local   # -> ~/.local/bin/linji

Dev scenarios (need ../zds built — cd ../zds && zig build -Dtarget=x86_64-linux-gnu.2.36; the explicit target uses zig's bundled CRT because today's system glibc crt1.o carries .sframe relocations zig's linker rejects): zig build scenario-space, zig build scenario-serve.

Accounts #

Any atproto handle works. To post in spaces hosted on app0.linji.at, the bot's records are hosted on our zds (phase 001: member records live on the host's zds — see docs/architecture.dj "External members"), and an admin must add the account to the space's member list:

linji space add-member --space at://<community>/space/at.linji.space/<skey> --did <bot-did>
linji space rename --space at://<community>/space/at.linji.space/<skey> --name "<display name>"

Login is OAuth. Tokens refresh themselves afterwards; the session lives in $LINJI_HOME/accounts.json (default ~/.local/share/linji).

linji login <handle> --pds <pds-url>     # prints an authorize URL, opens consent

Headless servers: log in once on a machine with a browser, then copy accounts.json to the server (chmod 600 — it carries the DPoP key and refresh token). Refresh is automatic from then on.

Commands (bot-relevant) #

linji thread list --space <uri>                      # thread AT-URIs + titles
linji thread create --space <uri> --title <t>        # -> thread AT-URI
linji post --space <uri> --thread <at-uri> <text…>   # -> post AT-URI
linji read --space <uri> [--tag <t>]                 # merged thread view
linji watch --space <uri> [--interval ms]            # follow new posts
linji edit|delete --space <uri> --post <at-uri> …
linji export --space <uri> --thread <at-uri>         # thread as Markdown

--as <handle> picks a non-default account. --tag filters to threads containing an inline hashtag.

A minimal bot #

The loop: watch for new posts, reply with post. Here reacting to a #question tag:

#!/bin/sh
SPACE="at://did:plc:community/space/at.linji.space/general"
THREAD="at://did:plc:bot/at.linji.thread/…"          # reply thread

linji watch --space "$SPACE" --interval 3000 | while IFS= read -r line; do
  case "$line" in
    *"#question"*) linji post --space "$SPACE" --thread "$THREAD" \
      "收到 — 让我想想。" ;;
  esac
done

For machine-readable reads, the BFF serves GET /api/space?uri=<space> (JSON: threads + posts) once you hold a session cookie; the CLI's rendered output is the stable interface for shell bots.

Run it under your service manager. app0/dinit/ has examples for the dev stack.

Dev rig #

dinit -d app0/dinit dev        # zds + seed + BFF + vite (see app0/dinit/dev)
# seeded accounts: community/alice/bob (.test, password <name>-password)

Layout, architecture, roadmap: see ../docs/.