August 12, 2026 at 12:00:00 AM UTC
wolfram's CLI becomes a real AT Protocol client, and the agent layer sheds its stubs
wolfram
The CLI (wolfram_cli, renamed to wolf partway through) went from a thin demo over the SDK to something closer to a full AT Protocol client, and the wf_agent_* layer it sits on filled in several long-standing gaps at the same time.
from demo to full client
main.c started this window as a flat ~2500-line dispatch table covering the basics — post, follow, timeline, mute, and so on. feat(cli): expand wolfram CLI to full AT Protocol client coverage added the commands that were missing: cli_search (search-actors, search-typeahead, search-starter-packs), cli_feed (get-actor-status, get-feed-generators, get-suggested-follows-by-actor, age-assurance begin/get-config/get-state), cli_ozone (moderation query-statuses/get-suggestions/get-label-definitions, team, server, communication, setting, signature, report, queue operations), and cli_admin (search-accounts, get-account-info, get-subject-status, get-invite-codes, delete-account, account-invite toggling, send-email). main.c was split into 14 focused cli_* modules, with dispatch and shared helpers left behind in main.c itself.
A follow-up commit, feat(cli): wire up undispatched commands and fix associated bugs, actually dispatched the search/feed/admin commands the previous commit had added but not wired into main(), and rewrote cmd_list to handle create/update/delete/add-item/remove-item before falling through to the read path — cmd_list_update in particular became a proper fetch-merge-put through wf_agent_get_record_typed/wf_agent_put_record_typed so existing fields ($type, createdAt, purpose) survive an update instead of being clobbered. A separate commit, fix(cli): add delete, whoami, and describe-server commands, filled in three more that had been documented in the usage string but never implemented.
The binary itself was renamed in feat(cli): rename binary to 'wolf' derivative of wolfram — wolfram_cli's OUTPUT_NAME changed from wolfram to wolf, and every usage string, README.md, and docs/cli.md reference followed. Version bumped 0.13.0 to 0.13.1 for the rename.
splitting main.c and agent.c
Two structural refactors ran in parallel. refactor(cli): split the OAuth flow into cli_oauth.c pulled the self-contained oauth-login/oauth-callback flow (PAR discovery, pending-state persistence, callback URL parsing, token exchange) out of main.c into its own module, introducing src/cli/main_internal.h for the helpers every command group needs (usage_stream, resolve_actor_to_did, resolve_post_cid, finish_agent_response, and others) — the same internal-header pattern the SDK side already used, applied to the CLI for the first time. main.c dropped from 3055 to 2702 lines. The commit message notes read_text_file stayed behind in main.c despite living textually inside the OAuth block, since repo put-record --json <file> also depends on it.
On the agent side, refactor(agent): split the local repo mirror into agent_repo_sync.c separated agent.c's two distinct responsibilities: the app.bsky/com.atproto convenience API (post, follow, like, feeds, notifications, account lifecycle, PLC operations) versus the offline local repo mirror — wf_agent_seed_repo, wf_agent_apply_repo_diff, the wf_store persistence bridge, and the com.atproto.sync.* read wrappers that serve it. agent.c dropped from 4295 to 3929 lines, verified against both WOLFRAM_BUILD_STORE=ON and OFF builds since the persistence bridge is gated internally rather than at the CMake source-list level.
A broader refactor: sort src/ root files into their existing subdirectories followed, moving 24 root-level *_typed.c files plus bsky_agent.c, blob.c, and embed.c into src/agent/ to match where graph_typed.c, chat_typed.c, and the rest already lived — closing out an inconsistency rather than introducing a new convention. include/wolfram/*.h was untouched throughout, since none of this affects the installed public API.
bugs the modularization surfaced
Splitting the CLI apart exposed a handful of real bugs that had been sitting in the flat file. fix(cli): fix use-after-free printing garbage in delete-record output found that cmd_repo's delete path called wf_agent_session_data_free(&sd) — which frees sd.did — before the success printf that used repo (an alias of sd.did), so delete-record printed allocator bytes instead of the actual DID. Fixed by moving the free after the print, and freeing on the error path too, which the original code skipped entirely.
fix(cli): keep subcommand --json flags while stripping the global one fixed the global --json flag stripper, which stripped every --json token found anywhere in argv, not just a leading global one — so repo put-record --json <record>, where --json is the subcommand's own flag, silently lost its argument. The fix stops stripping once the command word has been seen.
fix(cli): use typed result structs in server, identity, and chat commands fixed a class of bug that only showed up on GCC: cli_server.c, cli_identity.c, and cli_chat.c were passing a wf_response* into wf_agent_*_typed calls that expect a real typed output struct. Clang only warned; GCC 14's -Wincompatible-pointer-types treats it as a hard error, which was breaking MetalBear's Alpine image build. Each command now passes the correctly typed struct and prints its fields.
Error reporting also improved: fix(agent): expose the last XRPC error message added wf_agent_last_error(), returning the server's XRPC error message from the agent's most recent request. fix(cli): surface server XRPC error messages on write failures then threaded it through a new cli_agent_error() helper so post, reply, and other write commands print the server's actual error text instead of just a bare status code.
agent-layer stubs, resolved
Several wf_agent_* wrappers that had been deliberately left as "honest stubs" — failing cleanly rather than faking a response — got real implementations this window. feat(agent): implement revokeAccountCredentials wrapper and CLI command replaced a stub whose signature didn't carry the lexicon-required account at-identifier at all; wf_agent_revoke_account_credentials now takes account directly and calls through the generated wf_lex_com_atproto_temp_revoke_account_credentials_main_call, with a matching wolf revoke-account-credentials command.
fix(actor-status): parse statusView's createdAt/durationMinutes from record, implement agent wrappers fixed a parsing bug and closed out a permanent stub at once: wf_actor_status_parse_view read createdAt/durationMinutes at the top level of a statusView, but the lexicon defines statusView.record as an opaque blob holding the actual app.bsky.actor.status record, so those fields were never populated from a real response. wf_agent_get_actor_status/get_status/put_status had been stubs waiting on getActorStatus/getStatus/putStatus RPCs that don't exist upstream and never will — bluesky-social's own liveNow feature reads status from a plain getProfile(s) response and writes it via com.atproto.repo.putRecord with rkey self. All three wrappers were reimplemented through the existing wf_agent_get_profile_raw/wf_agent_put_record primitives, with new mock-PDS integration tests covering the no-live-status and record-nesting cases the old stub-argument tests never exercised. A later docs: commit cleaned up comments in actor_status_typed.h, temp_typed.h/.c, and plc.c that still called these paths "honest stubs" or "TODO" after they'd already been resolved — including plc.c's did:web rejection, which turned out to be deliberate permanent behavior (did:web has no PLC operation to build) rather than an unimplemented one.
Smaller agent additions: feat(agent): add scoped muteActor convenience wrapper added wf_agent_mute_actor_scoped, taking only_reposts/only_quoteposts flags against app.bsky.graph.muteActor. wf_agent_profile and wf_agent_profile_view both gained viewer.blocking/viewer.muted fields, parsed and freed the same way viewer.following already was, for consumers like Cobalt's profile screen that need mute/block state next to a fetched profile. On the video side, fix(agent): route video getJobStatus/getUploadLimits through generated calls and feat(cli): surface app.bsky.video job status on upload connected video_typed's failureCode field through to wolf video upload's output.
wolf browse, identity, and plc log
The most substantial addition, feat(cli): add browse/identity/plc log -- the PDSls equivalent for wolf, composed primitives wolf already had into a browsing experience matching pdsls.dev: wolf browse <service> <handle-or-did> [collection] [rkey] lists a repo's collections via describeRepo, lists a collection's records via paginated listRecords, or shows a full record via getRecord, depending on how many arguments follow. wolf identity <service> <handle-or-did> resolves the DID document through a new wf_did_resolve_raw — deliberately not through whatever PDS service happens to point at, since resolution shouldn't depend on being able to log into the repo's own host — and prints every alsoKnownAs handle with a live bidirectional check via wf_agent_verify_handle, plus verification methods, service endpoints, and current PLC rotation keys for did:plc subjects. wolf plc log <did> walks the full PLC operation history via a new wf_plc_get_audit_log, diffing each entry against the previous one (handles, rotation keys, PDS endpoint, signing key) and normalizing the pre-2022 legacy genesis format so real accounts with old history still diff correctly. All three commands are anonymous and read-only, matching the underlying XRPC endpoints, and were verified end to end against live production data (jay.bsky.team).
all entries