xmemcli — xmemory command-line client
Single-command CLI for reading from, writing to, and managing xmemory instances over the HTTP API at $XMEM_API_URL (default https://api.xmemory.ai), using an API key from the Console UI or xmemcli auth login.
Full command reference is included below.
Usage
xmemcli auth login [--no-browser] [--timeout S] [--console-url URL] [--callback-port N] [--email ADDR [--org-name NAME]]
xmemcli auth logout
xmemcli auth status
xmemcli read <query>
xmemcli write <text>
xmemcli write-status <write_id> [<id> …]
xmemcli quota
xmemcli instance templates
xmemcli instance create --name NAME (--template ID | --schema-file PATH | --schema-stdin)
xmemcli binding list [--autoload]
xmemcli binding add <instance_id> [--tier autoload|available|off] [--scope project|user]
xmemcli binding remove <instance_id> [--scope project|user]
xmemcli context [--text] [--max-tokens N] [--no-live-state]
xmemcli trace ls [--instance ID]… [--trace-type T]… [--hours-back N] [--user-id U] [--api-key-id K] [--limit N] [--cursor TS_MS:TRACE_ID] [--counts-only]
xmemcli trace ls --search [--text …] [--request …] [--response …] [--object NAME]… [--field NAME]… [--status S]… [--trace-id ID]… [--session-id S] [--instance ID] [--trace-type T]… [--hours-back N]
xmemcli trace get <trace_id> [--trace-type write|read|create|update|evolution]
Global flags / env: --api-key / $XMEM_API_KEY, --instance-id / $XMEM_INSTANCE_ID, --api-url / $XMEM_API_URL, --rc-dir / $XMEM_RC_DIR, --binding-dir / $XMEM_BINDING_DIR, --json, --verbose. See xmemcli help options. trace … additionally take --console-url / $XMEM_CONSOLE_URL (the Console origin that serves them; inferred for the public and local pairs).
Credential resolution: --api-key → $XMEM_API_KEY → nearest .xmemrc.json (walk up from --rc-dir toward $HOME). Origin: --api-url → $XMEM_API_URL → rc api_url → default. The mcp transport resolves its own origin the same way: --mcp-url → $XMEM_MCP_URL → rc mcp_url → default, where mcp_url is what the API told auth login about itself.
context is the exception: it runs unattended from a session-start hook, with no person and no chosen directory behind it, so it resolves the credential from $HOME only — a project .xmemrc.json is used only when --api-key / $XMEM_API_KEY or --rc-dir / $XMEM_RC_DIR names it explicitly. That stops a cloned repository shipping the key and origin its own context is fetched with.
Two local files, same discovery rule, different jobs: .xmemrc.json holds the credential (0600, never committed, nearest wins); .xmemory.json holds the instance binding (0644, meant to be committed at project scope, every scope merged). binding commands are local-only — no network, no credential.
Read / write modes
| CLI | API field |
|---|---|
xmemcli write --extraction fast (default) |
extraction_logic: "fast" |
xmemcli write --extraction deep |
extraction_logic: "deep" |
xmemcli write --no-wait |
async write; poll with write-status |
xmemcli read --read-mode single (default) |
mode: "single-answer" |
xmemcli read --read-mode raw |
mode: "raw-tables" |
xmemcli read --read-mode xresponse |
mode: "xresponse" |
xmemcli read --related-types |
include_related_types: "types" — the response's related_types object is kept in the curated output |
xmemcli read --related-types-depth 2 |
include_related_types: "types" and related_types_depth: 2 — the catalog entries carry their distance and, below the last level, their own edges |
--read-mode selects the server-side answer shape; --json and the TTY rule govern stdout format.
Curated output
The CLI does not echo raw API payloads. Server internals (sql, diff_plan, extraction trees) are never emitted. Read responses keep pending_suggestions (schema-evolution hint). --verbose adds trace_id and console_url when present.
trace … is the one deliberate exception. Those commands return the Console's own operation documents — the paged operations list, the Inspect search, and the per-operation detail — verbatim, because the Console already curates them per caller (a super admin sees LLM calls and raw token counts; everyone else does not). They are JSON on every stream and carry every internal id the Console does: trace_id, instance_id, session_id, user_id, api_key_id, and on detail documents source_id, migration_id, llm_call_id, gateway_request_id, …
Trace — the Console's operation traces, from the CLI
xmemcli auth status # super_admin: which Console view trace will answer with
xmemcli org list instances # the instances this key can see — what trace ls is scoped to
xmemcli trace ls # every visible instance, newest first, one page
xmemcli trace ls --instance "$ID" --trace-type write --trace-type read --limit 50
xmemcli trace ls --counts-only --hours-back 168 # counts, per-instance counts and series, no rows
xmemcli trace ls --search --text "Ada" --instance "$ID" # Inspect search — opt-in, more expensive
xmemcli trace get 0001_abc --trace-type write # one operation, every internal id
Paging is a cursor the previous page returns:
PAGE=$(xmemcli trace ls --instance "$ID" --limit 100)
NEXT=$(echo "$PAGE" | jq -r '"\(.next_before_ts_ms):\(.next_before_trace_id)"')
xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
--search has no cursor — the Console's Inspect endpoint doesn't page; it returns the newest matches up to a ceiling and the CLI warns when a response sits at it. A key sees the operations of the instances it can list — never more than its owner sees in the Console; a super admin's key sees what a super admin sees. Full reference: .
Output personality — JSON vs pretty
- Non-TTY stdout or
--json: exactly one JSON document. - Interactive TTY without
--json: short one-line summaries per command.
Scripts can rely on xmemcli … | jq without --json.
Example session
export XMEM_API_KEY="xmem_..."
export XMEM_INSTANCE_ID="<uuid>"
xmemcli write "John likes orange"
xmemcli read "what colour does John like?"
ID1=$(xmemcli write --no-wait "Ivy likes amber" | jq -r .write_id)
ID2=$(xmemcli write --no-wait "Lev likes indigo" | jq -r .write_id)
xmemcli write-status "$ID1" "$ID2"
Install
Requires Python 3.12+. Stdlib only — no runtime dependencies.
Preferred:
uv tool install xmemcli
When uv is not available:
pip install xmemcli
Then:
xmemcli version
xmemcli auth login
Version: 1.3.0.
xmemcli auth flow — PKCE handoff, email sign-in, and credentials
The CLI is human-approved for key acquisition: an AI-assisted IDE may run reads, writes, and inventory, and may even initiate key acquisition, but a key is only ever issued after one explicit human approval — a click in the browser (PKCE handoff) or an approval of a sign-in email (--email). A key must never enter the AI transcript — it is handed to the CLI out-of-band via xmemcli auth login or environment / .xmemrc.json, and the CLI prints only its prefix.
Two acquisition paths share xmemcli auth login:
- Browser PKCE handoff (default): the CLI sends the human to
/xmemcli/<challenge>on the Console, listens on an ephemeral127.0.0.1port named in that URL, and the Console delivers a signed short-lived code (not the key) to that callback — in the background where the browser allows it, otherwise through a link the human clicks. - Headless email sign-in (
--email <addr>): the CLI plays the waiting-browser role of the Console's cross-device email sign-in and then mints its own key — no browser on this machine. See Email sign-in below.
Scope. Shipped in this package:
.xmemrc.jsondiscovery, PKCEauth login/logout/status, emailauth login --email,/whoamiconsumption, andquota. Server endpoints:POST /cli/handoff,POST /cli/token, and — for the email path — the Console sign-in and accounts routes listed below (all pre-existing Console surfaces; the email path added no server endpoint).
Acquiring a key
The human either:
- Provides a key they already hold (
$XMEM_API_KEY,.xmemrc.json, or paste at login), or - Creates a key in the Console and clicks "Use this key in the CLI" (when the page was opened via
/xmemcli/<challenge>), or - Approves a sign-in email for an
auth login --emailrun, after which the CLI mints its own key.
During onboarding, the Console may auto-handoff an onboarding key while a short-lived cookie bridge is active.
Why a code, not the key
Plaintext keys in URLs leak to history and proxies. Handoff uses PKCE + signed code + localhost callback:
- CLI mints
verifier, deriveschallenge=base64url(SHA256(verifier)), opens/xmemcli/<challenge>. - Human clicks "Use this key in the CLI" → Console
POSTs the key to/cli/handoffbound to the challenge → receives a short-livedcode. - Browser delivers to
http://127.0.0.1:<port>/callback?code=…&state=<challenge>— key never in the URL. - CLI
POST /cli/tokenwith{ code, verifier }→ receives key → writes0600.xmemrc.json.
Sequence
AI-assisted IDE ── xmemcli auth login ──► CLI: PKCE verifier + localhost listener
Human ── opens Console URL ──► creates key → "Use this key in the CLI"
CLI ◄── local callback ── redeems code + verifier → writes .xmemrc.json
AI-assisted IDE ── xmemcli read/write ──► CLI resolves key; never handles plaintext key
Email sign-in — --email
xmemcli auth login --email <addr> [--org-name <name>] acquires the key with one human action and no browser on this machine — the flow an agent-driven IDE uses. The CLI takes the waiting-device role of the Console's cross-device email sign-in; every endpoint below already served the Console before this flag existed. The fully headless path requires the server to have cross-device hand-off enabled; a server without it only offers the typed six-digit code, so a run with no interactive terminal is refused there (after cancelling the attempt it had to start to find out).
POST /api/kingdom-signin-request— the server emails a six-digit code + one-click link; the CLI holds the signed pending cookie in memory. The reply's matching code is consumed only as the hand-off signal and is not printed (the compare UI is retired across all sign-in surfaces for now).- The human opens the email on any device and Approves. This is the single human action.
- The CLI claim-polls
POST /api/kingdom-signin-claimon the Console's own waiting-tab cadence (a short grace, ten fast polls, then a slow poll; transient network failures count as missed polls, not aborts) and exchanges the proof viaPOST /api/kingdom-signin-mintfor the browser-session cookie — held in memory only, never written to disk. - With the session, the CLI runs the Console onboarding's core sequence:
GET /api/check-user; when the address has no account,GET /api/suggest-slug(server-derived org name,--org-nameoverrides; ignored for an existing account) andPOST /api/public-signup— note that creating a new organisation requires the deployment to allow public registration, which the hosted service currently does not: an existing account gets a key, a new address gets the server's "organisation creation is not available" refusal (the Console's admin-fallback signup arm is not implemented here); thenGET /api/accounts/users/by-email,GET /api/accounts/api-roles(the seeded default role, or the org's only role — an ambiguous set is refused, never guessed), andPOST /api/accounts/api-keyswith no cluster list — the key attaches to every cluster the org has at mint time. Clusters created later are attached by a server-side backfill that repairs only keys already linked to every other cluster in the org; an org with no clusters at mint time gets a key attached to nothing. The key is namedCLI email login (<hostname>, <timestamp>)so every mint stays individually identifiable and revocable on the Console's API Keys page. - The key is written to
.xmemrc.json(0600), stdout carries the prefix only, and the session cookie dies with the process.
The wait is bounded by --timeout — 600 seconds for this path by default, which is also a ceiling: nothing stays claimable past the server's pending window, so larger values are clamped. The emailed one-click link and six-digit code themselves expire at about five minutes; the wait continues past that because an approval given in time stays claimable, and the terminal says when the emailed artifacts have lapsed. The Console origin must be https (or a loopback host, for local development). The Console must also match the API origin the key will be used against: with a non-default --api-url the CLI only infers a Console where the layout determines one — a single-origin /xmem base names its /console sibling on the same host, and that sibling beats $XMEM_CONSOLE_URL on the same call — and the local-dev convention pairs :8000 with :8002 — otherwise it refuses to guess (pass --console-url; $XMEM_CONSOLE_URL only when --api-url was not given). Because that inference exists, a single-origin deployment's rendered sign-in command names the API alone; every other layout still renders the --api-url/--console-url pair, since naming one without the other is worse than naming neither (api/agent_setup_renderer.py), and an explicitly named Console that provably mismatches the API origin (mixed loopback-ness, or a default-production half paired with a non-default one) is refused rather than trusted.
When the server offers no matching code on the request reply (cross-device hand-off disabled, no public Console origin configured, or a transient degradation), there is nothing for a headless starter to claim — the emailed link then signs in the approving browser — so the CLI falls back to the emailed six-digit code typed into the terminal (POST /api/kingdom-signin-verify), still a single human action. That fallback needs an interactive stdin; a non-interactive run cancels the attempt it just started (the mode is only known from the server's reply, after the email is on its way) and refuses with instructions — the typed code dies with that cancel, but on such a server the one-click link has nothing durable to revoke and stays redeemable in a browser until it expires (about five minutes), which the terminal states plainly.
A credential that already resolves (via /whoami) to the requested address at the requested origin — checked across --api-key, $XMEM_API_KEY, and the rc, in that order — short-circuits without sending any email, so re-running the command is free (a matching $XMEM_API_KEY is copied into .xmemrc.json so the project gains a durable credential; an explicit --api-key is never persisted); when the check itself cannot be made (the API is unreachable) the CLI refuses to mint rather than stranding a duplicate key. Every abort of a started attempt — Ctrl+C, timeout, or an error mid-wait — best-effort-cancels it (POST /api/kingdom-signin-cancel, retried briefly) and reports what the cancel actually achieved: a confirmed cancel of a cross-device attempt makes the emailed link unapprovable; an unconfirmed one is reported as such; and on a hand-off-disabled server the link has nothing durable to revoke — it stays redeemable (in the browser that opens it) until it expires on its own, and the terminal says so.
Credential store (.xmemrc.json)
Project-local .xmemrc.json: {email, api_url, api_key}. Found by walking up from --rc-dir / CWD, ceiling $HOME.
Those three keys are required and must be strings. Anything else in the file is ignored, not rejected (since 0.0.12): the rc is the only durable credential store, so a file this CLI cannot load is a CLI that cannot run any command at all — which would make adding a field to the file a breaking change far wider than the feature that added it.
Since 1.1.0 the file may also carry mcp_url — the MCP resource the API reported for itself when the key was issued, written only when the server names one. It is what lets xmemcli mcp reach an on-premise MCP host: that host is in no table the client was built with, and the rc is the one anchor a cloned .mcp.json does not control. A blank or non-string value is read as absent. It outranks the single-origin sibling inference below — a deployment naming itself beats a shape we derived — and both beat the built-in default. The mount path is part of the destination: https://host/mcp authorises that mount, not the sibling services beside it — and the instance id is canonicalised as a UUID before it is appended, so nothing following the mount can walk back out of it. Every lookup that reads mcp_url refuses redirects rather than merely dropping the credential across them, because what it learns is a host the key is later sent to. Every mutation of the file — write, in-place update, and logout's delete — is serialised under an exclusive lock on its containing directory, and an update is additionally bound to the identity and origin it was decided from. What protects an in-place update is that comparison, which is portable; the lock narrows what remains — the moment between comparing and replacing — where the platform offers one. A failed record is a warning, a failed de-authorisation fails the command. The enrichment write after login is conditional on the same comparison, so logging out or signing in again while it is in flight is not undone by it.
Resolution order: --api-key → $XMEM_API_KEY → .xmemrc.json. Origin: --api-url → $XMEM_API_URL → rc api_url → default — except when the key is explicit (--api-key / $XMEM_API_KEY): the rc's api_url is skipped and the origin falls back to the built-in default unless you also set --api-url / $XMEM_API_URL. The localhost-key-at-prod guard applies when the key comes from the rc and an explicit --api-url / $XMEM_API_URL mismatches that rc origin. A base URL's path is preserved, never stripped — on a single-origin deployment the API lives at https://host/xmem, and that mount is part of every request the CLI builds; query, fragment, and userinfo on a base URL refuse normalisation rather than being stripped, so sibling inference cannot run on a shape the caller did not supply. Console resolution for login: --console-url → Console pinned by a positional (or mistaken --api-url) single-origin URL → $XMEM_CONSOLE_URL when --api-url was not given → sibling inference from the call's --api-url when it applies → sibling inference from $XMEM_API_URL → default. An explicit --api-url suppresses $XMEM_CONSOLE_URL for that invocation (a five-host --api-url without --console-url is a usage error, not a fall-through to the env Console). A named API origin from $XMEM_API_URL is held to the same infer-or-refuse rule, so an env-only non-default API without a Console cannot mint at the hosted Console and record that key against the foreign API; an inferred sibling that is remote HTTP is refused under the same HTTPS-or-loopback rule as a positional login URL. The same resolved Console is passed to the post-login /whoami that records mcp_url, so a leftover $XMEM_CONSOLE_URL cannot steer that lookup either. When --api-url is present, $XMEM_API_URL is not consulted for Console inference. Because a mounted base cannot also serve the /api-prefixed spelling of a route, the compatibility candidate in whoami_urls / token_redeem_urls ({base}/api/whoami, {base}/api/cli/token — a root-served deployment's second form) is offered only for a path-less base. The MCP bridge resolves its endpoint as --mcp-url → $XMEM_MCP_URL → the /mcp sibling of the effective API base (--api-url → $XMEM_API_URL → rc → default) → default, and its foreign-origin guard accepts the /xmem ↔ /mcp sibling pair as one deployment.
Identity — /whoami
xmemcli auth status calls GET /whoami with the active bearer key and reports {authenticated, email, key_prefix}. Exit 0 when the key resolves; auth error when it does not.
The AuthApiKey body also carries mcp_url, this deployment's own MCP origin (null where it serves none). The field is always present, null included: an absent key means the server predates it, which is a different answer and is treated as one. auth login reads it from there — on both paths, since the email flow never calls POST /cli/token — and records it in the rc. The lookup is best-effort: any failure simply leaves the key out of the file, and the transport falls back to its table of shipped API↔MCP pairs.
Security model
| Threat | Mitigation |
|---|---|
| Key in AI transcript | localhost callback / in-process mint → 0600 store; login stdout shows key_prefix only |
| Key in URL | URL carries code only; key over TLS on handoff/token |
| Captured code replay | PKCE verifier never leaves CLI; code is TTL-bounded |
| CSRF on callback | state = PKCE challenge; redeem requires verifier |
| Wrong Console page | CLI button only on /xmemcli/<challenge> |
| Session token outliving the login | the email-path session cookie is held in process memory only — never printed, never on disk — and is discarded when the process exits; the token itself stays server-valid until its ~2-day expiry, so the containment is the CLI never letting it out of memory, not revocation |
| Session cookie over cleartext | the email path refuses non-https Console origins (loopback excepted), refuses every redirect on its cookie-carrying requests, and fails fast when a tunnelled Console marks its cookie Secure for an http:// origin |
| Unsolicited sign-in email initiation | server-side send limits per mailbox (enforced per server node, so the fleet-wide budget scales with node count); the email itself grants nothing without the inbox owner's Approve, and the CLI best-effort-cancels attempts it cannot complete |
| Approving someone else's email sign-in | user-mediated — the confirmation page leads with "only if this is your address" and states that approving lets the requesting device act on the account, including creating API keys; the stake is higher here than in the browser flow, because approving a CLI-started attempt ends in a durable API key, not a two-day browser session. The matching-code comparison that once narrowed this further is retired for now (the code still travels on the wire but no surface displays it). Approve only sign-ins you started. |
Each login binds its own port and names it in the login URL, so concurrent logins never contend for one listener. Security rests on PKCE and on the callback being reachable only from the CLI's own machine, not on port secrecy. For SSH or devcontainer use the port has to exist before the tunnel does, so pin it: --callback-port <n> or $XMEM_CALLBACK_PORT, then forward that port.
Quota (observe vs escalate)
xmemcli quota observes usage freely. Raising limits requires human approval in the Console — the CLI must not self-grant quota increases.
xmemcli — capabilities reference
Version: 1.3.0 (see xmemcli version)
xmemcli is the command-line client for the xmemory HTTP API. It is human-approved for key acquisition: an AI-assisted IDE may run reads, writes, and inventory, and may initiate auth login, but a key is only issued after one explicit human approval — a click in the Console UI (PKCE handoff) or an approval of a sign-in email (--email). Quota limits change only in the Console UI. The full acquisition contract is in the auth-flow reference bundled with this package.
xmemcli … # installed CLI
Commands
Every command below has dedicated help — see Help.
Data
| Command | Description |
|---|---|
write <text> |
Sync write (POST …/write). --extraction fast|deep (default fast). --no-wait queues async write, returns write_id. --scope TYPE:ID (repeatable) anchors the write to existing objects: only they may be modified/deleted, new objects and links to them are allowed, anything else fails; fast extraction only. |
write-status <write_id> … |
Poll async writes until terminal or --timeout (default 60s). Accepts multiple ids. |
read <query> |
Read from active instance. --read-mode single|raw|xresponse, --scope TYPE:ID (repeatable), --scope-relations, --related-types (also return related_types: per object type the read touched, the fields it did not return and the object types a declared relation links it to; needs instance.get_own on the key), --related-types-depth N (1, 2 or 3, refused as a usage error otherwise; follow the relations N levels out, each catalog entry saying its distance; implies --related-types). |
Requires --instance-id or $XMEM_INSTANCE_ID for read/write (or auto-pick when exactly one instance is visible — see Global flags).
Org inventory
Permission-filtered view of what this API key can see.
| Command | Description |
|---|---|
org list clusters |
Clusters visible to this credential |
org list instances |
Instances visible to this credential. A key minted with data.read but not instance.get_own (the supported shape for trace …) gets the ids only, with a warning saying so — the same set trace ls is scoped to. |
org list keys |
API keys visible to this credential (metadata only; plaintext never returned) |
Instances
| Command | Description |
|---|---|
instance get <instance_id> |
Fetch one instance |
instance templates |
List the ready-made templates a create can name — id, label, one-line description, who each is for, example writes and reads, and the object-type names. No schema crosses the wire; the server holds the shape. |
instance create |
Create from a template or an XMD/JSON schema. Required --name; then exactly one of --template <id>, --schema-file or --schema-stdin. --template sends only the id: no schema is composed, generated or repaired, so the shape you get is exactly the one you picked. |
instance instructions [instance_id] [text] [--clear] |
Read, set or clear the instance's standing instructions to an agent — the owner's own words, carried to every agent that connects it. No text and no --clear reads and changes nothing. Setting reads the current value first and writes with the epoch it read, so an edit composed against instructions someone has since replaced is refused rather than applied (the console form and the instance chat write the same field). Clearing is --clear; an empty string is refused, because that is what a shell substitutes for an unset variable. The id may be pinned with --instance-id or $XMEM_INSTANCE_ID and the text given alone — a lone positional that is not an id is read as the text. |
instance setup [instance_id] [--format agent|project] |
How to connect this instance to an agent, ordered for where it is likely to be used. Prints the same steps the get_setup_instructions MCP tool gives an agent, so a person and the agent beside them read the same thing. --format project additionally prints the shared setup files to commit; each teammate still approves the install and signs in once. Carries no credential. |
Schema & XMD
XMD (xmemory Data) is the typed schema format for instances. These commands mirror the Console schema workflow (read/update/dry-run/migrations, LLM generate/enhance, and validation).
Command map
| User task | CLI command | Notes |
|---|---|---|
| Read live schema | schema get [instance_id] |
Live schema for one instance. -o writes YAML. |
| Apply migration | schema update [instance_id] |
--schema-file, optional --migration-plan, --confirm-destructive. |
| Preview migration | schema dry-run [instance_id] |
Same flags as schema update; returns planned DDL without applying. |
| List migrations | schema migrations list [instance_id] |
Newest first. --limit, --before-id, --include-yaml. |
| Fetch one migration | schema migrations get [instance_id] <migration_id> |
Optional positional instance id; else --instance-id / $XMEM_INSTANCE_ID. --include-yaml. |
| Review suggestions | schema suggestions review [instance_id] |
Suggestion engine: consolidated proposal from read-traffic gaps. |
| Decide suggestions | schema suggestions decide [instance_id] |
--proposal-version (required) + --accept/--reject/--defer/--file. |
| Apply suggestions | schema suggestions apply [instance_id] |
--proposal-version plus mandatory --confirm-destructive; preview removals in Console first. |
| Enhance bound schema | xmd enhance --from-instance <description> |
Uses live schema; requires instance id. |
| Synthesize schema | xmd generate <description> |
Cluster-scoped LLM synthesis. -o writes YAML. |
| Enhance from file | xmd enhance [schema_file] <description> |
From a file, or --from-instance for bound enhance. -o, --plan-output. |
| Create instance | instance create |
--name, --schema-file or --schema-stdin. |
| Validate schema | xmd validate / validate-yaml / validate-json |
Cluster-scoped server validation. |
| Convert JSON Schema | xmd convert |
JSON Schema → XMD YAML via the API (validate_json_schema). |
Not exposed in CLI (Console-only today): the suggestions before/after YAML preview.
Typical workflow
# 0. Or skip 1-2 entirely: pick a ready-made shape
xmemcli instance templates
xmemcli instance create --name demo --template personal_crm
# 1. Synthesize or enhance
xmemcli xmd generate "people and their pets" -o schema.yml
xmemcli xmd enhance schema.yml "add a Pet.breed field" -o next.yml --plan-output plan.json
# 2. Validate before create or update
xmemcli xmd validate next.yml
# 3. Create instance OR migrate existing
xmemcli instance create --name demo --schema-file schema.yml
xmemcli schema dry-run INST_ID --schema-file next.yml --migration-plan plan.json
xmemcli schema update INST_ID --schema-file next.yml --migration-plan plan.json --confirm-destructive
# 4. Inspect history
xmemcli schema migrations list INST_ID
xmemcli schema migrations get INST_ID MIGRATION_UUID
# 5. Suggestion engine (from real read traffic) — confirm with the user before decide/apply
xmemcli schema suggestions review INST_ID
xmemcli schema suggestions decide INST_ID --proposal-version PV --accept FP1 --accept FP2
xmemcli schema suggestions apply INST_ID --proposal-version NEXT_PV --confirm-destructive
Commands
| Command | Description |
|---|---|
schema get [instance_id] |
Live XMD schema. Optional -o file. |
schema update [instance_id] |
Replace schema from --schema-file. Optional --schema-type, --migration-plan, --confirm-destructive. |
schema dry-run [instance_id] |
Preview migration (same schema/plan flags as update). |
schema migrations list [instance_id] |
Applied migrations, newest first. --limit (default 50), --before-id, --include-yaml. |
schema migrations get [instance_id] <migration_id> |
One record. Optional positional instance id; else global instance flags. --include-yaml. |
schema suggestions review [instance_id] |
Consolidated proposal from read-traffic gaps. Returns proposal_version + items. |
schema suggestions decide [instance_id] |
Record accept/reject/defer. Requires --proposal-version plus fingerprints or --file. |
schema suggestions apply [instance_id] |
Apply accepted items as one migration. Requires --proposal-version and --confirm-destructive. |
xmd generate <description> |
LLM-synthesize XMD. Optional -o. Requires cluster resolution. |
xmd enhance [schema_file] <description> |
LLM-augment schema from file, or --from-instance for live schema. -o, --plan-output. |
xmd validate <schema_file> |
Validate; YAML vs JSON inferred from .json extension. |
xmd validate-yaml <schema_file> |
Force YAML validation endpoint. |
xmd validate-json <schema_file> |
Force JSON Schema validation endpoint. |
xmd convert <json_schema_file> |
JSON Schema → XMD YAML via the API (validate_json_schema). Optional -o, --root-object. |
Instance id for schema * commands: positional [instance_id], else --instance-id / $XMEM_INSTANCE_ID.
Binding (local only — no network, no credential)
Which instances an agent working in this directory should know about, and how eagerly to
engage each one. Stored in .xmemory.json, discovered by walking up from --binding-dir /
$XMEM_BINDING_DIR (or the cwd), ceilinged at $HOME — the same rule .xmemrc.json has always
used, and no git anywhere. From a start outside $HOME the walk checks that directory and
stops, rather than climbing into a shared parent like /work or /tmp where another user's file
could be planted. A session-start hook passes the project root explicitly, so it never depends on
the walk at all.
~/.xmemory.json is consulted as the outermost scope whenever $HOME is the user's own rather than part of the tree being read. Where the session root is the home directory — a dotfiles repository, a devcontainer or an archive unpacked as $HOME — that file is treated as project scope like any other file inside the tree.
Unlike the rc — where the nearest file simply wins — every scope is merged, git-config style:
~/.xmemory.json supplies personal instances, a repository's committed .xmemory.json adds the
team's, and the nearer file wins field by field on any instance both mention.
| Command | Description |
|---|---|
binding list |
Merged view for this directory. --autoload lists only what a session-start hook would pull. |
binding add <id> |
Bind an instance, or update the binding it already has. --name, --purpose, --tier, --engage (repeatable), --scope. |
binding remove <id> |
Unbind from one scope. To silence an instance inherited from a wider scope, bind it here with --tier off instead. |
context |
Session-start pack for the autoload-tiered bindings here. --text prints the pack alone, --max-tokens, --no-live-state, --timeout. The only one of these that needs a credential. |
Tiers: autoload (pull its context every session) · available (default; engage on demand) ·
off (bound but dormant).
Scopes: --scope project (default) writes the committable file — at an existing nearer
binding, else the git work-tree root when git can say and the reader would actually look
there, else the cwd. git only ever suggests: no git, or a directory that is not a checkout, is
an ordinary state and never blocks a write. The one refusal left is when the target would resolve
onto the home directory, which would write the user-scope file while calling it a project one.
--scope user always writes ~/.xmemory.json.
context makes no request when nothing is tiered autoload, and resolves no credential
either — a hook running on every session start must not error in a project that was never
going to fetch anything. A server without the endpoint is treated as nothing to inject, so a
client ahead of its deployment degrades quietly instead of failing every session, as does a
binding this build cannot parse.
--timeout bounds the whole command. Not the request — everything: binding discovery,
the credential preflight, the git provenance lookup, name resolution, TLS, response headers and
body. It is a wall-clock limit, because it is the only bound a session-start hook has (POSIX
sh has no portable timeout) and a bound with an exception in it is not one. Expiry degrades
like every other failure: exit 0, empty pack, one warning: line naming the budget.
--text prints the pack and nothing else, for a caller that will wrap it. Do not point a
hook at it directly: a hook's stdout is parsed as its control document on some events, so a
pack that happened to be valid JSON would be read as control fields rather than injected. A
companion editor-integration plugin wraps this output in the documented hook envelope its own
client expects; it ships from a separate repository and is not part of this one.
No secrets. An instance id is not a credential, so a project .xmemory.json is written
0644 and is meant to be committed — the opposite of .xmemrc.json (0600, never
committed). ~/.xmemory.json is written 0600: it stays on one machine, and on a shared host
its instance names and purposes are nobody else's business.
Scope precedence has one exception: your own entries are a ceiling. A nearer scope normally
wins field by field, but a project file arrives by git clone and is the less trusted of the
two. So for any instance ~/.xmemory.json lists, a committed file may make it less eager,
never more:
- Tier. Ranked
autoload>available>off. A project may take yourautoloaddown toavailableoroff; it can never raiseofftoavailable, oravailabletoautoload. An omittedtierin your file meansavailableand binds as one — sobinding add --scope user <id>with no--tiercaps that instance atavailableeverywhere. If you want a project'sautoloadto stand, say--tier autoloadin your own file: your entry states the most eager you are willing to be, not merely that you know the id. engage. Replaced by a nearer scope for every other instance, but not for one of yours — otherwise a committed file could rewrite the cues attached to your own instance, which is a quieter version of raising its tier.
Everything else (name, purpose) still merges nearest-wins, and a project may always add
detail to an instance you listed.
File format (v1). id is required; everything else is optional. Unknown keys are rejected,
so a typo fails loudly rather than being ignored.
{
"version": 1,
"instances": [
{
"id": "9c421cd4-77ab-4a51-a7e3-b8fd5d09d394",
"name": "Team Knowledge",
"purpose": "shared dev conventions",
"tier": "autoload",
"engage": ["a convention is learned or corrected"]
}
]
}
Trace — the Console's operation traces
What the Console shows on an instance's Operations tab (a paged list), on Inspect (a
search across operations), and in each operation's detail pane — fetched from the same Console
endpoints, with this CLI's API key, and emitted as the Console serves them. Nothing is curated
or reshaped: the Console already curates per caller, and a field the Console adds shows up here
the same day. These documents are for scripts and agents; on a terminal they are only indented
and coloured (NO_COLOR / FORCE_COLOR honoured). Flags only — there is no query language to
learn; the CLI composes the Console's search clauses itself.
| Command | Console view | Description |
|---|---|---|
trace ls |
Operations tab / Home | Operations, newest first, across every instance this credential can see (nothing given — the Home view), or the --instance ID given (repeatable, or a comma list; default --instance-id / $XMEM_INSTANCE_ID). Returns operations[], counts, instance_counts, series, and the paging cursor next_before_ts_ms + next_before_trace_id (with has_more_operations). Flags: --trace-type T (repeatable / comma; write, read, create, update, evolution), --hours-back N (server default 24, 0 = unbounded) or --start-ts / --end-ts (epoch seconds), --user-id, --api-key-id, --include-console-reads, --bucket-ms, --limit N (default 100, Console cap 500), --cursor TS_MS:TRACE_ID (the previous page's two halves, joined with a colon), --counts-only (ops_limit=0: what Home fetches — counters and series, no rows; refused together with --limit / --cursor, which would have nothing to page). |
trace ls --search |
Inspect | The Inspect search, for the filters only it can answer: --text (free text), --request / --response (text on either side of the operation), --object NAME / --field NAME (repeatable; all must match), --status S (repeatable / comma; any may match: ok, error, stale, timed_out, internal_error, quota_exceeded, in_progress, schema-gap), --trace-id ID (repeatable / comma), --session-id; plus --instance (at most one — the Inspect scope), --trace-type, --hours-back (a floor applied to the hits after the search — Inspect's own recency windows decide the candidate pool, so this narrows a result, unlike the query window it is on trace ls), --user-id, --api-key-id. Returns the Inspect document: merged[] hits with kind, href (/write/{trace}, /read/{trace}, /create/…, /update/…, /evolution/…), trace_id, instance_id, session_id, user_id, api_key_id / api_key_name / api_key_prefix, status, time_taken_ms, match_items. |
trace get <trace_id> |
Detail pane | One operation's full document. --trace-type (operation_type on a trace ls row; the first path segment of a --search hit's href) makes it one request; without it the five detail endpoints are probed in order write, read, create, update, evolution and the first that knows the trace answers. A miss — the Console's {"found": false}, or its 404 for a trace outside your scope — is stage: "not_found", exit 3. |
--search is opt-in, on purpose. The Inspect search runs text predicates across the trace store with a candidate pool of thousands and a merge — markedly more expensive than the indexed, cursor-paged trace ls. Passing a search-only flag (--text, --request, --response, --object, --field, --status, --trace-id, --session-id) without --search is a usage error (exit 2) whose message says exactly that, so an agent reasons about whether it needs the search or whether trace ls narrowed by --instance / --trace-type / --hours-back plus trace get will do. Conversely --limit, --cursor, --counts-only, --start-ts / --end-ts, --bucket-ms are refused with --search because the Inspect endpoint cannot honour them, and --include-console-reads because Inspect's only spelling of that toggle is a bare phrase inside q itself — which is also why a free-text value containing the words "include console reads" is refused rather than sent (the Console would flip the toggle and strip the phrase from the query).
The pagination gap — read this before scripting --search. trace ls pages properly (the Console's Operations tab does). trace ls --search does not: the Console's Inspect endpoint has no cursor — it returns the newest 50 matches when no filter narrows it (latest-activity mode) and up to 5 000 matches per source once filtered, and stops; the Console's own Inspect view has the same cliff. So --search takes no --cursor / --limit, and when a response sits at one of those ceilings the CLI emits a warning (stderr, and folded into the JSON document's warnings array) saying the result was cut and how to narrow it. The warning is best-effort — the warning is one-sided by construction: it fires on the count the client can see, and the Console narrows after its per-source row caps (type/status resolution, console-read exclusion, the caller's instance allowlist), so a capped response can shrink below the ceiling — even to near-empty for a scoped key searching without an instance filter — with no client-visible signal. A missing warning is not proof of completeness — and a warning is not proof of a cut: the 5 000 figure is per source, while the count the warning tests is the cross-source merge (truncated only in latest-activity mode), so several uncapped sources together can trip it on a complete result. Until the Console endpoint pages (and reports truncation itself), treat every --search result as "the newest matches", not "all matches".
Two document shapes. trace ls returns the Operations-tab document; trace ls --search returns the Inspect document (above). They differ because the Console's two backends differ and this CLI does not reshape what the Console serves. Both carry the same identifiers per row (trace_id, instance_id, session_id, user_id, api_key_id, status), which is what trace get needs next.
Every operation row carries its internal ids. operations[] rows have operation_type, trace_id, instance_id, timestamp, status, is_success, error_code, session_id, user_id, user_email, api_key_id, api_key_name, api_key_prefix, time_taken_ms, text_snippet, object_names, field_names.
Three of those never resolve for an API key, super admin or not, and this is by design rather than a gap.
user_emailis always absent: the accounts/userslookup is closed to keys (an agent may learn who it is, never enumerate the organisation's users), so rows keepuser_idand you resolve names yourself if you need them — and aUser:<email>search clause matches nothing; filter byuser_id.api_key_name/api_key_prefixresolve only for the caller's own keys, becauseGET /api-keysscopes a key caller to its owner's keys. So a super admin's key does not see literally everything its owner's browser sees: it sees the same rows and the same fields, with those three enrichments empty. Everything scope-bearing — which instances, which detail fields,llm_calls, xuid substitution — is identical. Detail documents add what the Console's pane shows:source_id,write_mode/read_mode,scope,extracted_objects,diff_plan(sanitised unless super admin),written_objects,migration_id,proposal_version,schema_version,stage,triggered_evolution {migration_id, evolution_trace_id, update_trace_id}, and — unrestricted callers only (a super admin, or a browser session on aKINGDOM_LOCALConsole) —llm_calls[](llm_call_id,gateway_request_id,model_type,provider, …),full_error_message, raw token counts, unsanitised xuids,sql_queries,ddl_queries, andschema_generation.
A scoped key gets [] for sql_queries / ddl_queries / schema_generation, not the rows. Those three come from read_sql_queries, instance_create_ddl_queries and schema_generate_*, which carry no instance ownership column — schema_generate_*'s trace id is client-supplied — so they cannot be scoped in SQL and are served only to callers with no instance restriction at all. This is a deliberate fail-closed choice, not a bug to work around: an empty array means "not available to this credential", never "this operation ran no SQL". llm_calls[] behaves the same way and always has. Xuids are hidden for every non-super-admin, including on writes that carry no identity map — the substitution is done by the Console, server-side, so a caller with no browser sees #hidden rather than a raw id.
Who sees what — the Console decides; auth status tells you which view to expect.
- A regular key sees the operations of the instances the accounts API lists for it — the same set
org list instancesprints. That is at most what its owner sees in the Console, never more. - Access requires the key's role to carry
data.read— the same permission the data-read endpoint requires. A role deliberately scoped away from data (auth statusshows the codes underpermissions) is refused with 403 → exit 3, stagepermission, whoever owns the key — a permission denial, not a bad credential: rotating or re-issuing the key cannot fix a role that was scoped this way on purpose. An unknown or missing key is the separate 401 → exit 4 (auth). - A key whose owner is a super admin sees the super-admin Console: every organisation's instances, no instance filter, and the super-admin-only detail fields above. The accounts
is_super_adminflag decides — the key sees the same rows and the same scope-bearing fields its owner's browser session sees, minus the three enrichments no key resolves (user_email, andapi_key_name/api_key_prefixfor other people's keys; see the note above).auth statusreports it assuper_admin. - No new permission is involved: the Console answers a key exactly as it would answer its owner's browser session, narrowed to the key's instances. It never mints a session for a key. Filters that need to look other users up (
User:-style email filters) are not offered: an API key can identify only itself, never list or look up users — filter by--user-id.
The Console origin. trace … talk to the Console, a second origin. It is --console-url → $XMEM_CONSOLE_URL → inferred: https://console.xmemory.ai for the default API, :8002 for a local :8000. A key bound to any other API origin gets a usage error (exit 2) rather than a guess — the credential-binding rule holds across origins.
Parity. These commands are the Console's data path, not a copy of it. If the Console gains a filter or a field, it is added to the same list here and is available; the MCP server exposing the same data is deferred and will follow the same rule (see the note in the MCP server source).
Auth & meta
| Command | Description |
|---|---|
auth login |
PKCE via Console /xmemcli/<challenge> → .xmemrc.json. --no-browser, --timeout, --console-url, --callback-port (pins the loopback callback port for ssh -L; 0 or omitted means any free port, otherwise 1024–65535; also $XMEM_CALLBACK_PORT, which is read globally). With --email ADDR (+ optional --org-name, used only when a new account is created; requires --email): headless email sign-in — one emailed approval, then the CLI mints its own org-wide key named CLI email login (<hostname>, <timestamp>); details in the auth-flow reference. New-organisation creation additionally requires the deployment to allow public registration. Payload shapes: a fresh email mint carries method: email, key_name, api_role, and org_created; the browser path carries method: pkce (no role/org fields); a matching --api-key or rc credential short-circuits to status: already_authorized with source: flag / rc (persisted: false on the flag arm, which saves nothing); a matching $XMEM_API_KEY is copied into the rc and reports status: authorized, source: environment. Exit codes follow the standard HTTP contract (401 → 4 with stage auth, 403 → 3 with stage permission, other HTTP/network/server-state failures → 3). |
auth logout |
Remove active .xmemrc.json |
auth status |
Call /whoami. No key → exit 0; bad key → auth error; good key → email + prefix + super_admin (whether the key's owner is a super admin — what decides which Console view trace … return) + permissions (the key's own capability codes; trace … requires data.read among them). |
mcp <instance-id> |
Serve one instance to an MCP client over stdio, using this CLI's credential. Not for humans — see below. |
version |
CLI release version (no network) |
status |
Local health: version + whether .xmemrc.json exists, and the mcp_url it declares when it declares one (no network) |
mcp is a transport, not a command you run. An MCP client starts it and speaks
JSON-RPC to it over stdio; it forwards each frame to <mcp-url>/instance/<id> with this
CLI's credential attached, and writes the answer back. The Console renders the
registration as claude mcp add <name> -- xmemcli mcp <instance-id> (with
--mcp-url <origin> appended when the MCP origin is not the client's own default), or the
equivalent for another client — replace the bare xmemcli with its absolute path, in
double quotes. The client stores the command as written and resolves it against its own
PATH every time it starts the server, and that PATH need not carry the directory the
tool was installed into; registered bare, it fails there — Claude Code reports
ENOENT: Executable not found in $PATH: "xmemcli". command -v xmemcli prints the path on
macOS and Linux, (Get-Command xmemcli).Source in PowerShell on Windows. If the bare name
is registered already, remove that server entry before adding it again: Claude Code refuses
mcp add for a name that already exists rather than replacing it.
It exists because an MCP client can reference an environment variable in an
Authorization header but cannot read .xmemrc.json. Reading the credential per
connection means no credential is captured when the server is registered, so a
configuration written today still works in a session opened next week with an empty
environment.
Where a file-held key may be sent. The MCP origin is configured separately from the API
origin, and a project .mcp.json arrives by git clone — it controls that server's args and
env, so it is not allowed to vouch for itself. A key read from .xmemrc.json therefore reaches
only the MCP origin that same file records in mcp_url, the origin paired with its api_url in
this client's built-in table of shipped deployments, or a loopback host when both ends are on this
machine. Anywhere else needs a key named on the call with --api-key.
A recorded origin says where, never how: a remote one must be https, so a hand-written
http:// entry is refused rather than sending the key in clear text. Loopback stays exempt — those
bytes never leave the machine.
The mount path is part of the destination, not decoration. A deployment serving every route
group on one hostname puts MCP at https://host/mcp, the REST API at https://host/xmem and the
OAuth server at https://host/oauth2; an rc recording the first authorises the first only.
auth login fills in mcp_url from what the API reports about itself, which is what makes an
on-premise deployment work: its MCP host is in no table this client was built with, and every
attempt to derive one host from the other has been wrong in a new way. If the transport refuses
an origin you expect it to accept, run auth login against that deployment again — the rc predates
the field, or that server does not report one. That works even when the stored key is still good:
the --email path records the origin it learns while checking, and reports it as mcp_url on the
already_authorized payload. A deployment that has stopped serving MCP answers null, and the
recorded origin is dropped rather than left authorised — and if that removal cannot be written,
the command fails rather than reporting success over a host that is still authorised.
Two consequences worth knowing:
- stdout is the protocol. Alone among these commands it emits no JSON document and
honours neither
--jsonnor the exit-code table above; its exit code is a process status. Diagnostics go to stderr. - Signed out is reported, not fatal. Every request is answered with a JSON-RPC error
naming
auth login, because a client that is told only "the server failed to start" gives the reader nothing to act on. Exit code and stage do not apply.
Console /xmemcli routing:
- Onboarding → silent onboarding-key handoff to the CLI (no overlay, no button click).
- Post-onboarding → API Keys create modal → “Use this key in the CLI”.
Quota (read-only)
| Command | Description |
|---|---|
quota |
Usage / limit / window start for the key's cluster (derived from instance → cluster). |
- Default: exit
0when quota is readable; stderr warning when usage is high. --fail-if-under THRESHOLD— exit 7 when remaining quota is below that share of the limit (0.1or10%). Distinct from exit 6 (API quota exhausted). Seexmemcli help quota.
Quota increases are Console-only — see What xmemcli does not do.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Usage error (bad flags, unknown help topic, missing required args) |
| 3 | HTTP / request failure — see HTTP status mapping (includes permission denied, other API errors, and unwritable -o paths) |
| 4 | Auth error — invalid or missing API key (401; stage: "auth") |
| 5 | Timeout |
| 6 | Quota exhausted (API quota-exceeded response; stage: "quota") |
| 7 | Quota headroom below --fail-if-under |
| 8 | Write still processing — the API's wait window expired while the write kept running (HTTP 200 envelope; stage: "processing"). Not a failure: poll write-status, never retry |
| 130 | Interrupted (Ctrl+C) |
HTTP status → exit code and JSON stage
xmemcli mirrors the API's 401 vs 403 distinction on the process exit code and on the machine-readable stage field. This is the single contract for every command that performs an HTTP call (read/write, org list *, schema/XMD, quota, auth status, …) — with one deliberate exception, below.
context is exempt, by design — from the release that introduces context, which is the
first one where this carve-out means anything. It is the one command nobody invokes: a
session-start hook runs it unattended, and a non-zero exit there costs the user their session
rather than their context. So every HTTP outcome — 401 and 403 included — exits 0 with an
empty context and an explanatory warnings[] entry. Use auth status to tell a credential
problem apart from an empty binding; context will not.
The exemption goes further than HTTP status. Any runtime failure inside context — a malformed
binding, an unreadable response shape, a timeout, a bug nobody anticipated — exits 0 with an
empty pack and a warning: line naming the cause. This is enforced in code rather than by
enumerating failure modes, so an unforeseen one degrades like the rest.
And it is exempt from the one-JSON-document rule on stdout. Every other command emits a JSON
error document in machine mode, including on usage errors. A piped, non---json context whose
arguments fail to parse writes zero bytes to stdout and puts its reason on stderr instead,
because a hook discards stdout at exit 2 — so a JSON error document there is a session start
failing with no stated cause anywhere the user can see it. Usage and parse errors are the only
paths that still exit 2; they are a mistake in the hook's own configuration, not a runtime
condition, and the hook author is the one reading stderr.
Quick reference
| API HTTP status | Shell exit code | JSON stage |
Meaning |
|---|---|---|---|
| 401 | 4 | auth |
Bad credential — missing, invalid, expired, or inactive API key |
| 403 | 3 | permission |
Bad authorization — key is valid but this key/role cannot perform the operation |
| Other 4xx/5xx | 3 | request |
Request failed for another reason (404, 422, 5xx, …) |
| 402 (quota exhausted) | 6 | quota |
Quota window exhausted — human must extend quota in Console |
any status with errors[0].code == WRITE_STILL_PROCESSING |
8 | processing |
Write is still running — the write did not fail. Matched on the code, whatever the status: a keepalive-flattened 200 and an unflattened 500 both exit 8, so this row overrides the 4xx/5xx row above. Poll write-status <write_id>; a retry starts a second write |
How to interpret failures
- Exit 4 /
stage: "auth"/ HTTP 401 → fix the key: login again, rotate the key, check$XMEM_API_KEY/.xmemrc.json, confirm--api-urlpoints at the environment that issued the key. - Exit 3 /
stage: "permission"/ HTTP 403 → the key works, but the operation needs a broader API role (for exampledata.write,instance.generate_schema,cluster.get_own). Do not treat this as a wrong password. - Exit 3 /
stage: "request"→ other HTTP failure (instance not found, validation error, server error). Read the error message and HTTP status in the JSON body.
Some commands attach a more specific stage (validate, usage, limit, not_found for trace get, …) while still using exit 3 unless the row above applies. write adds processing, which is in the table above and exits 8 rather than 3. The schema suggestions commands add their own stages: stale_proposal (re-run review for a fresh proposal_version), invalid_decision, dependency (accept the dangling dependencies first), not_initialised (submit a write first), and apply; review reports an evolution_in_progress condition as a successful status with exit 0.
Envelope errors (HTTP 200 with non-empty errors[]) are not HTTP 401/403; they typically exit 3 with stage: "request". Two exceptions. QUOTA_EXCEEDED exits 6 with stage: "quota", the same as it does on a real HTTP 402 — the keepalive flattening does not change the exit code — and this applies to every command that classifies the error, not just write and read. WRITE_STILL_PROCESSING exits 8 with stage: "processing" and is not a failure at all; unlike the quota rung this one is write-only, because its guidance is to poll write-status, which is meaningless for any other verb — elsewhere it falls through to 3/request.
Known bug. Some subcommands in the
instance,schemaandxmdgroups —instance get,schema getandxmd generateamong them — unwrap the response outside their error handling, so an envelope error never reaches the classifier and escapes as an uncaught exception: a traceback and shell exit 1, which is not in the table above. Those report 1, not the 6 this section describes, until that is fixed. It is not whole groups:instance instructions, for example, reaches the classifier and exits 6 as documented.
Breaking change in 0.0.5: HTTP 403 previously exited 4 (auth); it now exits 3 (permission). Scripts that treated exit 4 as “any auth-layer failure” must check stage or handle exit 3 for permission denials.
New in 1.3.0: exit 8 with stage: "processing" for a write whose server-side wait window expired while the write kept running. In --json mode that document also carries a write_id field (omitted when the server sent no handle), so a script polls with it directly instead of parsing the message. Additive — no previously documented mapping changed — but a script that reads “non-zero, and not a code I recognise” as failed-and-retryable will now retry a write that has not failed, starting a second one the account pays for. Branch on 8 (or on stage) and poll xmemcli write-status <write_id> until it settles.
New in 1.2.0: instance templates lists the ready-made instance shapes the service ships, with the prose to choose between them, and instance create --template <id> builds one from a pinned schema the server already holds. No schema is generated on that path: no description to compose, no xmd generate hop, and the instance comes out identical to the one the console would have made from the same template. (Creating an instance still generates its agent brief afterwards, on this path as on any other.)
New in 1.1.0: auth login records the deployment's own MCP origin in .xmemrc.json as mcp_url (omitted where the server reports none), and mcp both connects there by default and accepts it as the pair its key may reach. Requires 0.0.12 or newer to read such a file — see the note below.
Compatibility change in 0.0.12: an .xmemrc.json carrying a key this release does not recognise is now accepted, and the unknown key ignored; email, api_url, and api_key are still required and still have to be strings. Until 0.0.12 the key set was closed, which made adding a field to the file a breaking change for every command rather than for the one that would read it — a CLI that cannot load the rc cannot resolve a credential for anything. Nothing writes an extra key yet; this release exists so that a later one can.
Compatibility change in 0.0.13 (single-origin deployments): an API base URL's path is now preserved, never stripped — --api-url https://host/xmem builds requests under /xmem instead of reducing to the bare origin. Where the base's path is exactly /xmem, the CLI infers its single-origin siblings on the same host: auth login finds the Console at /console without --console-url, and xmemcli mcp reaches /mcp without --mcp-url (explicit --console-url / --mcp-url and $XMEM_CONSOLE_URL / $XMEM_MCP_URL still win when inference does not apply; on a call that names --api-url …/xmem, sibling inference beats $XMEM_CONSOLE_URL; the MCP origin guard accepts the /xmem ↔ /mcp pair as one deployment). Bare origins and every other path shape behave exactly as before. See cli/.
Two consequences worth knowing. Connect instructions from the API assume 1.0.0 or newer everywhere, and 1.1.1 on a single-origin deployment — earlier releases let $XMEM_CONSOLE_URL steer sign-in and token redemption to a foreign Console. On a single-origin deployment they render auth login --api-url https://host/xmem with no --console-url. The inference is keyed on the conventional mounts (/xmem, /console, /mcp). A deployment that mounts its services under other prefixes is not inferable: name --console-url and --mcp-url explicitly there, which is what its own rendered commands do.
Console instance URLs (1.0.0+): xmemcli auth login https://host/console/instance/<uuid> infers the API base (/xmem) and Console (/console) from that page URL on a single-origin host — no extra flags or env vars. The Connect tab paste prompt uses this form. When no .xmemrc.json exists yet, the CLI stages the inferred api_url into a placeholder rc as soon as login starts; an existing credential file is never rewritten mid-login. Remote plain HTTP is refused (HTTPS or loopback only) — the same rule as the plain URLs below.
Plain single-origin login URLs (1.1.1+): xmemcli auth login https://host/console or …/xmem (trailing slash optional) infer the same sibling pair with no instance id — useful when signing in before you have an instance page open. Remote plain HTTP is refused (HTTPS or loopback only). An existing .xmemrc.json credential is never rebound mid-login; only a missing rc gets a placeholder api_url.
Compatibility change in 1.0.0: console instance page URLs for login (above), immediate api_url staging in .xmemrc.json, and 1.0.0 as the minimum version named by connect setup instructions. Revised in 1.1.1: staging writes only a placeholder when no rc exists (an existing credential file is left alone until login succeeds); single-origin connect instructions name 1.1.1 as their minimum — earlier releases let $XMEM_CONSOLE_URL steer the sign-in Console; and HTTPS-or-loopback now applies to every single-origin login URL, including instance pages, not only the new /console and /xmem forms. A mistaken --api-url http://host/xmem is refused the same way.
See also ACCOUNTS_AUTH.md for the server-side 401/403/404 rules.
Help
xmemcli supports root -h / --help (same overview as bare xmemcli) and the help subcommand for nested topics.
| Invocation | What you get |
|---|---|
xmemcli |
Full command overview (grouped list of every capability) |
xmemcli help |
Same as bare xmemcli |
xmemcli help options |
Global flags (--api-url, --api-key, --json, …) |
xmemcli help <topic> |
Usage, options, and subcommands for that topic |
xmemcli <cmd> … -h / --help |
Same as help <topic> for that command path |
Unknown help topics exit 2 (same as other usage errors).
Every command in this document has help. Examples:
xmemcli help write
xmemcli help write-status
xmemcli help read
xmemcli help org
xmemcli help org list instances
xmemcli help instance create
xmemcli help schema
xmemcli help schema get
xmemcli help schema update
xmemcli help schema dry-run
xmemcli help schema migrations
xmemcli help schema migrations list
xmemcli help schema migrations get
xmemcli help schema suggestions
xmemcli help schema suggestions review
xmemcli help schema suggestions decide
xmemcli help schema suggestions apply
xmemcli help xmd
xmemcli help xmd generate
xmemcli help xmd enhance
xmemcli help xmd validate
xmemcli help xmd validate-yaml
xmemcli help xmd validate-json
xmemcli help xmd convert
xmemcli help trace
xmemcli help trace ls
xmemcli help trace get
xmemcli help auth login
xmemcli help quota
xmemcli help options
Nested topics work at any level that exists in the parser (help org, help org list, help org list keys, …). Unknown topics print a short error and suggest xmemcli or help options.
What xmemcli does not do
| Area | Use the Console instead |
|---|---|
| Quota increases | No quota request-increase. Observe with xmemcli quota; raise limits in Billing / Quotas UX. |
| Routine API key creation | Post-onboarding: API Keys UI (CLI opens that flow via /xmemcli). |
| Billing & plans | Console only |
| Users, roles, org admin | Console only |
| Instance chat | Console only (trace inspection is xmemcli trace …, see Trace) |
| Cluster create/delete | CLI lists clusters; provisioning in Console |
read --extraction deep |
Server has no read-depth knob today |
| Raw REST proxy | Curated response fields only |
| Instance deletion | Human operators only — a hidden instance delete exists for interactive TTY use (type yes to confirm). It is omitted from help and must not be used by agents or automation. Console UI remains the primary human path. |
On HTTP 402 (quota exhausted), stop and ask a human to extend quota in the Console.
Global flags & environment
These apply before any subcommand (xmemcli help options).
| Flag | Env var | Purpose |
|---|---|---|
--api-url |
XMEM_API_URL |
API origin (default https://api.xmemory.ai) |
--api-key |
XMEM_API_KEY |
Bearer API key |
--instance-id |
XMEM_INSTANCE_ID |
Default instance for read/write and cluster derivation |
| --rc-dir | XMEM_RC_DIR | Where auth login writes .xmemrc.json; reads walk up toward $HOME |
| --binding-dir | XMEM_BINDING_DIR | Where binding commands look for .xmemory.json; reads walk up toward $HOME and merge every scope |
| --console-url (accepted by trace … only; $XMEM_CONSOLE_URL is read by auth status too) | XMEM_CONSOLE_URL | Console origin serving the trace views (inferred for the public and local API pairs; required for any other API origin). A named Console that provably does not pair with the API origin is refused; the refusal names the setting that supplied it, since only trace accepts the flag |
| --json | — | Force one JSON document on stdout |
| --verbose | — | Add trace_id / console_url when present (never SQL, diff plans, or internals) |
Credential resolution: --api-key → $XMEM_API_KEY → nearest .xmemrc.json → Not logged in — run xmemcli auth login first.
.xmemrc.json shape: {email, api_url, api_key}, all three required and all three strings, plus an optional mcp_url recorded by auth login. Keys beyond those are ignored rather than rejected (see the 0.0.12 note above), so a file written by a newer CLI still loads here.
context is the one exception, and it is a security boundary. Every other command walks up
from the current directory for the nearest .xmemrc.json, which is deliberate: a person chose
that directory and ran the command in it. context has neither — a session-start hook runs it
unattended, in whatever tree the editor happened to open — so it resolves the credential from
$HOME only, and consults a project .xmemrc.json only when the caller named that source
explicitly with --api-key / $XMEM_API_KEY or --rc-dir / $XMEM_RC_DIR. Without the pin, a
cloned repository could ship a key and an origin and have both used automatically. This is a
rule rather than a detector: earlier versions tried to detect a checkout-supplied credential
and each delivery mechanism turned out to be a fresh bypass.
Origin resolution: --api-url → $XMEM_API_URL → rc api_url → default — with one escape hatch: when the key comes from --api-key / $XMEM_API_KEY (not from the rc), the rc's api_url is not consulted and the origin falls back to the built-in default (https://api.xmemory.ai) unless you also set --api-url / $XMEM_API_URL. That keeps an explicitly supplied key from being silently paired with an unrelated origin in a project .xmemrc.json. If $XMEM_API_KEY is set without $XMEM_API_URL, the CLI warns once per invocation.
MCP origin resolution (mcp only): --mcp-url → $XMEM_MCP_URL → the rc's mcp_url → default (https://mcp.xmemory.ai). The rc rung follows the same escape hatch as the API origin: a key named with --api-key / $XMEM_API_KEY never inherits an origin from a project file.
Request identification (new in 1.3.0): every outbound request carries X-Xmemory-Client: xmemcli/<version> (python <version>; <system>-<machine>), e.g. xmemcli/1.3.0 (python 3.12.4; Linux-x86_64). Each field is reduced to letters, digits, ., _ and -, so nothing an OS or a patched interpreter reports can forge the note's own punctuation or fail the request when it is sent; a field with nothing usable left reads unknown. No hostname is included. The server uses this header to attribute traffic to a client. The CLI sets no User-Agent of its own, then or now: the installed urllib opener supplies its default, so the User-Agent the server sees is Python-urllib/<version> and the identity comes from the header above. Direct API requests from 1.2.0 and earlier send neither, so they record client_name: python-urllib -- indistinguishable from any third-party Python caller. Reading a drop in that bucket as third-party traffic leaving is therefore wrong; it is old CLIs upgrading. Three outcomes are distinct and must not be read as one figure: a recognised header, the other bucket a present but unrecognised header lands in, and an absent property, which means neither header named a client. Before 1.3.0 the email sign-in exchange was the only path that named itself, and it sent a bare xmemcli/<version> with no platform hint; it now sends the same header and the same value as every other path. What the server records for it still differs, though, and not because of the CLI: the console-facing calls (email sign-in, token redemption, whoami) reach the accounts API through the console, which rebuilds the request on its own HTTP client and forwards only Cookie, Content-Type, Authorization and Host. X-Xmemory-Client does not survive that hop, so those land as console. The attribution described above is what direct API calls record.
Instance resolution (read/write): --instance-id → $XMEM_INSTANCE_ID → if exactly one instance is visible, auto-select with a stderr warning → else error.
Output: Pipes and non-TTY stdout → JSON. Interactive TTY → one-line summaries unless --json.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xmemcli-1.3.0.tar.gz.
File metadata
- Download URL: xmemcli-1.3.0.tar.gz
- Upload date:
- Size: 253.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7869c4d6cd7e640bc134f169f66439a776007d56fd5feebf9dc4e17b698f3bee
|
|
| MD5 |
d60a1b52e64111f7e23bddfe0f2c895d
|
|
| BLAKE2b-256 |
dc96ac15f190c6a7f4876387c5d1e87ea64bce0bd3675418e9a06c69e5a772a5
|
File details
Details for the file xmemcli-1.3.0-py3-none-any.whl.
File metadata
- Download URL: xmemcli-1.3.0-py3-none-any.whl
- Upload date:
- Size: 241.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e730e9e74266b480886739e064382a110dbcffa2dd3d922d6f3d5e32f711385
|
|
| MD5 |
ee412f9a221f976a91e5067c386cd80b
|
|
| BLAKE2b-256 |
9140e7873892148306bf6efb33f807daafa750aa994b723bc0f57153e2d154ff
|