unifier-cli 0.2.0

Filesystem postbox for inter-process communication via a Unix tree
Documentation

unifier

Filesystem postbox for inter-process communication. Programs share state by writing values to files in a Unix tree, treating the filesystem as a global data structure — the same strategy used by filesystem-git-issues (directories are namespaces, files are keys, UUID-named files are messages).

No central server is required.

State layout

Default root: ~/.local/unifier (override with $UNIFIER_HOME or --home).

~/.local/unifier/
  keys/<path>                                    # persistent key-value (global)
  mailbox/<recipient>/<uuid>.txt                 # point-to-point messages
  cron/<min>_<hour>_<dom>_<mon>_<dow>/<uuid>.txt # scheduled drops
  chroots/
    <name>/
      keys/
      mailbox/
      cron/                                      # isolated subtree (--chroot)

Cron schedule directories use five underscore-separated fields. Use * for “any” (e.g. 0_9_*_*_* = 09:00 daily).

CLI reference

Command Description
put <key> <value> Write a persistent key under keys/
get <key> Read a key
del <key> Delete a key
send [--from <agent>] <recipient> <message> Drop a mailbox envelope (id, from, to, payload)
message --from <agent> <recipient> '<json>' Structured agent mailbox message + event-socket wakeup
event '<json>' Post a JSON event under events/ and notify listeners
daemon watch Print wakeup notices from .daemon/events.sock (for Jan cron)
cron <schedule> <message> Schedule a cron message
poll <recipient> [--ack] Collect mailbox messages
poll-cron [--ack] Collect messages whose schedule matches now
list <path> List message files under a subtree
ack <uuid|path> Remove a processed message
root Print the effective state root
daemon start Start hot in-memory daemon (background)
daemon stop Stop daemon (flushes dirty state)
daemon status Show whether daemon is running
daemon flush Write dirty in-memory state to disk
tick start [label] Begin ACID tick (reads frozen previous state)
tick end Commit tick to disk with versioning under ticks/<n>/
tick status Show committed/active tick, queue, and locks
tick lock <key> / tick unlock <key> Lock keys during active tick
chroot init <name> Create chroots/<name>/ with keys/, mailbox/, cron/
chroot list List chroot names

Global flags:

  • --home <path> / $UNIFIER_HOME — top-level store directory
  • --chroot <name> / $UNIFIER_CHROOT — scope data commands to chroots/<name>/
  • --no-daemon — force direct filesystem access even when a hot daemon is running

Examples

# Build
cargo build --release

# Shared key-value
unifier put app/theme dark
unifier get app/theme

# Mailbox messaging
unifier send --from builder worker "rebuild docs"
unifier poll worker --ack

# Agent envelopes + wakeup (Jan listens on events.sock)
unifier message --from myagent youragent '{"hello":"world"}'
unifier daemon watch   # prints {"kind":"mailbox","id":"...","from":"myagent","to":"youragent"}

# Cron drops
unifier cron 0_0_*_*_* "nightly backup"
unifier poll-cron --ack

# Chroots
unifier chroot init work
unifier --chroot work put deploy/target staging
unifier --chroot work get deploy/target   # works
unifier get deploy/target                 # not found (global scope)

export UNIFIER_CHROOT=work
unifier send worker "task"

Installation

cargo install unifier-cli

The binary is unifier. crates.io uses unifier-cli because unifier is already taken.

Releasing

Same flow as jan-cli. From unifier/:

./scripts/release.sh              # patch bump
./scripts/release.sh --minor
./scripts/release.sh --major
./scripts/release.sh --set 1.2.3
./scripts/release.sh --dry-run
./scripts/release.sh --resume

Monorepo aliases:

mdo release
mdo release-dry-run
mdo release-minor
mdo release-major
mdo publish

This bumps Cargo.toml + CHANGELOG.md, commits those version files, runs tests, then cargo publish as unifier-cli.

Development

cargo test
cargo clippy -- -D warnings
cargo fmt

Summary of changes

Initial implementation

Created the unifier Rust CLI project with:

  • Filesystem-as-store model — inspired by filesystem-git-issues: entities map to directories, scalar values to files, messages to UUID .txt drops.
  • State root at ~/.local/unifier with $UNIFIER_HOME / --home override.
  • Key-value storeput, get, del under keys/, with atomic writes for put.
  • Mailboxsend, poll, ack for point-to-point messages under mailbox/<recipient>/.
  • Cron postboxcron, poll-cron, list using five-field schedule directory names matched against the current clock.
  • Path safety — keys and recipients reject ..; message ack resolves paths relative to the store root.
  • Tests — unit tests for cron parsing/matching; integration tests for put/get, mailbox, cron, and root.
  • project.meta.yaml — monorepo metadata, build scripts, and roadmap for future applications (zshrc hooks, agent pipelines, job queues, etc.).

Chroot support

Added isolated subtrees so multiple programs can share one global store without seeing each other's data:

  • On-disk layoutchroots/<name>/ contains a full copy of the layout (keys/, mailbox/, cron/).
  • --chroot / $UNIFIER_CHROOT — all data commands operate only inside the named chroot; UnifierHome::path() returns the effective subtree root.
  • Admin commandschroot init <name> creates a new sandbox; chroot list enumerates existing chroots (always uses the global root).
  • Scope module (scope.rs) — centralizes path validation: resolve_under_root() rejects .. and absolute escapes; used by list and ack.
  • Isolation tests — verify keys in one chroot are invisible to the global store and other chroots; path escape attempts fail.
  • Backward compatibility — omitting --chroot preserves the original global-root behavior.

Source layout

src/
  main.rs          # binary entry point
  lib.rs           # library + run()
  home.rs          # UnifierHome (global + effective root)
  scope.rs         # chroot path confinement
  chroot.rs        # chroot init/list
  paths.rs         # path builders for keys, mailbox, cron
  postbox.rs       # put/get/send/poll/ack operations
  cron.rs          # schedule parsing and matching
  fs_text.rs       # read/write helpers
  constants.rs     # directory name constants
  error.rs         # Error type
  cli/
    defs.rs        # clap command definitions
    mod.rs         # dispatch
tests/
  cli_integration.rs

Hot daemon

Added an in-memory hot store with on-demand disk flush:

  • HotStore (store.rs) — loads keys/mailbox/cron from disk; tracks dirty entries; flush() writes only changed data
  • Daemon processunifier daemon start|run|stop|status|flush|watch; control socket at <store>/.daemon/unifier.sock; event output at <store>/.daemon/events.sock
  • Mailbox envelopes — messages store {id, from, to, payload}; send/message/event broadcast a wakeup notice (to + id) so Jan can fetch mailbox/<to>/<id>.txt
  • Auto-routing — data commands use the daemon when running; --no-daemon forces direct filesystem access
  • Shutdown flushdaemon stop and daemon shutdown request flush dirty state before exit

Source layout (updated)

src/
  store.rs         # HotStore (in-memory + flush)
  daemon/          # protocol, client, server, lifecycle
  ...

Roadmap

See project.meta.yaml for the full list. Next major themes:

  • Span-tree logging (unifier log) and daemon web server
  • Tick-based agents (sense/decide/act lockstep, concurrency modes, differential sensing)
  • Factorio factory model: isolated arms coordinated toward a shared system goal