The shell: one CLI for humans and agents
One neosian for two readers: the operator console for the state your
agents already write, and the agent-facing grammar underneath it:
every verb with --json and exit tiers, so a script or an agent uses
the same doors. On a terminal the verbs render (a table, a tree,
markdown); under a pipe, NO_COLOR or --json the bytes are plain and
identical.
Set up and talk
Section titled “Set up and talk”neosian status [--json] # is this machine set up?neosian setup [--client C]… [--write] # wire the installed agents to this storeneosian configure [--list | --provider NAME --key - | --env NAME --key - | --delete]neosian chat [PROMPT] [--model M] [--agent FILE] [--resume ID] [--json]neosian update [--check | --write] [--mode off|notify|auto]neosian # on a terminal: chat; under a pipe: the helpstatus: the home and whether it exists; the config and which providers have a key (names and sources, never values); this directory’s two scopes; per client (Claude Code, Codex, OpenCode, Muse Code, Cursor) installed / MCP registered / hooks present / at which level (user,project, orboth) / the interpreter those files name still resolving; the last recorded session; the one-writer note, and a note when a client carries the hooks at both levels (they would run twice); the installation shape with its upgrade line; the update knob. Exit 0 whenever it ran; findings are data. For Muse, managed hooks count as user level;mcp_shadowed_bynames a preserved shared project MCP entry that overrides the user registration.setup: detects the clients present and runs both installers for each (mcp installandrecord install), once per machine: each client’s own config, the home, no mount (--level projectfor this directory’s files). It prints what would land;--writeapplies it, and runs the client’s own CLI for a file that CLI owns (claude mcp add-json,codex mcp add) when it is on PATH, printing the line otherwise (exit 1: something is left to do).--rootand--urlreach both installers:neosian setup --url URL --writemoves every client on the machine to the state process in one run.configure: keys under<home>/config.toml, one row per provider the catalog knows (shipped, door rows, registered doors);--provider NAME --key -reads the key from stdin, never argv;--env NAME --key -names a door by the env var its key lives in, for one the shell has not loaded (an agent file registers it); bare on a terminal prompts for each. Every stored key reaches the environment before a model is built, a door registered later included.chat: the resident agent. It knows neosian (thedocstool reads the shipped pages on demand), writes to/userand/projecton the home and loads the skills your other agents wrote. A PROMPT or piped stdin runs one turn;--jsonthe envelope (text, tool calls, usage, µ$), refused with no turn (exit 2) since a session cannot print one object;--model fakeis keyless. The model:--model, else[chat] modelinconfig.toml, else the first provider with a key (Anthropic, OpenAI, Cerebras, the shipped door rows by each door’s first model, registered doors).update: checks PyPI’s simple index;[update] modeisoff(default),notify(one stderr line on the human verbs, once per 24 h) orauto(applies a uv tool install within the major, never a pre-release over a stable, then re-executes itself).
Chat’s MCP servers. [[chat.mcp]] tables in config.toml name the
servers chat opens for the session (one turn or the loop) and adds as
tools, each mirroring McpServer (neosian docs mcp): name, then
command + args + env (a subprocess) or url + headers
(streamable HTTP), and prefix for names that clash. Values are
literal: the file is 0600.
[[chat.mcp]]name = "github"command = "npx"args = ["-y", "@modelcontextprotocol/server-github"]env = { GITHUB_TOKEN = "ghp_..." }prefix = "gh" # gh__search_issues, ...
[[chat.mcp]]name = "state"url = "http://127.0.0.1:8765/mcp"headers = { Authorization = "Bearer ..." }A malformed table is grammar (exit 2, nothing spawned); a server that
cannot be reached exits 1 naming it; a tool named like one chat already
has (docs, memory, the skills pair, recall_turn, search_history) is refused until
the table sets prefix. playground runs the agent file as written and
reads no table; chat --agent FILE adds the servers. The session banner
names each server and its tool count.
No flags means this project. Every verb below resolves the working
directory’s layout (user:<login> at /user, user:<login>/proj:<slug>
at /project, the same pair a registered agent’s sessions get) when no
--scope or --mount names a mount; NEOSIAN_SCOPE is --scope’s
environment twin. The store is the home unless --root, --url or the DSN names
one.
Try an agent: playground and eval
Section titled “Try an agent: playground and eval”neosian playground AGENT_FILE [--model M | --menu] [--resume ID] [--json]neosian eval SUITE [--json] [--output DIR]playground: your agent file (it exportsconfiguration, anAgentConfig) under chat’s run tier, exactly as written: its tools and its prompt, without the resident agent’sdocstool (neosian chat --agent FILEis the path that adds it). The model is--model, else the file’s own;--menupicks it from a menu on a terminal, never beside--modeland never without a terminal (exit 2). Piped stdin runs one turn and prints the answer,--jsonthe envelopechatprints; a terminal opens a session. Turns persist under the home, and a file that names no memory gets this directory’s layout.eval: runs a YAML suite over its matrix and exits 1 when a case fails, so it gates CI;--jsonprints the artifact’s document and--output DIRnames the directory it lands in (.neosian/evalsby default). The paths a suite names (agent:, a variant’sprompt:) resolve beside the suite file, so it runs from any directory; a model listed twice, or a YAML value JSON cannot hold (an unquoted date), is refused at load. The progress tree draws on a terminal only. Comparing models side by side is the suite’smodels:axis.
Every prompt has a flag. A menu or a prompt is a terminal’s
convenience over a flag that exists: --menu picks what --model
names, configure prompts for what --provider NAME --key - takes, a
session’s turns are what a PROMPT or piped stdin carries. --json
never opens a prompt: it prints one object, so chat and playground
refuse it with no turn (exit 2) and bare configure lists.
Memory from the shell
Section titled “Memory from the shell”neosian memory <command> is the shell transport over the same
dispatcher the function tool, the native Anthropic declaration, the
MCP server, and the state process’s HTTP wire execute. An agent with nothing but shell access
operates the same memory the runtime transports serve.
python -m neosian.memory is the sandbox-safe twin for a venv whose
bin is not on PATH.
The grammar
Section titled “The grammar”neosian memory view [PATH] [--view-range START END] # PATH defaults to /neosian memory create PATH --content TEXTneosian memory str_replace PATH --old-str TEXT --new-str TEXTneosian memory insert PATH --insert-line N --insert-text TEXTneosian memory delete PATHneosian memory rename OLD_PATH NEW_PATHneosian memory maintain [--model MODEL] [--min-age-days N]neosian memory versions PATH [--limit N]neosian memory redact PATH [--all]neosian memory revert PATH --version Nview / renders the memory index: the first command to try. - as
the value of --content / --new-str / --insert-text reads stdin
(the heredoc idiom; exactly one payload flag per command):
neosian memory create user/prefs.md --content - --scope user:me <<'EOF'User prefers concise answers.EOFStore flags (on every command)
Section titled “Store flags (on every command)”| flag | meaning |
|---|---|
--root DIR |
FileStore root (created on first write); default: the home, ~/.neosian or $NEOSIAN_HOME |
--url URL |
the state process instead of a root; its token in NEOSIAN_CLIENT_TOKEN |
--scope SCOPE |
single read-write mount of SCOPE at /memories (the sugar); NEOSIAN_SCOPE is its environment twin |
--mount scope=...,path=... |
explicit mount; repeatable; append ,ro (read-only) or ,eo (edit-only) |
| (neither) | this directory’s project layout: user:<login> at /user, user:<login>/proj:<slug> at /project, the pair --level project installs render; a directory with no name refuses at exit 2 (the two doors a client spawns, neosian mcp and neosian record, serve /user alone there instead) |
--actor NAME |
who writes, <kind>:<id> (default cli:local; cli:<host> names the agent driving the shell) |
--schema NAME |
Postgres schema (Postgres only) |
Postgres arrives only through the NEOSIAN_POSTGRES_DSN environment
variable; there is no --dsn flag (argv is world-readable), and the
state process through --url with NEOSIAN_CLIENT_TOKEN set the same
way. Exactly one of the three stores is named; the others are refused. An edit-only mount (eo) fixes its document set:
existing documents stay editable, but nothing may be created, deleted,
or renamed there (pre-created layouts the agent works within).
Exit tiers, everywhere
Section titled “Exit tiers, everywhere”- 0: success.
- 1: the command ran and failed; a corrective failure rendered
as
error: [code] messageplus ahint:line on stderr. - 2: the invocation was wrong: grammar, an unknown name, an invalid scope or mount. Nothing is constructed on this tier.
- 130: interrupt.
stdout carries the artifact; stderr carries guidance, so redirecting
stdout always captures something well-formed. With --json anywhere in
argv, a tier-2 error also prints one {"error": "usage", "hint": …}
object on stdout; the tier and the stderr text stay.
On the six commands, --json prints the memory tool’s result envelope
verbatim, one JSON object on stdout, exit 0/1 by its success field:
neosian memory view / --root .neosian/memory --scope user:me --jsonmaintain and the operator verbs print their own envelopes instead
(described below), still exactly one JSON object on stdout. Argv-tier
errors (exit 2) stay argparse text on stderr: a shell answers grammar
before any envelope exists.
maintain: the gardener
Section titled “maintain: the gardener”maintain is not one of the six dispatch commands: it runs the
maintenance pass over the writable mounts (neosian docs memory).
Keyless by default (prune empty documents, merge byte-identical
duplicates keeping the oldest), and --model MODEL adds the semantic
pass (merge overlapping, prune stale, promote), which needs that
provider’s API key: a missing key is refused at construction, never a
silent half-pass. --min-age-days N (default 7) protects recently
updated documents from deletion. Its --json envelope is its own,
{"writes": [...], "model", "usage", "cost_micro_usd"}, and a
requested model stage that fails exits 1 and says so on stderr while
the deterministic actions stand.
The operator verbs: audit and remedy
Section titled “The operator verbs: audit and remedy”versions, redact and revert sit beside maintain on the operator
side of the line: keyless acts over the store, never part of the
agent-facing six-command vocabulary.
versions PATH [--limit N] lists a document’s version rows newest
first. Text output is the audit trail without content: one line per
row (version, action, actor, timestamp). --json carries every row’s
full content: that is the point-in-time read, and it means history
reveals everything a document ever held. redact is the only eraser.
Empty history is an answer (exit 0), not an error.
redact PATH [--all] clears content everywhere for one document,
current state and every version row alike, preserving the audit skeleton
(paths, versions, actors, timestamps). A mount root redacts the whole
scope, but only with the explicit --all; without it the grammar
refuses. Redaction is the one irreversible act: the skeleton is
deliberately not restorable, and revert refuses redacted history.
Its --json envelope is {"path", "scope_wide", "matched"}.
revert PATH --version N undoes one write: N names the row to
undo (find it with versions) and must be the newest. One rule covers
every case (no live document before row N means delete, otherwise the
prior content comes back) and the revert appends a new version row,
never rewriting history. Its --json envelope is the write receipt’s
fields (command, path, version, previous_path).
Search the history: neosian search
Section titled “Search the history: neosian search”neosian search TERMS... [--conversation ID]... [--limit N] [--json]
answers “where was this said” across every conversation the store
holds, newest first: a turn matches when it holds every term as a
case-insensitive substring (message text, tool calls and their
arguments, tool results; DESIGN §32, the one rule neosian docs stores
states). The words are the terms, so no quoting is needed; --conversation
narrows to one conversation and repeats; --limit is the newest N, 1 to
500 (default 20). Each hit is one line, [<id> #<turn>] <stamp> <actor> <snippet>, and --json carries {query, conversations, limit, client, hits: [{conversation_id, turn, created_at, actor, snippet}]}; on a
terminal the hits render as a table. It takes the store selection above
(--root, --url, or the DSN) and answers identically on every
substrate; there is no --scope, since turns carry none. Exit tiers
hold: no term, a bad id or a limit outside the range is grammar (exit
2, nothing constructed); no hit is an answer (exit 0). Inside an agent
the same search is the search_history tool (neosian docs memory).
The ledger: neosian audit
Section titled “The ledger: neosian audit”neosian audit [--scope SCOPE] [--conversation ID] [--actor A] [--since T] [--limit N] [--json] answers “what was done, by whom, when” for a scope
(default: NEOSIAN_SCOPE, else this directory’s project scope),
newest first: every memory version row (deleted documents included),
every redaction, and one conversation’s turns when named. It takes the
store selection above (--root, --url, or the DSN; --scope is a
raw scope here, not a mount) and answers identically on every
substrate. --actor filters by prefix: claude-code:s1 matches
claude-code:s1#4 and claude-code:s1/conv:x#2. Through the state
process every actor carries the client prefix the daemon asserted.
Exit tiers hold; an empty ledger is an answer (exit 0).
Moving a store: neosian export / neosian import
Section titled “Moving a store: neosian export / neosian import”neosian export DIR writes the store to DIR, whole: every scope and
conversation, version history and redaction trail included, verbatim.
DIR is a FileStore root: cat it, grep it, neosian serve --root DIR it, or neosian import DIR it into any other store: a fresh
root, Postgres by the DSN, or the state process by --url. Both verbs
take the store selection above (the home when none is named) and
--scope S / --conversation C (repeatable) to move only what they
name; naming either moves nothing of the other kind. An import needs
every unit it touches (a scope, a conversation) to be empty in the
target: an occupied one refuses the whole run before anything is
written (memory_conflict / agent_conversation_conflict, reason
target_occupied); nothing merges, nothing overwrites. --json prints
one object (verb, archive, client, units with the per-unit
counts). Exit tiers hold: 2 for a bad name or a missing archive, 1 when
a store refuses, 0 with the report (nothing to export is an answer).
The record: neosian record
Section titled “The record: neosian record”neosian record is what a foreign agent’s hooks call: one hook payload
on stdin per event, hook_event_name saying which. UserPromptSubmit
opens a span, PostToolUse adds a tool round, Stop lands it as one
turn by <agent>:<session_id> in the conversation the session id names
and writes the scope’s sessions document. It takes the store and mount
flags above plus --agent KIND (default claude-code), --spool DIR (default spool/ under the home, never the store) and --project DIR, the directory whose layout is the default (the working directory
when absent; a once-per-machine Claude Code hook line passes
"$CLAUDE_PROJECT_DIR", since a hook’s working directory moves with the
agent’s cd). The sessions document lands in the mount at /project
when there is one, else the first read-write mount; a neosian
Conversation with a writable /project mount writes its own the same
way, so the listing is complete. neosian record install --client claude-code|codex|opencode|muse-code|cursor [--level user|project] [--write] renders or applies the hooks, the mcp install twin
(neosian docs agents). The exit tiers bend once
for the hook’s sake: 2 only for argv, 1 for everything after, so a
broken store never blocks the agent. Startup context is printed on stdout;
Cursor returns it as JSON additional_context and returns {} for other
successful hooks. --json prints the diagnostic envelope
({"event", "session_id", "actor", "disposition", "conversation_id", "turn", "document", "client", "context"}).
One writer per root
Section titled “One writer per root”A FileStore root is owned by one writer at a time. Do not run
neosian memory writes against a root an MCP server, a neosian serve process, or an embedding application is serving: route
multi-writer needs to Postgres or to the state process itself
(--url, so the shell and an agent’s MCP server both write through
the one process that owns the files). The
full rule: neosian docs topology.