A small, self-contained command-line client for the OwnSona MCP memory server. Lets you read from and write to your memory store from the terminal — handy for scripting, bulk operations, and feeding remembered facts into another LLM.
The sources live under cli/.
- What it does
- Building
- Configuration
- Subcommands
- Examples
- Teaching from a long-form text
- Exit codes
- Source layout
Talks to the OwnSona MCP server over HTTPS using JSON-RPC. Maps a
curated subset of the server's MCP tools to subcommands (the rest
are read-mostly diagnostics like count_memories or memory_stats,
and the cleanup-batch tools like forget_batch /
update_memory_batch / find_near_duplicates which are aimed at
LLM-driven cleanup workflows rather than terminal use):
| Subcommand | MCP tool | Purpose |
|---|---|---|
add |
remember |
Store a new memory |
query |
recall |
Find memories by semantic similarity |
search |
text_search |
Plain substring search |
list |
list_memories |
List most-recent memories |
update |
update_memory |
Replace a memory's text/tags/etc |
confirm |
confirm |
Mark a memory as still-current |
reinforce |
reinforce |
Feedback: raise/lower a memory's learned salience |
conflicts |
find_conflicts |
Surface memories that may contradict each other |
relations |
query_relations |
Multi-hop traversal of the relation graph |
forget |
forget |
Soft- or hard-delete a memory |
prompt |
build_context_prompt |
Build an LLM prompt with relevant facts |
import |
remember_batch |
Bulk-load facts from a file |
teach |
(LLM + remember_batch) |
Extract facts from prose via an LLM, then bulk-load |
teach is the only subcommand that calls a generative LLM directly
(see Teaching from a long-form text).
The others stay vendor-neutral — they just talk to OwnSona.
Output is human-readable by default; pass --json (a global flag) for
the raw server response, suitable for piping into jq.
Written in portable C11. One runtime dependency: libcurl (statically
linked where the platform supports it, dynamic otherwise). cJSON is
vendored in cli/src/.
One-line install of build dependencies per platform:
Linux (Fedora):
sudo dnf install gcc make libcurl-devel openssl-devel zlib-develLinux (Debian / Ubuntu):
sudo apt install build-essential libcurl4-openssl-dev libssl-dev zlib1g-devmacOS (Homebrew):
brew install curl openssl@3 pkg-configWindows (MSYS2 UCRT64 shell):
pacman -S mingw-w64-ucrt-x86_64-toolchain \
mingw-w64-ucrt-x86_64-curl \
mingw-w64-ucrt-x86_64-opensslThen:
cd cli
make # builds ./ownsona (or ownsona.exe on Windows)
make test # sanity-check the binary --- doesn't hit the serverThe Makefile detects the platform via uname -s and asks
curl-config --static-libs for static linker flags. When those flags
yield a true single-file binary (typical on macOS Homebrew and on
MSYS2 UCRT64), you can copy ownsona to another machine of the same
OS and run it as-is. When the platform's libcurl package doesn't
ship the static .a (typical on Fedora and Debian/Ubuntu), the build
falls back to dynamic linking and prints a warning at link time —
you'll need libcurl on the target machine.
The binary is small (~80–100 KB dynamically linked, ~3-5 MB statically).
The CLI reads two kinds of settings:
- MCP server credentials — always needed.
- LLM credentials — only needed by the
teachsubcommand.
If you do not pass --config PATH and do not set $OWNSONA_CONFIG,
the CLI looks for its config file at an OS-specific default path:
| OS | Default config path |
|---|---|
| Linux / BSD | ~/.config/ownsona/config.ini |
| macOS | ~/Library/Application Support/ownsona/config.ini |
| Windows | %LOCALAPPDATA%\ownsona\config.ini |
Create the parent directory if it doesn't already exist:
# Linux / BSD
mkdir -p ~/.config/ownsona
# macOS
mkdir -p ~/"Library/Application Support/ownsona":: Windows (cmd.exe)
mkdir "%LOCALAPPDATA%\ownsona"If the default file is missing, the CLI continues silently — it will only complain when something it needs (server URL, credentials) is still unset after the environment and CLI overrides are applied.
For every setting, highest priority wins:
- CLI flag (
--config,--server,--token,--model,--subject, …). - Environment variable (
OWNSONA_SERVER,OWNSONA_TOKEN,OWNSONA_LLM_API_KEY,OWNSONA_LLM_MODEL,OWNSONA_LLM_BASE_URL,OWNSONA_SUBJECT,OWNSONA_CONFIG). - Config file (at
$OWNSONA_CONFIGif set, otherwise the OS-specific default above). - Built-in defaults (only for the LLM-side keys:
model = gpt-4o,base_url = https://api.openai.com/v1,subject_name = the user).
INI-style. # and ; start comments. [sections] are parsed but
ignored (forward-compatibility placeholder). Quotes around a value
are stripped.
A template is shipped at cli/config.ini.example;
copy it to the OS-specific default path (see the
Default config file location table),
fill in server_url, and run ownsona auth login to populate the
OAuth credentials.
# --- MCP server (required for every subcommand) ---
server_url = https://your.host/mcp
# --- keep-flag administration (used only by enumerate k/r) ---
# Must match the server's OwnsonaAdminSecret. Env: OWNSONA_ADMIN_SECRET.
# admin_secret = <shared-secret>
# --- LLM (used only by `teach`) ---
# Required only when running `teach`.
llm_api_key = sk-...
llm_model = gpt-4o # default; --model NAME overrides per-run
llm_base_url = https://api.openai.com/v1
subject_name = Blake # how `teach` refers to you in facts
# --- OAuth state (auto-managed by `ownsona auth login`) ---
# You don't write these by hand; `auth login` populates them and the
# per-request refresh path keeps them current.
# oauth_client_id = ...
# oauth_refresh_token = ...
# oauth_access_token = ...
# oauth_access_token_expires_at = ...
# oauth_authorization_server = ...
# oauth_resource = ...The CLI uses OAuth 2.1 (auth code + PKCE + RFC 7591 dynamic client
registration + RFC 8707 resource indicators) against the AS that
protects your server_url. Discovery is automatic via RFC 9728.
First-time setup:
ownsona auth loginauth login opens your browser to the server's /oauth/authorize
endpoint. You log in (with OWNSONA_LOGIN_USERNAME /
OWNSONA_LOGIN_PASSWORD from the server's application.ini —
single-user server), click Allow on the consent page, and the CLI
captures the resulting access + refresh tokens via a one-shot
localhost listener. The tokens get written into your config file
atomically.
If the browser doesn't open (headless / SSH), the CLI also prints the URL to stderr — paste it into any browser that can reach the configured server.
From then on, every subcommand uses the saved tokens. The access
token expires after ~1 hour by default; before each call, the CLI
trades the refresh token for a fresh one and rewrites the config.
The user sees no interruption until the refresh token itself
eventually expires (default 30 days), at which point auth login
needs to be re-run.
Check current status:
ownsona auth statusprints the configured server, the AS issuer, the client_id, and how many seconds remain on the cached access token.
Legacy static-bearer mode: if your server issues long-lived
bearer tokens out of band (e.g. an external IdP that you've taken
care of separately, or a non-OAuth MCP server), set token = <jwt>
in the config file. When token is set, the CLI uses it verbatim
and skips the refresh path entirely.
The llm_api_key can be the same OpenAI key the server uses for
embeddings, or a separate key if you want to track CLI costs
independently.
Global options come before the subcommand name; subcommand options come after.
ownsona [--config PATH] [--server URL] [--token TOKEN] [--json] <command> [...]
Run ownsona --help for the top-level help, and
ownsona <command> --help for per-command help.
Curation:
ownsona display [ids] [-k <YNU>] show id, keep, text (id order)
ownsona enumerate [ids] [-k <YNU>] walk + act on each memory
ownsona change <id> "<new text>" replace one memory's text
ownsona delete <ids> hard-delete selected memories
ownsona add "<text>" store a memory (alias: remember, new)
ownsona stats store overview: counts, keep breakdown, tags
ownsona dump [file] write all memories as JSON (file or stdout)
ownsona restore <file> re-insert memories from a dump file
Other:
ownsona query "<question>" semantic recall
ownsona search "<substring>" substring search
ownsona list recent memories
ownsona update <id> "<new text>" replace a memory
ownsona confirm <id> refresh last_confirmed_at
ownsona reinforce <id>... [--delta N] [--query "..."] feedback: adjust learned salience (+ contextual ranking)
ownsona conflicts [--threshold N] surface possibly-contradicting memories
ownsona relations "<entity>" [--max-hops N] traverse the relation graph (multi-hop)
ownsona forget <id> soft-delete (--hard to drop)
ownsona prompt "<user prompt>" build an LLM-ready prompt
ownsona import FILE bulk-load JSON or one-per-line
ownsona teach FILE extract facts from prose via LLM
ownsona auth login OAuth bootstrap (run once)
ownsona auth status show current credentials state
ownsona -h | -? help
ownsona -v | -V version
ownsona (no command) prints a short banner
ids is a single token: an id (5), a range (5-9), a list (5,7,9),
or any combination separated by commas (5-9,12,20-$). $ means the
last id. Omit it to select all. Ids that don't exist are skipped.
Every memory has a keep flag: Y (protected — cannot be changed or
deleted by anyone, including LLM clients), N (not protected), or U
(unspecified, the default). -k <YNU> filters display/enumerate by
flag, e.g. -k NU shows only the No and Unspecified rows.
Only this CLI can change the flag, and only with admin_secret set in
the config (matching the server's OwnsonaAdminSecret). In enumerate:
k protects (Y), r un-protects (N), d hard-deletes, c changes the
text, ?/h lists commands, <Enter> skips, q quits. To delete a
protected memory, r it first.
Changing the flag is gated by a shared secret that must be set in both
places, with the same value. Until it is, set_keep fails closed:
the CLI reports "admin_secret is not configured", and even with the CLI
side set, the server rejects the call as "Unknown tool: set_keep".
-
Pick a strong secret, e.g.:
openssl rand -hex 32
-
Server — set it in
application.iniand restart so it's picked up (it's read once at startup; no rebuild needed if you edit the deployed copy, but also add it to the source-treeapplication.iniso future WAR builds keep it):OwnsonaAdminSecret = <the secret> -
CLI — set the same value in your config (
admin_secret), or exportOWNSONA_ADMIN_SECRET:admin_secret = <the secret>
Leave the server's OwnsonaAdminSecret unset to disable keep changes
entirely (fail closed). The CLI config holds a secret, so keep it locked
down: chmod 600 ~/.config/ownsona/config.ini.
add and change add a trailing period to the text if missing and stamp
source_provider=ownsona / source_client=cli.
ownsona dump backup.json # full JSON snapshot (or: ownsona dump > backup.json)
ownsona restore backup.json # re-insert into the store
ownsona restore backup.json --dry-run # preview without writingdump writes the server's export_memories snapshot — every memory as
JSON, soft-deleted rows included (use --active-only to exclude them).
Embedding vectors are not included; they're re-derived from text on
restore.
restore re-inserts the active rows via remember_batch, preserving
text, tags, source_provider, importance, capture_mode, session_id,
expires_at, and last_confirmed_at, and re-applies the keep flag for
Y/N rows (needs admin_secret). It is content-level, not
byte-faithful: the MCP tool surface can't preserve original ids or
created_at timestamps (the server assigns new ones), and soft-deleted
rows are skipped. It's meant for restoring into an empty store —
against a populated one the default insert policy creates duplicates
(use --dedup skip_if_near to merge). (importance is preserved only
when the dump was produced by a server that includes it in the export
output.)
--tags T1,T2,...— comma-separated tags. Used byadd,update,query(as a filter),import(default for line-format files),teach.--limit N— cap on returned rows. Used byquery,search,list,prompt.--dedup POLICY— one ofinsert,skip_if_near,ask(defaultaskforadd,skip_if_nearforteach).--expires-at,--last-confirmed-at— ISO 8601 timestamps onaddandupdate.
$ ownsona add "My dog's name is Mochi." --tags family,pets
Ok (id=42)
$ ownsona query "what is my dog's name"
1 match
[42] score=0.913 My dog's name is Mochi.
tags: family pets
at: 2026-05-16T12:00:00Z
$ ownsona prompt "What should I name my new dog?" --max-chars 1000 \
| claude --no-mcp
$ cat facts.txt
# preferences
I prefer concise answers.
I work in Pacific Time.
My favorite editor is Emacs.
$ ownsona import facts.txt --tags preferences --dedup skip_if_near
inserted=3 duplicates=0 errors=0 (total=3)
[0] id=43 Ok
[1] id=44 Ok
[2] id=45 Ok
$ cat facts.json
[
{"text": "I live in Texas.", "tags": ["personal"], "expires_at": "2030-01-01T00:00:00Z"},
{"text": "I drive a Subaru.", "tags": ["personal"], "importance": 0.3}
]
$ ownsona import facts.json --dedup skip_if_near
$ ownsona add "My dog's name is Mochi." --dedup skip_if_near
Ok (id=99)
$ ownsona forget 42 --reason "misremembered name" --replaced-by 99
Forgotten (id=42)
A future add of "Coco is my dog's name" will now show up with a
previously_corrected block, warning that this fact was already
forgotten.
$ ownsona query "where do I live"
1 match
[17] score=0.842 Blake lives in Austin.
$ ownsona reinforce 17
reinforced 1 memory
[17] salience=0.775 use_count=1
A negative delta demotes a memory that proved unhelpful:
ownsona reinforce 17 --delta=-1. Reinforcement never edits text and
never deletes; it only nudges the learned ranking weight.
Pass --query with the question the memory answered to also teach the
store which memory fits which kind of question (contextual ranking):
$ ownsona reinforce 17 --query "where do I live"
reinforced 1 memory
[17] salience=0.775 use_count=1
Future recalls similar to "where do I live" will rank memory 17 higher.
$ ownsona relations "my manager" --max-hops 2
2 relations
Dana --[manages]--> the user (from memory [12])
Dana --[is married to]--> Sam (from memory [40])
The graph is populated by a background extraction job (off by default; set
GRAPH_EXTRACTION_ENABLED=true and the LLM_* keys, and uncomment the
ExtractRelations crontab line). With no relations extracted yet,
relations returns nothing — fall back to query.
$ ownsona conflicts --threshold 0.85
1 conflict group
group (max_similarity=0.913, pairs=1):
[42] score=0.913 Blake lives in Dallas.
[17] score=0.913 Blake lives in Austin.
conflicts only surfaces same-topic candidates (semantically close and
sharing a tag) — it does not judge which is right. Resolve by confirming
the correct one, or by re-stating the fact with add on a client that
passes supersedes (the old fact is wrong) or downweights (merely
outdated).
ownsona teach reads a plain-text file (autobiography draft, journal,
notes, anything narrative) and uses an LLM to extract durable
third-person facts about you, then submits them as a batch of
memories.
- Reads the file and splits it into chunks (default 4000 characters, broken at paragraph then sentence boundaries).
- For each chunk, POSTs to your configured chat-completion endpoint
(
{llm_base_url}/chat/completions) with a system prompt that instructs the model to extract durable facts as a JSON array. - Collects all returned facts.
- Default behavior: dry-run. Prints the JSON to stdout (or
--output FILE) so you can eyeball before committing. - Pass
--committo actually submit the facts viaremember_batch, in 200-at-a-time chunks, withdedup_policy=skip_if_nearby default so re-runs after manuscript edits don't double up.
teach needs the LLM config keys filled in. Set llm_api_key either
in your config file (see Configuration for the
OS-specific default path) or via $OWNSONA_LLM_API_KEY.
| Flag | Purpose |
|---|---|
--subject NAME |
How to refer to you in extracted facts (default the user; recommend setting to your name) |
--model NAME |
Override the LLM model just for this run |
--chunk-size N |
Characters per LLM call (default 4000; valid 500–32000) |
--tags T1,T2 |
Apply these tags to every extracted fact |
--dedup POLICY |
dedup_policy for the resulting remember_batch calls (default skip_if_near) |
--max-facts N |
Sanity cap on total extracted facts (default 10000) |
--commit |
Actually insert the facts (default is dry-run) |
--yes |
With --commit, skip the y/N confirmation prompt |
--output FILE |
In dry-run, write JSON here instead of stdout |
# 1) Extract to a file for review.
ownsona teach my-life.txt --subject Blake --output review.json
# 2) Open review.json, edit if you want, drop or rephrase anything.
# 3) Submit the reviewed file via `import` (no second LLM call).
ownsona import review.json --dedup skip_if_nearThis two-step teach --output → human review → import avoids
spending a second LLM call if you decided to edit the result. You
can also just ownsona teach my-life.txt --commit if you trust the
extraction.
A 100K-word autobiography is about 150 chunks of 4000 chars each. Roughly:
| Model | Approx. cost |
|---|---|
gpt-4o-mini |
$0.05 – $0.15 |
gpt-4o |
$1 – $3 |
Numbers vary with chunk size and how dense the extraction is.
Dry-runs cost the same as commits — the LLM call is the expense, the
remember_batch insertion is free.
The --model flag and llm_base_url config key let you point
teach at any service that exposes OpenAI's /chat/completions
shape: OpenAI itself, OpenRouter, Anthropic via a compat proxy, a
local Ollama server, etc. Default is OpenAI's endpoint.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Server error, transport failure, or ok:false tool result |
| 2 | Usage error (bad flag, missing arg, etc.) |
cli/
├── Makefile portable per-OS build
├── README.md short pointer to this file
├── config.ini.example config template
├── src/
│ ├── cJSON.{c,h} vendored, MIT
│ ├── config.c INI loader + env/CLI overrides
│ ├── http.c libcurl wrapper for the MCP server
│ ├── llm.c libcurl wrapper for the chat-completion endpoint
│ ├── mcp.c MCP JSON-RPC envelope + unwrap
│ ├── main.c argv parsing, subcommand dispatch, output
│ └── cmd_*.c one file per subcommand
└── include/
└── ownsona.h shared declarations
cJSON is vendored from https://github.com/DaveGamble/cJSON (MIT
licensed; retains its own license header). To update, replace
cli/src/cJSON.c and cli/src/cJSON.h with the upstream release.
The rest of the CLI is BSD 2-Clause, same as the OwnSona server.