xmemcli 1.3.1__tar.gz → 1.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xmemcli-1.3.1 → xmemcli-1.5.1}/PKG-INFO +52 -18
- {xmemcli-1.3.1 → xmemcli-1.5.1}/README.md +51 -17
- {xmemcli-1.3.1 → xmemcli-1.5.1}/pyproject.toml +1 -1
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/__init__.py +1 -1
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_email_login.py +28 -4
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_help.py +1 -1
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_instance_schema.py +27 -38
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_login.py +7 -1
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_org_cluster.py +21 -4
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_output.py +10 -6
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_pretty.py +408 -17
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_trace.py +206 -56
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_version.py +1 -1
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/cli.py +470 -60
- {xmemcli-1.3.1 → xmemcli-1.5.1}/.gitignore +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_binding.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_binding_cmd.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_context_cmd.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_envelope.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_mcp_bridge.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_rc.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_text.py +0 -0
- {xmemcli-1.3.1 → xmemcli-1.5.1}/xmemcli/_xmem_bridge.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: xmemcli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Command-line client for the xmemory HTTP API: PKCE auth, instances, read/write, schema and XMD tools.
|
|
5
5
|
Keywords: api,cli,llm,memory,xmemory
|
|
6
6
|
Requires-Python: <4,>=3.12
|
|
@@ -20,6 +20,8 @@ xmemcli auth logout
|
|
|
20
20
|
xmemcli auth status
|
|
21
21
|
xmemcli read <query>
|
|
22
22
|
xmemcli write <text>
|
|
23
|
+
xmemcli write (--mutations-file PATH | --mutations-stdin) [--delete TYPE:ID]…
|
|
24
|
+
xmemcli write --delete TYPE:ID [--delete TYPE:ID]…
|
|
23
25
|
xmemcli write-status <write_id> [<id> …]
|
|
24
26
|
xmemcli quota
|
|
25
27
|
xmemcli instance templates
|
|
@@ -29,7 +31,7 @@ xmemcli binding add <instance_id> [--tier autoload|available|off] [--scope proje
|
|
|
29
31
|
xmemcli binding remove <instance_id> [--scope project|user]
|
|
30
32
|
xmemcli context [--text] [--max-tokens N] [--no-live-state]
|
|
31
33
|
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]
|
|
32
|
-
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]
|
|
34
|
+
xmemcli trace ls --search [--text …] [--request …] [--response …] [--object NAME]… [--field NAME]… [--status S]… [--trace-id ID]… [--session-id S] [--instance ID] [--trace-type T|delete]… [--hours-back N] [--limit N] [--cursor TS_MS:TRACE_ID:INSTANCE_ID]
|
|
33
35
|
xmemcli trace get <trace_id> [--trace-type write|read|create|update|evolution]
|
|
34
36
|
```
|
|
35
37
|
|
|
@@ -48,6 +50,9 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
|
|
|
48
50
|
| `xmemcli write --extraction fast` (default) | `extraction_logic: "fast"` |
|
|
49
51
|
| `xmemcli write --extraction deep` | `extraction_logic: "deep"` |
|
|
50
52
|
| `xmemcli write --no-wait` | async write; poll with `write-status` |
|
|
53
|
+
| `xmemcli write --mutations-file m.json` / `--mutations-stdin` | `structured_mutations` — the JSON array as given; no `text`, no `extraction_logic` |
|
|
54
|
+
| `xmemcli write --scope Pet:name=Luna --scope-mode drop` | `scope: {objects: [...], mode: "drop"}` — out-of-scope changes are skipped and reported; `reject` is not sent |
|
|
55
|
+
| `xmemcli write --delete Person:name=Ada` | appends `{"object_mutation": {"object_type": "Person", "delete": {"key": {"name": "Ada"}}}}` (`Person:<xuid>` sends `{"xuid": …}` as the key) |
|
|
51
56
|
| `xmemcli read --read-mode single` (default) | `mode: "single-answer"` |
|
|
52
57
|
| `xmemcli read --read-mode raw` | `mode: "raw-tables"` |
|
|
53
58
|
| `xmemcli read --read-mode xresponse` | `mode: "xresponse"` |
|
|
@@ -58,7 +63,7 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
|
|
|
58
63
|
|
|
59
64
|
## Curated output
|
|
60
65
|
|
|
61
|
-
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.
|
|
66
|
+
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). A text write keeps a compact `extracted` summary (`Type(identifier)` tags); a structured write runs no extraction and has none. `write` and `write-status` keep the server's `changes` block (created / updated with old → new / deleted, bulk summaries, advisory records) when the write changed something. `--verbose` adds `trace_id` and `console_url` when present.
|
|
62
67
|
|
|
63
68
|
**`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`, …
|
|
64
69
|
|
|
@@ -82,7 +87,17 @@ NEXT=$(echo "$PAGE" | jq -r '"\(.next_before_ts_ms):\(.next_before_trace_id)"')
|
|
|
82
87
|
xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
|
|
83
88
|
```
|
|
84
89
|
|
|
85
|
-
`--search`
|
|
90
|
+
`--search` pages too, with the Inspect endpoint's three cursor fields — follow `has_more`, since a page can come back short:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
PAGE=$(xmemcli trace ls --search --text "Ada")
|
|
94
|
+
# `select(.has_more)` so the last page yields an empty string rather than the literal `null:null:null`,
|
|
95
|
+
# which `--cursor` refuses.
|
|
96
|
+
NEXT=$(echo "$PAGE" | jq -r 'select(.has_more) | "\(.next_before_sort_at_ms):\(.next_before_trace_id):\(.next_before_instance_id)"')
|
|
97
|
+
[ -n "$NEXT" ] && xmemcli trace ls --search --text "Ada" --cursor "$NEXT"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A search with no filter shaping it is the Console's recent-activity view, which reaches back 180 days; when older operations exist, its last page says `truncated` and the CLI warns. 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: [](#trace--the-consoles-operation-traces).
|
|
86
101
|
|
|
87
102
|
## Output personality — JSON vs pretty
|
|
88
103
|
|
|
@@ -128,7 +143,7 @@ xmemcli version
|
|
|
128
143
|
xmemcli auth login
|
|
129
144
|
```
|
|
130
145
|
|
|
131
|
-
Version: **1.
|
|
146
|
+
Version: **1.5.1**.
|
|
132
147
|
|
|
133
148
|
|
|
134
149
|
---
|
|
@@ -196,7 +211,7 @@ Those three keys are required and must be strings. Anything else in the file is
|
|
|
196
211
|
|
|
197
212
|
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.
|
|
198
213
|
|
|
199
|
-
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.
|
|
214
|
+
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. An explicitly named Console (flag, env, or pinned) is also cross-checked against the API origin on this path — the browser redeem would fail safe on a wrong Console, but the interactive paste fallback presents the pasted key to it as a bearer — and a provably mismatched pair (mixed loopback-ness, or a production half paired with a non-production one) is refused before either worker starts, in the same words as the email sign-in. 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.
|
|
200
215
|
|
|
201
216
|
## Identity — `/whoami`
|
|
202
217
|
|
|
@@ -228,7 +243,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
|
|
|
228
243
|
|
|
229
244
|
# `xmemcli` — capabilities reference
|
|
230
245
|
|
|
231
|
-
**Version:** `1.
|
|
246
|
+
**Version:** `1.5.1` (see `xmemcli version`)
|
|
232
247
|
|
|
233
248
|
`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.
|
|
234
249
|
|
|
@@ -246,12 +261,23 @@ Every command below has dedicated help — see [Help](#help).
|
|
|
246
261
|
|
|
247
262
|
| Command | Description |
|
|
248
263
|
|---------|-------------|
|
|
249
|
-
| **`write`**
|
|
250
|
-
| **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. |
|
|
264
|
+
| **`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; by default new objects and links to them are allowed and anything else fails, and under `--scope-mode drop` only the objects a `--scope` names may be created and everything else is skipped; works with either extraction mode. `--scope-mode drop` (needs `--scope`) skips out-of-scope changes instead of failing and lists them under `changes.skipped_out_of_scope`; it creates only records a `--scope` names by primary key, and such a record need not be stored yet — create-or-update exactly the named record. `reject` is the server default and is not sent. Instead of text, a **structured write** sends `structured_mutations` — deterministic creates, updates and deletes with no extraction: `--mutations-file PATH` or `--mutations-stdin` (one JSON array, same shape as the API's `structured_mutations`; see [Structured writes](#structured-writes)) and/or `--delete TYPE:ID` (repeatable, `--scope` syntax: `Person:<xuid>` or `Person:name=Ada`; appended after the file's entries). Text and mutations are exclusive, as are the file and stdin forms; mutations refuse `--scope` and `--extraction deep`; empty or whitespace-only text is refused (exit **2**, before anything is read or sent). A text write's output lists what it extracted as `Type(identifier)` tags (`extracted`); a structured write's does not. Both carry the server's `changes` block (`created` / `updated` with per-field `old_value` → `new_value` / `deleted`, and any other bucket such as `created_keyless_objects`) whenever the write changed something, and on a drop-mode write that skipped everything, where the block carries `skipped_out_of_scope` alone. A `--no-wait` answer carries only the `write_id`; `write-status` reports the `changes` once the write completes. |
|
|
265
|
+
| **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. A completed write's non-empty `changes` block is kept, as on `write`. |
|
|
251
266
|
| **`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`). |
|
|
252
267
|
|
|
253
268
|
Requires **`--instance-id`** or **`$XMEM_INSTANCE_ID`** for read/write (or auto-pick when exactly one instance is visible — see [Global flags](#global-flags--environment)).
|
|
254
269
|
|
|
270
|
+
#### Structured writes
|
|
271
|
+
|
|
272
|
+
`--mutations-file PATH` / `--mutations-stdin` send one JSON array as the API's `structured_mutations`, as given. The server takes at most **100** entries per write, `--delete` entries included; the CLI refuses more with exit **2** before sending anything. `--mutations-stdin` needs a pipe or a file on stdin and refuses a terminal. Each entry is one of:
|
|
273
|
+
|
|
274
|
+
- **`object_mutation`** — `{"object_mutation": {"object_type": "Person", "create": {"key": {"name": "Ada"}, "values": {"age": 36}}}}`. `update` takes the same `key` + `values` (the key may be `{"xuid": "…"}`; a null value clears a field); `delete` takes `{"key": {…}}` alone.
|
|
275
|
+
- **`relation_mutation`** — `{"relation_mutation": {"relation_type": "works_at", "create": {"endpoints": [{"object_name": "person", "key": {"name": "Ada"}}, {"object_name": "company", "key": {"name": "ACME"}}]}}}`. `create` lists every participant role, each keyed by primary key or `{"xuid": "…"}`, with no `key` and no `values`. `delete` takes either `endpoints` (a subset may be given) or a top-level `"key": {"xuid": "…"}` — never both — and `"allow_bulk_delete": true` to remove every matched link (without it, a delete matching more than one link is refused). Relation `update` is always refused: re-link by deleting and creating.
|
|
276
|
+
|
|
277
|
+
`--delete TYPE:ID` appends the object `delete` above; each key field may appear once and none may be empty. A delete that matches no record is a no-op, not an error: exit **0** and no `deleted` line — in a terminal the write reads `write <id> ok · nothing matched or changed`.
|
|
278
|
+
|
|
279
|
+
A deployment that has not enabled structured writes answers **400** `Structured writes are not enabled on this deployment.`; a server that predates structured writes answers **422** with two errors, both reported: `Field required (field: text); Extra inputs are not permitted (field: structured_mutations)`. Both exit **3** with `stage: "request"`.
|
|
280
|
+
|
|
255
281
|
---
|
|
256
282
|
|
|
257
283
|
### Org inventory
|
|
@@ -261,7 +287,7 @@ Permission-filtered view of what this API key can see.
|
|
|
261
287
|
| Command | Description |
|
|
262
288
|
|---------|-------------|
|
|
263
289
|
| **`org list clusters`** | Clusters visible to this credential |
|
|
264
|
-
| **`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
|
|
290
|
+
| **`org list instances`** | Instances visible to this credential, as a bare array of records. A key minted with `data.read` but not `instance.get_own` (the supported shape for `trace …`) gets the ids only — the same set `trace ls` is scoped to — as a **degraded document** rather than an array: `{"items": [{"id": …}, …], "degraded": true, "missing_permission": "instance.get_own"}`, with the warning on stderr and in the document's `warnings`. The shape changes so a script cannot read an ids-only listing as a complete one; an array is never wrapped just to carry a warning. |
|
|
265
291
|
| **`org list keys`** | API keys visible to this credential (metadata only; plaintext never returned) |
|
|
266
292
|
|
|
267
293
|
---
|
|
@@ -460,27 +486,30 @@ learn; the CLI composes the Console's search clauses itself.
|
|
|
460
486
|
|
|
461
487
|
| Command | Console view | Description |
|
|
462
488
|
|---------|--------------|-------------|
|
|
463
|
-
| **`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). |
|
|
464
|
-
| **`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
|
|
465
|
-
| **`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**. |
|
|
489
|
+
| **`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` — `delete` is `--search` only), `--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). |
|
|
490
|
+
| **`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` (also `delete`), `--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`, `--limit N` (default 100, Console cap 500), `--cursor TS_MS:TRACE_ID:INSTANCE_ID` (the previous page's three cursor fields, joined with colons). Returns the Inspect document: `merged[]` hits with `kind`, `href` (`/write/{trace}`, `/read/{trace}`, `/create/…`, `/update/…`, `/evolution/…`, or `#` for an instance deletion, which has no detail page), `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id` / `api_key_name` / `api_key_prefix`, `status`, `time_taken_ms`, `match_items`; and the paging fields `has_more`, `next_before_sort_at_ms`, `next_before_trace_id`, `next_before_instance_id`, `truncated`. |
|
|
491
|
+
| **`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 deletion's `DL0000_aaa` id is answered without a request: it has no detail page. A miss — the Console's `{"found": false}`, or its 404 for a trace outside your scope — is `stage: "not_found"`, exit **3**. |
|
|
466
492
|
|
|
467
|
-
**`--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 `--
|
|
493
|
+
**`--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 `--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).
|
|
468
494
|
|
|
469
|
-
**
|
|
495
|
+
**Breaking change in 1.4.0:** `trace ls --search` now asks the Console for one *page* (100 rows by default) instead of the whole capped result, under the Console's paging version 2 (`paging=2`), in which every search mode pages. A single call is no longer the complete answer: loop on `has_more`, and do not read a row count, or an absent `warnings` array, as completeness. Against a Console that predates paging the CLI says so in a warning, because that Console still answers whole and capped. What it tests is the absence of `has_more`, not the rows: a Console that pages sends the paging fields even when it found nothing, and an empty answer from one that does not is exactly where a row count misleads.
|
|
496
|
+
|
|
497
|
+
**Paging a search — read this before scripting `--search`.** `trace ls --search` pages like `trace ls`, with the Inspect endpoint's own keyset cursor: pass the previous page's `next_before_sort_at_ms`, `next_before_trace_id` and `next_before_instance_id` back as `--cursor TS_MS:TRACE_ID:INSTANCE_ID` until `has_more` is `false`. Follow `has_more`, never the row count: the Console narrows each page after its row limits (type and status resolution, console-read exclusion), so a page can hold fewer rows than `--limit` — even none — and still have more. Paging reaches every match, with one exception the Console reports itself: a search with no filter that shapes the query (only scope such as `--instance`, `--session-id`, `--user-id`, `--api-key-id` or `--hours-back`) is the Console's recent-activity view, which reaches back 180 days. When this credential has older operations than that, the last page carries `truncated: true` and the CLI emits a **warning** (stderr, and folded into the JSON document's `warnings` array) naming the way past it — a filter such as `--trace-type` or `--text`. That flag comes from the endpoint, which knows what it could not reach; the CLI no longer guesses at a cut from how many rows came back. Asking for a page changes two things worth knowing before you script it: a free-text search answers from one candidate window per page rather than the exhaustive second pass the whole-result path ran, so a rare needle can give a short — even empty — first page with `has_more: true` where one call used to find the match; and a paged request is all-or-nothing, so a Console-side source failure is a `502` rather than a partial answer at exit 0. While a Console deploy is rolling out, a `--cursor` request can reach a node that does not page yet; it ignores the cursor, and the CLI exits **3** saying so rather than hand back its newest rows as the next page — retry the same `--cursor`.
|
|
470
498
|
|
|
471
499
|
**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.
|
|
472
500
|
|
|
473
501
|
**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`.
|
|
474
502
|
|
|
475
|
-
> **Three of those never resolve for an API key**, super admin or not, and this is by design rather than a gap. `user_email` is always absent: the accounts `/users` lookup is closed to keys (an agent may learn who *it* is, never enumerate the organisation's users), so rows keep `user_id` and you resolve names yourself if you need them — and a `User:<email>` search clause matches nothing; filter by `user_id`. `api_key_name` / `api_key_prefix` resolve only for the caller's **own** keys, because `GET /api-keys` scopes 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}`, `sql_queries
|
|
503
|
+
> **Three of those never resolve for an API key**, super admin or not, and this is by design rather than a gap. `user_email` is always absent: the accounts `/users` lookup is closed to keys (an agent may learn who *it* is, never enumerate the organisation's users), so rows keep `user_id` and you resolve names yourself if you need them — and a `User:<email>` search clause matches nothing; filter by `user_id`. For that reason `--user-id` refuses a value containing `@` as a usage error (exit **2**) on both `trace ls` and `trace ls --search`, rather than sending a filter that can only ever return an empty result at exit 0. `api_key_name` / `api_key_prefix` resolve only for the caller's **own** keys, because `GET /api-keys` scopes 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}`, `sql_queries` (the rows owned by the read's instance, for every caller who may open the read), and two tiers of gated fields. **Super admin only** — the accounts `is_super_admin` flag decides, for a key exactly as for its owner's browser session: `llm_calls[]` (`llm_call_id`, `gateway_request_id`, `model_type`, `provider`, …), `full_error_message`, raw token counts, and unsanitised xuids. **Callers with no instance restriction at all** — a super admin, or a browser session on a `KINGDOM_LOCAL` Console; never a scoped key: `ddl_queries`, `schema_generation`, and the ownerless rows of `sql_queries`.
|
|
476
504
|
|
|
477
|
-
**A scoped key gets `[]` for `ddl_queries` / `schema_generation`, not the rows.** Those two come from `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 none". `llm_calls[]` behaves the same way and always has. `sql_queries` is
|
|
505
|
+
**A scoped key gets `[]` for `ddl_queries` / `schema_generation`, not the rows.** Those two come from `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 none". `llm_calls[]` behaves the same way (gated on super admin) and always has. `sql_queries` is split: statements recorded with their owning instance reach every caller who may open the read, even when another instance reused the trace id, while statements recorded before ownership was tracked carry no owner and reach unrestricted callers only. A scoped key therefore sees `[]` for reads that old. To let a script tell the two apart, the Console sets a sibling boolean next to each — `sql_queries_withheld`, `ddl_queries_withheld`, `schema_generation_withheld` — that is `true` exactly when a row that exists was withheld from this caller: for `ddl_queries` / `schema_generation` that is every scoped caller, for `sql_queries` a scoped caller on a read that has ownerless rows. So `[]` with `…_withheld: false` is "none recorded", `[]` with `…_withheld: true` is "not yours to see". **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.
|
|
478
506
|
|
|
479
507
|
**Who sees what — the Console decides; `auth status` tells you which view to expect.**
|
|
480
508
|
|
|
481
509
|
- A regular key sees the operations of the instances the accounts API lists for it — the same set `org list instances` prints. That is at most what its owner sees in the Console, never more.
|
|
482
510
|
- 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 status` shows the codes under `permissions`) is refused with **403 → exit 3, stage `permission`**, 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`).
|
|
483
511
|
- 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_admin` flag 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`, and `api_key_name` / `api_key_prefix` for other people's keys; see the note above). `auth status` reports it as `super_admin`.
|
|
512
|
+
- When the Console **cannot** work out that scope — its accounts API is unreachable, answers non-2xx, is undecodable, or is not configured — it answers **503** with `{"error": "instance_scope_unresolved", "reason": "accounts_…"}` (or `accounts_unavailable` when the key's own lookup failed the same way) instead of an empty document. The CLI reports that as **exit 3, stage `unavailable`**, with a message that says the scope could not be resolved and that this is *not* "no operations": a zero-row answer from a Console that could not decide would have read as a quiet instance.
|
|
484
513
|
- 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`.
|
|
485
514
|
|
|
486
515
|
**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.
|
|
@@ -493,7 +522,7 @@ learn; the CLI composes the Console's search clauses itself.
|
|
|
493
522
|
|
|
494
523
|
| Command | Description |
|
|
495
524
|
|---------|-------------|
|
|
496
|
-
| **`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). |
|
|
525
|
+
| **`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). An explicitly named Console (`--console-url`, `$XMEM_CONSOLE_URL`, or the one pinned by a single-origin URL) is cross-checked against the API origin before the browser opens or a pasted key is sent anywhere — a provably mismatched pair (mixed loopback-ness, or a production half paired with a non-production one) is a usage error (exit **2**), on the interactive paste path as much as on the browser path. 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). |
|
|
497
526
|
| **`auth logout`** | Remove active `.xmemrc.json` |
|
|
498
527
|
| **`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). |
|
|
499
528
|
| **`mcp <instance-id>`** | Serve one instance to an MCP client over stdio, using this CLI's credential. Not for humans — see below. |
|
|
@@ -619,6 +648,7 @@ condition, and the hook author is the one reading stderr.
|
|
|
619
648
|
| **Other 4xx/5xx** | **3** | `request` | Request failed for another reason (404, 422, 5xx, …) |
|
|
620
649
|
| **402** (quota exhausted) | **6** | `quota` | Quota window exhausted — human must extend quota in Console |
|
|
621
650
|
| **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 |
|
|
651
|
+
| **503** with body `{"error": "instance_scope_unresolved" \| "accounts_unavailable", "reason": …}` | **3** | `unavailable` | The Console could not resolve what the key may see (its accounts API is unreachable, non-2xx, undecodable, or not configured). **Not** "no operations" and **not** a bad credential — nothing was listed or checked; retry once accounts recovers. Recognised on the body, so it is named the same on every command that meets it. |
|
|
622
652
|
|
|
623
653
|
#### How to interpret failures
|
|
624
654
|
|
|
@@ -634,6 +664,10 @@ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not
|
|
|
634
664
|
|
|
635
665
|
**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.
|
|
636
666
|
|
|
667
|
+
**New in 1.5.1:** `write --scope-mode drop` asks for a scoped write that skips and reports changes outside its scope instead of failing, and that creates only the records its `--scope` names — which may not be stored yet. The report is `changes.skipped_out_of_scope`: one terminal line per operation, type and set of fields, carrying `count=N`, the number of records it stands for, and naming a record's key only when the `--scope` named that record. A drop-mode write that skipped everything still returns a `changes` block, holding that report alone. A server that predates drop mode refuses `mode` in the scope (exit **3**), which is a clean failure; a deployment part-way through rolling it out is not, so publish 1.5.1 only after the server side is fully deployed — see [Releasing `xmemcli`](../docs/cli/XMEMCLI-RELEASE.md). `--scope-mode reject` is never sent, so plain scoped writes work against any server.
|
|
668
|
+
|
|
669
|
+
**New in 1.5.1:** `write` and `write-status` keep the server's `changes` block — what the write created, updated (field by field, old → new) and deleted, plus bulk summaries and advisory records — verbatim in `--json` output. In a terminal each change is one indented line: a bulk delete or update reads `deleted 3 × Ticket where status='open'`, one the server refused reads `not deleted Ticket: refused — <reason>`, the advisory `overwritten_field_values` / `merged_field_conflicts` records follow as `note:` lines, and past 50 lines the rest is counted in one closing line (`--json` is never truncated); `write-status` with several ids prints a status line per write, each followed by its changes. `changes` is omitted when the write changed nothing, so that output keeps its earlier shape; a drop-mode write that only skipped is the one write that changed nothing and still carries the block. `write` also takes structured mutations (`--mutations-file`, `--mutations-stdin`, `--delete TYPE:ID`) in place of text; the text positional is now optional, and a write with neither text nor mutations, or whose text is empty or only whitespace, is a usage error (exit **2**, `stage: "usage"`). Before 1.5.1 `write ""` reached the server, whose 400 exited **3** with `stage: "request"`, and whitespace-only text was sent and accepted. A structured write's output carries no `extracted`, since it runs no extraction; on a text write the tags now read `Type(identifier)` (`Person(name='Ada')`), as documented, where they read `?(Type)`. A malformed `--scope` (on `write` and `read`) or `--delete` — now including a key field given twice or an empty value — reports `stage: "usage"`, still exit **2**; `--scope` reported `"request"`. `--mutations-stdin` and `--schema-stdin` refuse a terminal instead of waiting on it, and every input file and piped input is read by one reader and decoded as strict UTF-8, so an input that is not UTF-8 (`--mutations-file`, `--schema-file`, `--migration-plan`, `schema suggestions decide --file`, the `xmd validate` / `convert` / `enhance` schema file, or the same bytes piped to `--mutations-stdin` / `--schema-stdin`) is a usage error at exit **2** instead of a traceback at exit **1** — or, piped, a garbled write at exit **0** — and a leading UTF-8 byte-order mark is accepted. Wherever an HTTP error from the API is reported as `HTTP <status>: …`, a body in the API's error envelope now gives the server's own message (`HTTP 400: <message>`, plus the request `field` when the server names one) instead of the first 200 characters of the raw body.
|
|
670
|
+
|
|
637
671
|
**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.
|
|
638
672
|
|
|
639
673
|
**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.)
|
|
@@ -12,6 +12,8 @@ xmemcli auth logout
|
|
|
12
12
|
xmemcli auth status
|
|
13
13
|
xmemcli read <query>
|
|
14
14
|
xmemcli write <text>
|
|
15
|
+
xmemcli write (--mutations-file PATH | --mutations-stdin) [--delete TYPE:ID]…
|
|
16
|
+
xmemcli write --delete TYPE:ID [--delete TYPE:ID]…
|
|
15
17
|
xmemcli write-status <write_id> [<id> …]
|
|
16
18
|
xmemcli quota
|
|
17
19
|
xmemcli instance templates
|
|
@@ -21,7 +23,7 @@ xmemcli binding add <instance_id> [--tier autoload|available|off] [--scope proje
|
|
|
21
23
|
xmemcli binding remove <instance_id> [--scope project|user]
|
|
22
24
|
xmemcli context [--text] [--max-tokens N] [--no-live-state]
|
|
23
25
|
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]
|
|
24
|
-
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]
|
|
26
|
+
xmemcli trace ls --search [--text …] [--request …] [--response …] [--object NAME]… [--field NAME]… [--status S]… [--trace-id ID]… [--session-id S] [--instance ID] [--trace-type T|delete]… [--hours-back N] [--limit N] [--cursor TS_MS:TRACE_ID:INSTANCE_ID]
|
|
25
27
|
xmemcli trace get <trace_id> [--trace-type write|read|create|update|evolution]
|
|
26
28
|
```
|
|
27
29
|
|
|
@@ -40,6 +42,9 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
|
|
|
40
42
|
| `xmemcli write --extraction fast` (default) | `extraction_logic: "fast"` |
|
|
41
43
|
| `xmemcli write --extraction deep` | `extraction_logic: "deep"` |
|
|
42
44
|
| `xmemcli write --no-wait` | async write; poll with `write-status` |
|
|
45
|
+
| `xmemcli write --mutations-file m.json` / `--mutations-stdin` | `structured_mutations` — the JSON array as given; no `text`, no `extraction_logic` |
|
|
46
|
+
| `xmemcli write --scope Pet:name=Luna --scope-mode drop` | `scope: {objects: [...], mode: "drop"}` — out-of-scope changes are skipped and reported; `reject` is not sent |
|
|
47
|
+
| `xmemcli write --delete Person:name=Ada` | appends `{"object_mutation": {"object_type": "Person", "delete": {"key": {"name": "Ada"}}}}` (`Person:<xuid>` sends `{"xuid": …}` as the key) |
|
|
43
48
|
| `xmemcli read --read-mode single` (default) | `mode: "single-answer"` |
|
|
44
49
|
| `xmemcli read --read-mode raw` | `mode: "raw-tables"` |
|
|
45
50
|
| `xmemcli read --read-mode xresponse` | `mode: "xresponse"` |
|
|
@@ -50,7 +55,7 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
|
|
|
50
55
|
|
|
51
56
|
## Curated output
|
|
52
57
|
|
|
53
|
-
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.
|
|
58
|
+
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). A text write keeps a compact `extracted` summary (`Type(identifier)` tags); a structured write runs no extraction and has none. `write` and `write-status` keep the server's `changes` block (created / updated with old → new / deleted, bulk summaries, advisory records) when the write changed something. `--verbose` adds `trace_id` and `console_url` when present.
|
|
54
59
|
|
|
55
60
|
**`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`, …
|
|
56
61
|
|
|
@@ -74,7 +79,17 @@ NEXT=$(echo "$PAGE" | jq -r '"\(.next_before_ts_ms):\(.next_before_trace_id)"')
|
|
|
74
79
|
xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
|
|
75
80
|
```
|
|
76
81
|
|
|
77
|
-
`--search`
|
|
82
|
+
`--search` pages too, with the Inspect endpoint's three cursor fields — follow `has_more`, since a page can come back short:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
PAGE=$(xmemcli trace ls --search --text "Ada")
|
|
86
|
+
# `select(.has_more)` so the last page yields an empty string rather than the literal `null:null:null`,
|
|
87
|
+
# which `--cursor` refuses.
|
|
88
|
+
NEXT=$(echo "$PAGE" | jq -r 'select(.has_more) | "\(.next_before_sort_at_ms):\(.next_before_trace_id):\(.next_before_instance_id)"')
|
|
89
|
+
[ -n "$NEXT" ] && xmemcli trace ls --search --text "Ada" --cursor "$NEXT"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A search with no filter shaping it is the Console's recent-activity view, which reaches back 180 days; when older operations exist, its last page says `truncated` and the CLI warns. 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: [](#trace--the-consoles-operation-traces).
|
|
78
93
|
|
|
79
94
|
## Output personality — JSON vs pretty
|
|
80
95
|
|
|
@@ -120,7 +135,7 @@ xmemcli version
|
|
|
120
135
|
xmemcli auth login
|
|
121
136
|
```
|
|
122
137
|
|
|
123
|
-
Version: **1.
|
|
138
|
+
Version: **1.5.1**.
|
|
124
139
|
|
|
125
140
|
|
|
126
141
|
---
|
|
@@ -188,7 +203,7 @@ Those three keys are required and must be strings. Anything else in the file is
|
|
|
188
203
|
|
|
189
204
|
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.
|
|
190
205
|
|
|
191
|
-
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.
|
|
206
|
+
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. An explicitly named Console (flag, env, or pinned) is also cross-checked against the API origin on this path — the browser redeem would fail safe on a wrong Console, but the interactive paste fallback presents the pasted key to it as a bearer — and a provably mismatched pair (mixed loopback-ness, or a production half paired with a non-production one) is refused before either worker starts, in the same words as the email sign-in. 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.
|
|
192
207
|
|
|
193
208
|
## Identity — `/whoami`
|
|
194
209
|
|
|
@@ -220,7 +235,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
|
|
|
220
235
|
|
|
221
236
|
# `xmemcli` — capabilities reference
|
|
222
237
|
|
|
223
|
-
**Version:** `1.
|
|
238
|
+
**Version:** `1.5.1` (see `xmemcli version`)
|
|
224
239
|
|
|
225
240
|
`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.
|
|
226
241
|
|
|
@@ -238,12 +253,23 @@ Every command below has dedicated help — see [Help](#help).
|
|
|
238
253
|
|
|
239
254
|
| Command | Description |
|
|
240
255
|
|---------|-------------|
|
|
241
|
-
| **`write`**
|
|
242
|
-
| **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. |
|
|
256
|
+
| **`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; by default new objects and links to them are allowed and anything else fails, and under `--scope-mode drop` only the objects a `--scope` names may be created and everything else is skipped; works with either extraction mode. `--scope-mode drop` (needs `--scope`) skips out-of-scope changes instead of failing and lists them under `changes.skipped_out_of_scope`; it creates only records a `--scope` names by primary key, and such a record need not be stored yet — create-or-update exactly the named record. `reject` is the server default and is not sent. Instead of text, a **structured write** sends `structured_mutations` — deterministic creates, updates and deletes with no extraction: `--mutations-file PATH` or `--mutations-stdin` (one JSON array, same shape as the API's `structured_mutations`; see [Structured writes](#structured-writes)) and/or `--delete TYPE:ID` (repeatable, `--scope` syntax: `Person:<xuid>` or `Person:name=Ada`; appended after the file's entries). Text and mutations are exclusive, as are the file and stdin forms; mutations refuse `--scope` and `--extraction deep`; empty or whitespace-only text is refused (exit **2**, before anything is read or sent). A text write's output lists what it extracted as `Type(identifier)` tags (`extracted`); a structured write's does not. Both carry the server's `changes` block (`created` / `updated` with per-field `old_value` → `new_value` / `deleted`, and any other bucket such as `created_keyless_objects`) whenever the write changed something, and on a drop-mode write that skipped everything, where the block carries `skipped_out_of_scope` alone. A `--no-wait` answer carries only the `write_id`; `write-status` reports the `changes` once the write completes. |
|
|
257
|
+
| **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. A completed write's non-empty `changes` block is kept, as on `write`. |
|
|
243
258
|
| **`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`). |
|
|
244
259
|
|
|
245
260
|
Requires **`--instance-id`** or **`$XMEM_INSTANCE_ID`** for read/write (or auto-pick when exactly one instance is visible — see [Global flags](#global-flags--environment)).
|
|
246
261
|
|
|
262
|
+
#### Structured writes
|
|
263
|
+
|
|
264
|
+
`--mutations-file PATH` / `--mutations-stdin` send one JSON array as the API's `structured_mutations`, as given. The server takes at most **100** entries per write, `--delete` entries included; the CLI refuses more with exit **2** before sending anything. `--mutations-stdin` needs a pipe or a file on stdin and refuses a terminal. Each entry is one of:
|
|
265
|
+
|
|
266
|
+
- **`object_mutation`** — `{"object_mutation": {"object_type": "Person", "create": {"key": {"name": "Ada"}, "values": {"age": 36}}}}`. `update` takes the same `key` + `values` (the key may be `{"xuid": "…"}`; a null value clears a field); `delete` takes `{"key": {…}}` alone.
|
|
267
|
+
- **`relation_mutation`** — `{"relation_mutation": {"relation_type": "works_at", "create": {"endpoints": [{"object_name": "person", "key": {"name": "Ada"}}, {"object_name": "company", "key": {"name": "ACME"}}]}}}`. `create` lists every participant role, each keyed by primary key or `{"xuid": "…"}`, with no `key` and no `values`. `delete` takes either `endpoints` (a subset may be given) or a top-level `"key": {"xuid": "…"}` — never both — and `"allow_bulk_delete": true` to remove every matched link (without it, a delete matching more than one link is refused). Relation `update` is always refused: re-link by deleting and creating.
|
|
268
|
+
|
|
269
|
+
`--delete TYPE:ID` appends the object `delete` above; each key field may appear once and none may be empty. A delete that matches no record is a no-op, not an error: exit **0** and no `deleted` line — in a terminal the write reads `write <id> ok · nothing matched or changed`.
|
|
270
|
+
|
|
271
|
+
A deployment that has not enabled structured writes answers **400** `Structured writes are not enabled on this deployment.`; a server that predates structured writes answers **422** with two errors, both reported: `Field required (field: text); Extra inputs are not permitted (field: structured_mutations)`. Both exit **3** with `stage: "request"`.
|
|
272
|
+
|
|
247
273
|
---
|
|
248
274
|
|
|
249
275
|
### Org inventory
|
|
@@ -253,7 +279,7 @@ Permission-filtered view of what this API key can see.
|
|
|
253
279
|
| Command | Description |
|
|
254
280
|
|---------|-------------|
|
|
255
281
|
| **`org list clusters`** | Clusters visible to this credential |
|
|
256
|
-
| **`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
|
|
282
|
+
| **`org list instances`** | Instances visible to this credential, as a bare array of records. A key minted with `data.read` but not `instance.get_own` (the supported shape for `trace …`) gets the ids only — the same set `trace ls` is scoped to — as a **degraded document** rather than an array: `{"items": [{"id": …}, …], "degraded": true, "missing_permission": "instance.get_own"}`, with the warning on stderr and in the document's `warnings`. The shape changes so a script cannot read an ids-only listing as a complete one; an array is never wrapped just to carry a warning. |
|
|
257
283
|
| **`org list keys`** | API keys visible to this credential (metadata only; plaintext never returned) |
|
|
258
284
|
|
|
259
285
|
---
|
|
@@ -452,27 +478,30 @@ learn; the CLI composes the Console's search clauses itself.
|
|
|
452
478
|
|
|
453
479
|
| Command | Console view | Description |
|
|
454
480
|
|---------|--------------|-------------|
|
|
455
|
-
| **`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). |
|
|
456
|
-
| **`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
|
|
457
|
-
| **`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**. |
|
|
481
|
+
| **`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` — `delete` is `--search` only), `--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). |
|
|
482
|
+
| **`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` (also `delete`), `--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`, `--limit N` (default 100, Console cap 500), `--cursor TS_MS:TRACE_ID:INSTANCE_ID` (the previous page's three cursor fields, joined with colons). Returns the Inspect document: `merged[]` hits with `kind`, `href` (`/write/{trace}`, `/read/{trace}`, `/create/…`, `/update/…`, `/evolution/…`, or `#` for an instance deletion, which has no detail page), `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id` / `api_key_name` / `api_key_prefix`, `status`, `time_taken_ms`, `match_items`; and the paging fields `has_more`, `next_before_sort_at_ms`, `next_before_trace_id`, `next_before_instance_id`, `truncated`. |
|
|
483
|
+
| **`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 deletion's `DL0000_aaa` id is answered without a request: it has no detail page. A miss — the Console's `{"found": false}`, or its 404 for a trace outside your scope — is `stage: "not_found"`, exit **3**. |
|
|
458
484
|
|
|
459
|
-
**`--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 `--
|
|
485
|
+
**`--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 `--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).
|
|
460
486
|
|
|
461
|
-
**
|
|
487
|
+
**Breaking change in 1.4.0:** `trace ls --search` now asks the Console for one *page* (100 rows by default) instead of the whole capped result, under the Console's paging version 2 (`paging=2`), in which every search mode pages. A single call is no longer the complete answer: loop on `has_more`, and do not read a row count, or an absent `warnings` array, as completeness. Against a Console that predates paging the CLI says so in a warning, because that Console still answers whole and capped. What it tests is the absence of `has_more`, not the rows: a Console that pages sends the paging fields even when it found nothing, and an empty answer from one that does not is exactly where a row count misleads.
|
|
488
|
+
|
|
489
|
+
**Paging a search — read this before scripting `--search`.** `trace ls --search` pages like `trace ls`, with the Inspect endpoint's own keyset cursor: pass the previous page's `next_before_sort_at_ms`, `next_before_trace_id` and `next_before_instance_id` back as `--cursor TS_MS:TRACE_ID:INSTANCE_ID` until `has_more` is `false`. Follow `has_more`, never the row count: the Console narrows each page after its row limits (type and status resolution, console-read exclusion), so a page can hold fewer rows than `--limit` — even none — and still have more. Paging reaches every match, with one exception the Console reports itself: a search with no filter that shapes the query (only scope such as `--instance`, `--session-id`, `--user-id`, `--api-key-id` or `--hours-back`) is the Console's recent-activity view, which reaches back 180 days. When this credential has older operations than that, the last page carries `truncated: true` and the CLI emits a **warning** (stderr, and folded into the JSON document's `warnings` array) naming the way past it — a filter such as `--trace-type` or `--text`. That flag comes from the endpoint, which knows what it could not reach; the CLI no longer guesses at a cut from how many rows came back. Asking for a page changes two things worth knowing before you script it: a free-text search answers from one candidate window per page rather than the exhaustive second pass the whole-result path ran, so a rare needle can give a short — even empty — first page with `has_more: true` where one call used to find the match; and a paged request is all-or-nothing, so a Console-side source failure is a `502` rather than a partial answer at exit 0. While a Console deploy is rolling out, a `--cursor` request can reach a node that does not page yet; it ignores the cursor, and the CLI exits **3** saying so rather than hand back its newest rows as the next page — retry the same `--cursor`.
|
|
462
490
|
|
|
463
491
|
**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.
|
|
464
492
|
|
|
465
493
|
**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`.
|
|
466
494
|
|
|
467
|
-
> **Three of those never resolve for an API key**, super admin or not, and this is by design rather than a gap. `user_email` is always absent: the accounts `/users` lookup is closed to keys (an agent may learn who *it* is, never enumerate the organisation's users), so rows keep `user_id` and you resolve names yourself if you need them — and a `User:<email>` search clause matches nothing; filter by `user_id`. `api_key_name` / `api_key_prefix` resolve only for the caller's **own** keys, because `GET /api-keys` scopes 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}`, `sql_queries
|
|
495
|
+
> **Three of those never resolve for an API key**, super admin or not, and this is by design rather than a gap. `user_email` is always absent: the accounts `/users` lookup is closed to keys (an agent may learn who *it* is, never enumerate the organisation's users), so rows keep `user_id` and you resolve names yourself if you need them — and a `User:<email>` search clause matches nothing; filter by `user_id`. For that reason `--user-id` refuses a value containing `@` as a usage error (exit **2**) on both `trace ls` and `trace ls --search`, rather than sending a filter that can only ever return an empty result at exit 0. `api_key_name` / `api_key_prefix` resolve only for the caller's **own** keys, because `GET /api-keys` scopes 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}`, `sql_queries` (the rows owned by the read's instance, for every caller who may open the read), and two tiers of gated fields. **Super admin only** — the accounts `is_super_admin` flag decides, for a key exactly as for its owner's browser session: `llm_calls[]` (`llm_call_id`, `gateway_request_id`, `model_type`, `provider`, …), `full_error_message`, raw token counts, and unsanitised xuids. **Callers with no instance restriction at all** — a super admin, or a browser session on a `KINGDOM_LOCAL` Console; never a scoped key: `ddl_queries`, `schema_generation`, and the ownerless rows of `sql_queries`.
|
|
468
496
|
|
|
469
|
-
**A scoped key gets `[]` for `ddl_queries` / `schema_generation`, not the rows.** Those two come from `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 none". `llm_calls[]` behaves the same way and always has. `sql_queries` is
|
|
497
|
+
**A scoped key gets `[]` for `ddl_queries` / `schema_generation`, not the rows.** Those two come from `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 none". `llm_calls[]` behaves the same way (gated on super admin) and always has. `sql_queries` is split: statements recorded with their owning instance reach every caller who may open the read, even when another instance reused the trace id, while statements recorded before ownership was tracked carry no owner and reach unrestricted callers only. A scoped key therefore sees `[]` for reads that old. To let a script tell the two apart, the Console sets a sibling boolean next to each — `sql_queries_withheld`, `ddl_queries_withheld`, `schema_generation_withheld` — that is `true` exactly when a row that exists was withheld from this caller: for `ddl_queries` / `schema_generation` that is every scoped caller, for `sql_queries` a scoped caller on a read that has ownerless rows. So `[]` with `…_withheld: false` is "none recorded", `[]` with `…_withheld: true` is "not yours to see". **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.
|
|
470
498
|
|
|
471
499
|
**Who sees what — the Console decides; `auth status` tells you which view to expect.**
|
|
472
500
|
|
|
473
501
|
- A regular key sees the operations of the instances the accounts API lists for it — the same set `org list instances` prints. That is at most what its owner sees in the Console, never more.
|
|
474
502
|
- 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 status` shows the codes under `permissions`) is refused with **403 → exit 3, stage `permission`**, 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`).
|
|
475
503
|
- 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_admin` flag 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`, and `api_key_name` / `api_key_prefix` for other people's keys; see the note above). `auth status` reports it as `super_admin`.
|
|
504
|
+
- When the Console **cannot** work out that scope — its accounts API is unreachable, answers non-2xx, is undecodable, or is not configured — it answers **503** with `{"error": "instance_scope_unresolved", "reason": "accounts_…"}` (or `accounts_unavailable` when the key's own lookup failed the same way) instead of an empty document. The CLI reports that as **exit 3, stage `unavailable`**, with a message that says the scope could not be resolved and that this is *not* "no operations": a zero-row answer from a Console that could not decide would have read as a quiet instance.
|
|
476
505
|
- 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`.
|
|
477
506
|
|
|
478
507
|
**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.
|
|
@@ -485,7 +514,7 @@ learn; the CLI composes the Console's search clauses itself.
|
|
|
485
514
|
|
|
486
515
|
| Command | Description |
|
|
487
516
|
|---------|-------------|
|
|
488
|
-
| **`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). |
|
|
517
|
+
| **`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). An explicitly named Console (`--console-url`, `$XMEM_CONSOLE_URL`, or the one pinned by a single-origin URL) is cross-checked against the API origin before the browser opens or a pasted key is sent anywhere — a provably mismatched pair (mixed loopback-ness, or a production half paired with a non-production one) is a usage error (exit **2**), on the interactive paste path as much as on the browser path. 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). |
|
|
489
518
|
| **`auth logout`** | Remove active `.xmemrc.json` |
|
|
490
519
|
| **`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). |
|
|
491
520
|
| **`mcp <instance-id>`** | Serve one instance to an MCP client over stdio, using this CLI's credential. Not for humans — see below. |
|
|
@@ -611,6 +640,7 @@ condition, and the hook author is the one reading stderr.
|
|
|
611
640
|
| **Other 4xx/5xx** | **3** | `request` | Request failed for another reason (404, 422, 5xx, …) |
|
|
612
641
|
| **402** (quota exhausted) | **6** | `quota` | Quota window exhausted — human must extend quota in Console |
|
|
613
642
|
| **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 |
|
|
643
|
+
| **503** with body `{"error": "instance_scope_unresolved" \| "accounts_unavailable", "reason": …}` | **3** | `unavailable` | The Console could not resolve what the key may see (its accounts API is unreachable, non-2xx, undecodable, or not configured). **Not** "no operations" and **not** a bad credential — nothing was listed or checked; retry once accounts recovers. Recognised on the body, so it is named the same on every command that meets it. |
|
|
614
644
|
|
|
615
645
|
#### How to interpret failures
|
|
616
646
|
|
|
@@ -626,6 +656,10 @@ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not
|
|
|
626
656
|
|
|
627
657
|
**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.
|
|
628
658
|
|
|
659
|
+
**New in 1.5.1:** `write --scope-mode drop` asks for a scoped write that skips and reports changes outside its scope instead of failing, and that creates only the records its `--scope` names — which may not be stored yet. The report is `changes.skipped_out_of_scope`: one terminal line per operation, type and set of fields, carrying `count=N`, the number of records it stands for, and naming a record's key only when the `--scope` named that record. A drop-mode write that skipped everything still returns a `changes` block, holding that report alone. A server that predates drop mode refuses `mode` in the scope (exit **3**), which is a clean failure; a deployment part-way through rolling it out is not, so publish 1.5.1 only after the server side is fully deployed — see [Releasing `xmemcli`](../docs/cli/XMEMCLI-RELEASE.md). `--scope-mode reject` is never sent, so plain scoped writes work against any server.
|
|
660
|
+
|
|
661
|
+
**New in 1.5.1:** `write` and `write-status` keep the server's `changes` block — what the write created, updated (field by field, old → new) and deleted, plus bulk summaries and advisory records — verbatim in `--json` output. In a terminal each change is one indented line: a bulk delete or update reads `deleted 3 × Ticket where status='open'`, one the server refused reads `not deleted Ticket: refused — <reason>`, the advisory `overwritten_field_values` / `merged_field_conflicts` records follow as `note:` lines, and past 50 lines the rest is counted in one closing line (`--json` is never truncated); `write-status` with several ids prints a status line per write, each followed by its changes. `changes` is omitted when the write changed nothing, so that output keeps its earlier shape; a drop-mode write that only skipped is the one write that changed nothing and still carries the block. `write` also takes structured mutations (`--mutations-file`, `--mutations-stdin`, `--delete TYPE:ID`) in place of text; the text positional is now optional, and a write with neither text nor mutations, or whose text is empty or only whitespace, is a usage error (exit **2**, `stage: "usage"`). Before 1.5.1 `write ""` reached the server, whose 400 exited **3** with `stage: "request"`, and whitespace-only text was sent and accepted. A structured write's output carries no `extracted`, since it runs no extraction; on a text write the tags now read `Type(identifier)` (`Person(name='Ada')`), as documented, where they read `?(Type)`. A malformed `--scope` (on `write` and `read`) or `--delete` — now including a key field given twice or an empty value — reports `stage: "usage"`, still exit **2**; `--scope` reported `"request"`. `--mutations-stdin` and `--schema-stdin` refuse a terminal instead of waiting on it, and every input file and piped input is read by one reader and decoded as strict UTF-8, so an input that is not UTF-8 (`--mutations-file`, `--schema-file`, `--migration-plan`, `schema suggestions decide --file`, the `xmd validate` / `convert` / `enhance` schema file, or the same bytes piped to `--mutations-stdin` / `--schema-stdin`) is a usage error at exit **2** instead of a traceback at exit **1** — or, piped, a garbled write at exit **0** — and a leading UTF-8 byte-order mark is accepted. Wherever an HTTP error from the API is reported as `HTTP <status>: …`, a body in the API's error envelope now gives the server's own message (`HTTP 400: <message>`, plus the request `field` when the server names one) instead of the first 200 characters of the raw body.
|
|
662
|
+
|
|
629
663
|
**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.
|
|
630
664
|
|
|
631
665
|
**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.)
|