xmemcli 1.2.0__tar.gz → 1.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xmemcli
3
- Version: 1.2.0
3
+ Version: 1.3.0
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
@@ -28,9 +28,12 @@ xmemcli binding list [--autoload]
28
28
  xmemcli binding add <instance_id> [--tier autoload|available|off] [--scope project|user]
29
29
  xmemcli binding remove <instance_id> [--scope project|user]
30
30
  xmemcli context [--text] [--max-tokens N] [--no-live-state]
31
+ 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]
33
+ xmemcli trace get <trace_id> [--trace-type write|read|create|update|evolution]
31
34
  ```
32
35
 
33
- Global flags / env: `--api-key` / `$XMEM_API_KEY`, `--instance-id` / `$XMEM_INSTANCE_ID`, `--api-url` / `$XMEM_API_URL`, `--rc-dir` / `$XMEM_RC_DIR`, `--binding-dir` / `$XMEM_BINDING_DIR`, `--json`, `--verbose`. See `xmemcli help options`.
36
+ Global flags / env: `--api-key` / `$XMEM_API_KEY`, `--instance-id` / `$XMEM_INSTANCE_ID`, `--api-url` / `$XMEM_API_URL`, `--rc-dir` / `$XMEM_RC_DIR`, `--binding-dir` / `$XMEM_BINDING_DIR`, `--json`, `--verbose`. See `xmemcli help options`. `trace …` additionally take `--console-url` / `$XMEM_CONSOLE_URL` (the Console origin that serves them; inferred for the public and local pairs).
34
37
 
35
38
  Credential resolution: `--api-key` → `$XMEM_API_KEY` → nearest `.xmemrc.json` (walk up from `--rc-dir` toward `$HOME`). Origin: `--api-url` → `$XMEM_API_URL` → rc `api_url` → default. The `mcp` transport resolves its own origin the same way: `--mcp-url` → `$XMEM_MCP_URL` → rc `mcp_url` → default, where `mcp_url` is what the API told `auth login` about itself.
36
39
 
@@ -48,6 +51,8 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
48
51
  | `xmemcli read --read-mode single` (default) | `mode: "single-answer"` |
49
52
  | `xmemcli read --read-mode raw` | `mode: "raw-tables"` |
50
53
  | `xmemcli read --read-mode xresponse` | `mode: "xresponse"` |
54
+ | `xmemcli read --related-types` | `include_related_types: "types"` — the response's `related_types` object is kept in the curated output |
55
+ | `xmemcli read --related-types-depth 2` | `include_related_types: "types"` and `related_types_depth: 2` — the catalog entries carry their `distance` and, below the last level, their own edges |
51
56
 
52
57
  `--read-mode` selects the **server-side answer shape**; `--json` and the TTY rule govern stdout format.
53
58
 
@@ -55,6 +60,30 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
55
60
 
56
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.
57
62
 
63
+ **`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
+
65
+ ## Trace — the Console's operation traces, from the CLI
66
+
67
+ ```bash
68
+ xmemcli auth status # super_admin: which Console view trace will answer with
69
+ xmemcli org list instances # the instances this key can see — what trace ls is scoped to
70
+ xmemcli trace ls # every visible instance, newest first, one page
71
+ xmemcli trace ls --instance "$ID" --trace-type write --trace-type read --limit 50
72
+ xmemcli trace ls --counts-only --hours-back 168 # counts, per-instance counts and series, no rows
73
+ xmemcli trace ls --search --text "Ada" --instance "$ID" # Inspect search — opt-in, more expensive
74
+ xmemcli trace get 0001_abc --trace-type write # one operation, every internal id
75
+ ```
76
+
77
+ Paging is a cursor the previous page returns:
78
+
79
+ ```bash
80
+ PAGE=$(xmemcli trace ls --instance "$ID" --limit 100)
81
+ NEXT=$(echo "$PAGE" | jq -r '"\(.next_before_ts_ms):\(.next_before_trace_id)"')
82
+ xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
83
+ ```
84
+
85
+ `--search` has no cursor — the Console's Inspect endpoint doesn't page; it returns the newest matches up to a ceiling and the CLI warns when a response sits at it. A key sees the operations of the instances it can list — never more than its owner sees in the Console; a super admin's key sees what a super admin sees. Full reference: [](#trace--the-consoles-operation-traces).
86
+
58
87
  ## Output personality — JSON vs pretty
59
88
 
60
89
  - **Non-TTY stdout** or **`--json`**: exactly one JSON document.
@@ -99,7 +128,7 @@ xmemcli version
99
128
  xmemcli auth login
100
129
  ```
101
130
 
102
- Version: **1.2.0**.
131
+ Version: **1.3.0**.
103
132
 
104
133
 
105
134
  ---
@@ -199,7 +228,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
199
228
 
200
229
  # `xmemcli` — capabilities reference
201
230
 
202
- **Version:** `1.2.0` (see `xmemcli version`)
231
+ **Version:** `1.3.0` (see `xmemcli version`)
203
232
 
204
233
  `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.
205
234
 
@@ -219,7 +248,7 @@ Every command below has dedicated help — see [Help](#help).
219
248
  |---------|-------------|
220
249
  | **`write`** `<text>` | Sync write (`POST …/write`). `--extraction fast\|deep` (default `fast`). `--no-wait` queues async write, returns `write_id`. `--scope TYPE:ID` (repeatable) anchors the write to existing objects: only they may be modified/deleted, new objects and links to them are allowed, anything else fails; fast extraction only. |
221
250
  | **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. |
222
- | **`read`** `<query>` | Read from active instance. `--read-mode single\|raw\|xresponse`, `--scope TYPE:ID` (repeatable), `--scope-relations`. |
251
+ | **`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`). |
223
252
 
224
253
  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)).
225
254
 
@@ -232,7 +261,7 @@ Permission-filtered view of what this API key can see.
232
261
  | Command | Description |
233
262
  |---------|-------------|
234
263
  | **`org list clusters`** | Clusters visible to this credential |
235
- | **`org list instances`** | Instances 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, with a warning saying so — the same set `trace ls` is scoped to. |
236
265
  | **`org list keys`** | API keys visible to this credential (metadata only; plaintext never returned) |
237
266
 
238
267
  ---
@@ -419,27 +448,77 @@ so a typo fails loudly rather than being ignored.
419
448
 
420
449
  ---
421
450
 
451
+ ### Trace — the Console's operation traces
452
+
453
+ What the Console shows on an instance's **Operations** tab (a paged list), on **Inspect** (a
454
+ search across operations), and in each operation's detail pane — fetched from the same Console
455
+ endpoints, with this CLI's API key, and emitted **as the Console serves them**. Nothing is curated
456
+ or reshaped: the Console already curates per caller, and a field the Console adds shows up here
457
+ the same day. These documents are for scripts and agents; on a terminal they are only indented
458
+ and coloured (`NO_COLOR` / `FORCE_COLOR` honoured). Flags only — there is no query language to
459
+ learn; the CLI composes the Console's search clauses itself.
460
+
461
+ | Command | Console view | Description |
462
+ |---------|--------------|-------------|
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`, `--hours-back` (a floor applied to the hits *after* the search — Inspect's own recency windows decide the candidate pool, so this narrows a result, unlike the query window it is on `trace ls`), `--user-id`, `--api-key-id`. Returns the Inspect document: `merged[]` hits with `kind`, `href` (`/write/{trace}`, `/read/{trace}`, `/create/…`, `/update/…`, `/evolution/…`), `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id` / `api_key_name` / `api_key_prefix`, `status`, `time_taken_ms`, `match_items`. |
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**. |
466
+
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 `--limit`, `--cursor`, `--counts-only`, `--start-ts` / `--end-ts`, `--bucket-ms` are refused with `--search` because the Inspect endpoint cannot honour them, and `--include-console-reads` because Inspect's only spelling of that toggle is a bare phrase inside `q` itself — which is also why a free-text value containing the words "include console reads" is refused rather than sent (the Console would flip the toggle and strip the phrase from the query).
468
+
469
+ **The pagination gap — read this before scripting `--search`.** `trace ls` pages properly (the Console's Operations tab does). `trace ls --search` does **not**: the Console's Inspect endpoint has no cursor — it returns the newest 50 matches when no filter narrows it (latest-activity mode) and up to 5 000 matches per source once filtered, and stops; the Console's own Inspect view has the same cliff. So `--search` takes no `--cursor` / `--limit`, and when a response sits at one of those ceilings the CLI emits a **warning** (stderr, and folded into the JSON document's `warnings` array) saying the result was cut and how to narrow it. The warning is **best-effort** — the warning is one-sided by construction: it fires on the count the client can see, and the Console narrows *after* its per-source row caps (type/status resolution, console-read exclusion, the caller's instance allowlist), so a capped response can shrink below the ceiling — even to near-empty for a scoped key searching without an instance filter — with no client-visible signal. A missing warning is not proof of completeness — and a warning is not proof of a cut: the 5 000 figure is *per source*, while the count the warning tests is the cross-source merge (truncated only in latest-activity mode), so several uncapped sources together can trip it on a complete result. Until the Console endpoint pages (and reports truncation itself), treat **every** `--search` result as "the newest matches", not "all matches".
470
+
471
+ **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
+
473
+ **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
+
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}`, and — **unrestricted callers only** (a super admin, or a browser session on a `KINGDOM_LOCAL` Console) — `llm_calls[]` (`llm_call_id`, `gateway_request_id`, `model_type`, `provider`, …), `full_error_message`, raw token counts, unsanitised xuids, `sql_queries`, `ddl_queries`, and `schema_generation`.
476
+
477
+ **A scoped key gets `[]` for `sql_queries` / `ddl_queries` / `schema_generation`, not the rows.** Those three come from `read_sql_queries`, `instance_create_ddl_queries` and `schema_generate_*`, which carry no instance ownership column — `schema_generate_*`'s trace id is client-supplied — so they cannot be scoped in SQL and are served only to callers with no instance restriction at all. This is a deliberate fail-closed choice, not a bug to work around: an empty array means "not available to this credential", never "this operation ran no SQL". `llm_calls[]` behaves the same way and always has. **Xuids are hidden for every non-super-admin**, including on writes that carry no identity map — the substitution is done by the Console, server-side, so a caller with no browser sees `#hidden` rather than a raw id.
478
+
479
+ **Who sees what — the Console decides; `auth status` tells you which view to expect.**
480
+
481
+ - 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
+ - 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
+ - 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`.
484
+ - 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
+
486
+ **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.
487
+
488
+ **Parity.** These commands are the Console's data path, not a copy of it. If the Console gains a filter or a field, it is added to the same list here and is available; the MCP server exposing the same data is deferred and will follow the same rule (see the note in the MCP server source).
489
+
490
+ ---
491
+
422
492
  ### Auth & meta
423
493
 
424
494
  | Command | Description |
425
495
  |---------|-------------|
426
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). |
427
497
  | **`auth logout`** | Remove active `.xmemrc.json` |
428
- | **`auth status`** | Call `/whoami`. No key → exit 0; bad key → auth error; good key → email + prefix. |
498
+ | **`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). |
429
499
  | **`mcp <instance-id>`** | Serve one instance to an MCP client over stdio, using this CLI's credential. Not for humans — see below. |
430
500
  | **`version`** | CLI release version (no network) |
431
501
  | **`status`** | Local health: version + whether `.xmemrc.json` exists, and the `mcp_url` it declares when it declares one (no network) |
432
502
 
433
503
  **`mcp` is a transport, not a command you run.** An MCP client starts it and speaks
434
504
  JSON-RPC to it over stdio; it forwards each frame to `<mcp-url>/instance/<id>` with this
435
- CLI's credential attached, and writes the answer back. Register it with
436
- `claude mcp add <name> -- xmemcli mcp <instance-id>` (or the equivalent for another
437
- client).
505
+ CLI's credential attached, and writes the answer back. The Console renders the
506
+ registration as `claude mcp add <name> -- xmemcli mcp <instance-id>` (with
507
+ `--mcp-url <origin>` appended when the MCP origin is not the client's own default), or the
508
+ equivalent for another client — **replace the bare `xmemcli` with its absolute path, in
509
+ double quotes**. The client stores the command as written and resolves it against its own
510
+ `PATH` every time it starts the server, and that `PATH` need not carry the directory the
511
+ tool was installed into; registered bare, it fails there — Claude Code reports
512
+ `ENOENT: Executable not found in $PATH: "xmemcli"`. `command -v xmemcli` prints the path on
513
+ macOS and Linux, `(Get-Command xmemcli).Source` in PowerShell on Windows. If the bare name
514
+ is registered already, remove that server entry before adding it again: Claude Code refuses
515
+ `mcp add` for a name that already exists rather than replacing it.
438
516
 
439
517
  It exists because an MCP client can reference an environment variable in an
440
518
  `Authorization` header but cannot read `.xmemrc.json`. Reading the credential per
441
- connection means nothing is captured when the server is registered, so a configuration
442
- written today still works in a session opened next week with an empty environment.
519
+ connection means no credential is captured when the server is registered, so a
520
+ configuration written today still works in a session opened next week with an empty
521
+ environment.
443
522
 
444
523
  **Where a file-held key may be sent.** The MCP origin is configured separately from the API
445
524
  origin, and a project `.mcp.json` arrives by `git clone` — it controls that server's `args` and
@@ -504,6 +583,7 @@ Quota **increases** are Console-only — see [What `xmemcli` does not do](#what-
504
583
  | **5** | Timeout |
505
584
  | **6** | Quota exhausted (API quota-exceeded response; `stage: "quota"`) |
506
585
  | **7** | Quota headroom below `--fail-if-under` |
586
+ | **8** | Write still processing — the API's wait window expired while the write kept running (HTTP `200` envelope; `stage: "processing"`). Not a failure: poll `write-status`, never retry |
507
587
  | **130** | Interrupted (Ctrl+C) |
508
588
 
509
589
  ### HTTP status → exit code and JSON `stage`
@@ -538,6 +618,7 @@ condition, and the hook author is the one reading stderr.
538
618
  | **403** | **3** | `permission` | **Bad authorization** — key is valid but this key/role cannot perform the operation |
539
619
  | **Other 4xx/5xx** | **3** | `request` | Request failed for another reason (404, 422, 5xx, …) |
540
620
  | **402** (quota exhausted) | **6** | `quota` | Quota window exhausted — human must extend quota in Console |
621
+ | **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 |
541
622
 
542
623
  #### How to interpret failures
543
624
 
@@ -545,12 +626,16 @@ condition, and the hook author is the one reading stderr.
545
626
  2. **Exit 3 / `stage: "permission"` / HTTP 403** → the key works, but the operation needs a broader API role (for example `data.write`, `instance.generate_schema`, `cluster.get_own`). Do **not** treat this as a wrong password.
546
627
  3. **Exit 3 / `stage: "request"`** → other HTTP failure (instance not found, validation error, server error). Read the error message and HTTP status in the JSON body.
547
628
 
548
- Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, …) while still using exit **3** unless the row above applies. The `schema suggestions` commands add their own stages: `stale_proposal` (re-run review for a fresh `proposal_version`), `invalid_decision`, `dependency` (accept the dangling dependencies first), `not_initialised` (submit a write first), and `apply`; `review` reports an `evolution_in_progress` condition as a successful status with exit 0.
629
+ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not_found` for `trace get`, …) while still using exit **3** unless the row above applies. `write` adds `processing`, which is in the table above and exits **8** rather than **3**. The `schema suggestions` commands add their own stages: `stale_proposal` (re-run review for a fresh `proposal_version`), `invalid_decision`, `dependency` (accept the dangling dependencies first), `not_initialised` (submit a write first), and `apply`; `review` reports an `evolution_in_progress` condition as a successful status with exit 0.
549
630
 
550
- **Envelope errors** (HTTP **200** with non-empty `errors[]`) are not HTTP 401/403; they typically exit **3** with `stage: "request"`.
631
+ **Envelope errors** (HTTP **200** with non-empty `errors[]`) are not HTTP 401/403; they typically exit **3** with `stage: "request"`. Two exceptions. `QUOTA_EXCEEDED` exits **6** with `stage: "quota"`, the same as it does on a real HTTP 402 — the keepalive flattening does not change the exit code — and this applies to every command that classifies the error, not just `write` and `read`. `WRITE_STILL_PROCESSING` exits **8** with `stage: "processing"` and is not a failure at all; unlike the quota rung this one is **`write`-only**, because its guidance is to poll `write-status`, which is meaningless for any other verb — elsewhere it falls through to **3**/`request`.
632
+
633
+ > **Known bug.** Some subcommands in the `instance`, `schema` and `xmd` groups — `instance get`, `schema get` and `xmd generate` among them — unwrap the response outside their error handling, so an envelope error never reaches the classifier and escapes as an uncaught exception: a traceback and shell exit **1**, which is not in the table above. Those report **1**, not the **6** this section describes, until that is fixed. It is not whole groups: `instance instructions`, for example, reaches the classifier and exits **6** as documented.
551
634
 
552
635
  **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.
553
636
 
637
+ **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
+
554
639
  **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.)
555
640
 
556
641
  **New in 1.1.0:** `auth login` records the deployment's own MCP origin in `.xmemrc.json` as `mcp_url` (omitted where the server reports none), and `mcp` both connects there by default and accepts it as the pair its key may reach. Requires 0.0.12 or newer to read such a file — see the note below.
@@ -612,6 +697,9 @@ xmemcli help xmd validate
612
697
  xmemcli help xmd validate-yaml
613
698
  xmemcli help xmd validate-json
614
699
  xmemcli help xmd convert
700
+ xmemcli help trace
701
+ xmemcli help trace ls
702
+ xmemcli help trace get
615
703
  xmemcli help auth login
616
704
  xmemcli help quota
617
705
  xmemcli help options
@@ -629,7 +717,7 @@ Nested topics work at any level that exists in the parser (`help org`, `help org
629
717
  | **Routine API key creation** | Post-onboarding: API Keys UI (CLI opens that flow via `/xmemcli`). |
630
718
  | **Billing & plans** | Console only |
631
719
  | **Users, roles, org admin** | Console only |
632
- | **Instance chat / trace inspection** | Console only |
720
+ | **Instance chat** | Console only (trace inspection is `xmemcli trace …`, see [Trace](#trace--the-consoles-operation-traces)) |
633
721
  | **Cluster create/delete** | CLI lists clusters; provisioning in Console |
634
722
  | **`read --extraction deep`** | Server has no read-depth knob today |
635
723
  | **Raw REST proxy** | Curated response fields only |
@@ -651,6 +739,7 @@ These apply **before** any subcommand (`xmemcli help options`).
651
739
 
652
740
  | `--rc-dir` | `XMEM_RC_DIR` | Where `auth login` writes `.xmemrc.json`; reads walk **up** toward `$HOME` |
653
741
  | `--binding-dir` | `XMEM_BINDING_DIR` | Where `binding` commands look for `.xmemory.json`; reads walk **up** toward `$HOME` and **merge** every scope |
742
+ | `--console-url` (accepted by `trace …` only; `$XMEM_CONSOLE_URL` is read by `auth status` too) | `XMEM_CONSOLE_URL` | Console origin serving the trace views (inferred for the public and local API pairs; required for any other API origin). A named Console that provably does not pair with the API origin is refused; the refusal names the setting that supplied it, since only `trace` accepts the flag |
654
743
  | `--json` | — | Force one JSON document on stdout |
655
744
  | `--verbose` | — | Add `trace_id` / `console_url` when present (never SQL, diff plans, or internals) |
656
745
 
@@ -672,6 +761,8 @@ and each delivery mechanism turned out to be a fresh bypass.
672
761
 
673
762
  **MCP origin resolution (`mcp` only):** `--mcp-url` → `$XMEM_MCP_URL` → the rc's `mcp_url` → default (`https://mcp.xmemory.ai`). The rc rung follows the same escape hatch as the API origin: a key named with `--api-key` / `$XMEM_API_KEY` never inherits an origin from a project file.
674
763
 
764
+ **Request identification (new in 1.3.0):** every outbound request carries `X-Xmemory-Client: xmemcli/<version> (python <version>; <system>-<machine>)`, e.g. `xmemcli/1.3.0 (python 3.12.4; Linux-x86_64)`. Each field is reduced to letters, digits, `.`, `_` and `-`, so nothing an OS or a patched interpreter reports can forge the note's own punctuation or fail the request when it is sent; a field with nothing usable left reads `unknown`. No hostname is included. The server uses this header to attribute traffic to a client. The CLI sets no `User-Agent` of its own, then or now: the installed `urllib` opener supplies its default, so the `User-Agent` the server sees is `Python-urllib/<version>` and the identity comes from the header above. Direct API requests from 1.2.0 and earlier send neither, so they record `client_name: python-urllib` -- indistinguishable from any third-party Python caller. Reading a drop in that bucket as third-party traffic leaving is therefore wrong; it is old CLIs upgrading. Three outcomes are distinct and must not be read as one figure: a recognised header, the `other` bucket a present but unrecognised header lands in, and an absent property, which means neither header named a client. Before 1.3.0 the email sign-in exchange was the only path that named itself, and it sent a bare `xmemcli/<version>` with no platform hint; it now sends the same header and the same value as every other path. What the server records for it still differs, though, and not because of the CLI: the console-facing calls (email sign-in, token redemption, whoami) reach the accounts API through the console, which rebuilds the request on its own HTTP client and forwards only Cookie, Content-Type, Authorization and Host. `X-Xmemory-Client` does not survive that hop, so those land as `console`. The attribution described above is what direct API calls record.
765
+
675
766
  **Instance resolution (read/write):** `--instance-id` → `$XMEM_INSTANCE_ID` → if exactly **one** instance is visible, auto-select with a **stderr warning** → else error.
676
767
 
677
768
  **Output:** Pipes and non-TTY stdout → JSON. Interactive TTY → one-line summaries unless `--json`.
@@ -20,9 +20,12 @@ xmemcli binding list [--autoload]
20
20
  xmemcli binding add <instance_id> [--tier autoload|available|off] [--scope project|user]
21
21
  xmemcli binding remove <instance_id> [--scope project|user]
22
22
  xmemcli context [--text] [--max-tokens N] [--no-live-state]
23
+ 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]
25
+ xmemcli trace get <trace_id> [--trace-type write|read|create|update|evolution]
23
26
  ```
24
27
 
25
- Global flags / env: `--api-key` / `$XMEM_API_KEY`, `--instance-id` / `$XMEM_INSTANCE_ID`, `--api-url` / `$XMEM_API_URL`, `--rc-dir` / `$XMEM_RC_DIR`, `--binding-dir` / `$XMEM_BINDING_DIR`, `--json`, `--verbose`. See `xmemcli help options`.
28
+ Global flags / env: `--api-key` / `$XMEM_API_KEY`, `--instance-id` / `$XMEM_INSTANCE_ID`, `--api-url` / `$XMEM_API_URL`, `--rc-dir` / `$XMEM_RC_DIR`, `--binding-dir` / `$XMEM_BINDING_DIR`, `--json`, `--verbose`. See `xmemcli help options`. `trace …` additionally take `--console-url` / `$XMEM_CONSOLE_URL` (the Console origin that serves them; inferred for the public and local pairs).
26
29
 
27
30
  Credential resolution: `--api-key` → `$XMEM_API_KEY` → nearest `.xmemrc.json` (walk up from `--rc-dir` toward `$HOME`). Origin: `--api-url` → `$XMEM_API_URL` → rc `api_url` → default. The `mcp` transport resolves its own origin the same way: `--mcp-url` → `$XMEM_MCP_URL` → rc `mcp_url` → default, where `mcp_url` is what the API told `auth login` about itself.
28
31
 
@@ -40,6 +43,8 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
40
43
  | `xmemcli read --read-mode single` (default) | `mode: "single-answer"` |
41
44
  | `xmemcli read --read-mode raw` | `mode: "raw-tables"` |
42
45
  | `xmemcli read --read-mode xresponse` | `mode: "xresponse"` |
46
+ | `xmemcli read --related-types` | `include_related_types: "types"` — the response's `related_types` object is kept in the curated output |
47
+ | `xmemcli read --related-types-depth 2` | `include_related_types: "types"` and `related_types_depth: 2` — the catalog entries carry their `distance` and, below the last level, their own edges |
43
48
 
44
49
  `--read-mode` selects the **server-side answer shape**; `--json` and the TTY rule govern stdout format.
45
50
 
@@ -47,6 +52,30 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
47
52
 
48
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.
49
54
 
55
+ **`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
+
57
+ ## Trace — the Console's operation traces, from the CLI
58
+
59
+ ```bash
60
+ xmemcli auth status # super_admin: which Console view trace will answer with
61
+ xmemcli org list instances # the instances this key can see — what trace ls is scoped to
62
+ xmemcli trace ls # every visible instance, newest first, one page
63
+ xmemcli trace ls --instance "$ID" --trace-type write --trace-type read --limit 50
64
+ xmemcli trace ls --counts-only --hours-back 168 # counts, per-instance counts and series, no rows
65
+ xmemcli trace ls --search --text "Ada" --instance "$ID" # Inspect search — opt-in, more expensive
66
+ xmemcli trace get 0001_abc --trace-type write # one operation, every internal id
67
+ ```
68
+
69
+ Paging is a cursor the previous page returns:
70
+
71
+ ```bash
72
+ PAGE=$(xmemcli trace ls --instance "$ID" --limit 100)
73
+ NEXT=$(echo "$PAGE" | jq -r '"\(.next_before_ts_ms):\(.next_before_trace_id)"')
74
+ xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
75
+ ```
76
+
77
+ `--search` has no cursor — the Console's Inspect endpoint doesn't page; it returns the newest matches up to a ceiling and the CLI warns when a response sits at it. A key sees the operations of the instances it can list — never more than its owner sees in the Console; a super admin's key sees what a super admin sees. Full reference: [](#trace--the-consoles-operation-traces).
78
+
50
79
  ## Output personality — JSON vs pretty
51
80
 
52
81
  - **Non-TTY stdout** or **`--json`**: exactly one JSON document.
@@ -91,7 +120,7 @@ xmemcli version
91
120
  xmemcli auth login
92
121
  ```
93
122
 
94
- Version: **1.2.0**.
123
+ Version: **1.3.0**.
95
124
 
96
125
 
97
126
  ---
@@ -191,7 +220,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
191
220
 
192
221
  # `xmemcli` — capabilities reference
193
222
 
194
- **Version:** `1.2.0` (see `xmemcli version`)
223
+ **Version:** `1.3.0` (see `xmemcli version`)
195
224
 
196
225
  `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.
197
226
 
@@ -211,7 +240,7 @@ Every command below has dedicated help — see [Help](#help).
211
240
  |---------|-------------|
212
241
  | **`write`** `<text>` | Sync write (`POST …/write`). `--extraction fast\|deep` (default `fast`). `--no-wait` queues async write, returns `write_id`. `--scope TYPE:ID` (repeatable) anchors the write to existing objects: only they may be modified/deleted, new objects and links to them are allowed, anything else fails; fast extraction only. |
213
242
  | **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. |
214
- | **`read`** `<query>` | Read from active instance. `--read-mode single\|raw\|xresponse`, `--scope TYPE:ID` (repeatable), `--scope-relations`. |
243
+ | **`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`). |
215
244
 
216
245
  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)).
217
246
 
@@ -224,7 +253,7 @@ Permission-filtered view of what this API key can see.
224
253
  | Command | Description |
225
254
  |---------|-------------|
226
255
  | **`org list clusters`** | Clusters visible to this credential |
227
- | **`org list instances`** | Instances 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, with a warning saying so — the same set `trace ls` is scoped to. |
228
257
  | **`org list keys`** | API keys visible to this credential (metadata only; plaintext never returned) |
229
258
 
230
259
  ---
@@ -411,27 +440,77 @@ so a typo fails loudly rather than being ignored.
411
440
 
412
441
  ---
413
442
 
443
+ ### Trace — the Console's operation traces
444
+
445
+ What the Console shows on an instance's **Operations** tab (a paged list), on **Inspect** (a
446
+ search across operations), and in each operation's detail pane — fetched from the same Console
447
+ endpoints, with this CLI's API key, and emitted **as the Console serves them**. Nothing is curated
448
+ or reshaped: the Console already curates per caller, and a field the Console adds shows up here
449
+ the same day. These documents are for scripts and agents; on a terminal they are only indented
450
+ and coloured (`NO_COLOR` / `FORCE_COLOR` honoured). Flags only — there is no query language to
451
+ learn; the CLI composes the Console's search clauses itself.
452
+
453
+ | Command | Console view | Description |
454
+ |---------|--------------|-------------|
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`, `--hours-back` (a floor applied to the hits *after* the search — Inspect's own recency windows decide the candidate pool, so this narrows a result, unlike the query window it is on `trace ls`), `--user-id`, `--api-key-id`. Returns the Inspect document: `merged[]` hits with `kind`, `href` (`/write/{trace}`, `/read/{trace}`, `/create/…`, `/update/…`, `/evolution/…`), `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id` / `api_key_name` / `api_key_prefix`, `status`, `time_taken_ms`, `match_items`. |
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**. |
458
+
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 `--limit`, `--cursor`, `--counts-only`, `--start-ts` / `--end-ts`, `--bucket-ms` are refused with `--search` because the Inspect endpoint cannot honour them, and `--include-console-reads` because Inspect's only spelling of that toggle is a bare phrase inside `q` itself — which is also why a free-text value containing the words "include console reads" is refused rather than sent (the Console would flip the toggle and strip the phrase from the query).
460
+
461
+ **The pagination gap — read this before scripting `--search`.** `trace ls` pages properly (the Console's Operations tab does). `trace ls --search` does **not**: the Console's Inspect endpoint has no cursor — it returns the newest 50 matches when no filter narrows it (latest-activity mode) and up to 5 000 matches per source once filtered, and stops; the Console's own Inspect view has the same cliff. So `--search` takes no `--cursor` / `--limit`, and when a response sits at one of those ceilings the CLI emits a **warning** (stderr, and folded into the JSON document's `warnings` array) saying the result was cut and how to narrow it. The warning is **best-effort** — the warning is one-sided by construction: it fires on the count the client can see, and the Console narrows *after* its per-source row caps (type/status resolution, console-read exclusion, the caller's instance allowlist), so a capped response can shrink below the ceiling — even to near-empty for a scoped key searching without an instance filter — with no client-visible signal. A missing warning is not proof of completeness — and a warning is not proof of a cut: the 5 000 figure is *per source*, while the count the warning tests is the cross-source merge (truncated only in latest-activity mode), so several uncapped sources together can trip it on a complete result. Until the Console endpoint pages (and reports truncation itself), treat **every** `--search` result as "the newest matches", not "all matches".
462
+
463
+ **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
+
465
+ **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
+
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}`, and — **unrestricted callers only** (a super admin, or a browser session on a `KINGDOM_LOCAL` Console) — `llm_calls[]` (`llm_call_id`, `gateway_request_id`, `model_type`, `provider`, …), `full_error_message`, raw token counts, unsanitised xuids, `sql_queries`, `ddl_queries`, and `schema_generation`.
468
+
469
+ **A scoped key gets `[]` for `sql_queries` / `ddl_queries` / `schema_generation`, not the rows.** Those three come from `read_sql_queries`, `instance_create_ddl_queries` and `schema_generate_*`, which carry no instance ownership column — `schema_generate_*`'s trace id is client-supplied — so they cannot be scoped in SQL and are served only to callers with no instance restriction at all. This is a deliberate fail-closed choice, not a bug to work around: an empty array means "not available to this credential", never "this operation ran no SQL". `llm_calls[]` behaves the same way and always has. **Xuids are hidden for every non-super-admin**, including on writes that carry no identity map — the substitution is done by the Console, server-side, so a caller with no browser sees `#hidden` rather than a raw id.
470
+
471
+ **Who sees what — the Console decides; `auth status` tells you which view to expect.**
472
+
473
+ - 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
+ - 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
+ - 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`.
476
+ - 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
+
478
+ **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.
479
+
480
+ **Parity.** These commands are the Console's data path, not a copy of it. If the Console gains a filter or a field, it is added to the same list here and is available; the MCP server exposing the same data is deferred and will follow the same rule (see the note in the MCP server source).
481
+
482
+ ---
483
+
414
484
  ### Auth & meta
415
485
 
416
486
  | Command | Description |
417
487
  |---------|-------------|
418
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). |
419
489
  | **`auth logout`** | Remove active `.xmemrc.json` |
420
- | **`auth status`** | Call `/whoami`. No key → exit 0; bad key → auth error; good key → email + prefix. |
490
+ | **`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). |
421
491
  | **`mcp <instance-id>`** | Serve one instance to an MCP client over stdio, using this CLI's credential. Not for humans — see below. |
422
492
  | **`version`** | CLI release version (no network) |
423
493
  | **`status`** | Local health: version + whether `.xmemrc.json` exists, and the `mcp_url` it declares when it declares one (no network) |
424
494
 
425
495
  **`mcp` is a transport, not a command you run.** An MCP client starts it and speaks
426
496
  JSON-RPC to it over stdio; it forwards each frame to `<mcp-url>/instance/<id>` with this
427
- CLI's credential attached, and writes the answer back. Register it with
428
- `claude mcp add <name> -- xmemcli mcp <instance-id>` (or the equivalent for another
429
- client).
497
+ CLI's credential attached, and writes the answer back. The Console renders the
498
+ registration as `claude mcp add <name> -- xmemcli mcp <instance-id>` (with
499
+ `--mcp-url <origin>` appended when the MCP origin is not the client's own default), or the
500
+ equivalent for another client — **replace the bare `xmemcli` with its absolute path, in
501
+ double quotes**. The client stores the command as written and resolves it against its own
502
+ `PATH` every time it starts the server, and that `PATH` need not carry the directory the
503
+ tool was installed into; registered bare, it fails there — Claude Code reports
504
+ `ENOENT: Executable not found in $PATH: "xmemcli"`. `command -v xmemcli` prints the path on
505
+ macOS and Linux, `(Get-Command xmemcli).Source` in PowerShell on Windows. If the bare name
506
+ is registered already, remove that server entry before adding it again: Claude Code refuses
507
+ `mcp add` for a name that already exists rather than replacing it.
430
508
 
431
509
  It exists because an MCP client can reference an environment variable in an
432
510
  `Authorization` header but cannot read `.xmemrc.json`. Reading the credential per
433
- connection means nothing is captured when the server is registered, so a configuration
434
- written today still works in a session opened next week with an empty environment.
511
+ connection means no credential is captured when the server is registered, so a
512
+ configuration written today still works in a session opened next week with an empty
513
+ environment.
435
514
 
436
515
  **Where a file-held key may be sent.** The MCP origin is configured separately from the API
437
516
  origin, and a project `.mcp.json` arrives by `git clone` — it controls that server's `args` and
@@ -496,6 +575,7 @@ Quota **increases** are Console-only — see [What `xmemcli` does not do](#what-
496
575
  | **5** | Timeout |
497
576
  | **6** | Quota exhausted (API quota-exceeded response; `stage: "quota"`) |
498
577
  | **7** | Quota headroom below `--fail-if-under` |
578
+ | **8** | Write still processing — the API's wait window expired while the write kept running (HTTP `200` envelope; `stage: "processing"`). Not a failure: poll `write-status`, never retry |
499
579
  | **130** | Interrupted (Ctrl+C) |
500
580
 
501
581
  ### HTTP status → exit code and JSON `stage`
@@ -530,6 +610,7 @@ condition, and the hook author is the one reading stderr.
530
610
  | **403** | **3** | `permission` | **Bad authorization** — key is valid but this key/role cannot perform the operation |
531
611
  | **Other 4xx/5xx** | **3** | `request` | Request failed for another reason (404, 422, 5xx, …) |
532
612
  | **402** (quota exhausted) | **6** | `quota` | Quota window exhausted — human must extend quota in Console |
613
+ | **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 |
533
614
 
534
615
  #### How to interpret failures
535
616
 
@@ -537,12 +618,16 @@ condition, and the hook author is the one reading stderr.
537
618
  2. **Exit 3 / `stage: "permission"` / HTTP 403** → the key works, but the operation needs a broader API role (for example `data.write`, `instance.generate_schema`, `cluster.get_own`). Do **not** treat this as a wrong password.
538
619
  3. **Exit 3 / `stage: "request"`** → other HTTP failure (instance not found, validation error, server error). Read the error message and HTTP status in the JSON body.
539
620
 
540
- Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, …) while still using exit **3** unless the row above applies. The `schema suggestions` commands add their own stages: `stale_proposal` (re-run review for a fresh `proposal_version`), `invalid_decision`, `dependency` (accept the dangling dependencies first), `not_initialised` (submit a write first), and `apply`; `review` reports an `evolution_in_progress` condition as a successful status with exit 0.
621
+ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not_found` for `trace get`, …) while still using exit **3** unless the row above applies. `write` adds `processing`, which is in the table above and exits **8** rather than **3**. The `schema suggestions` commands add their own stages: `stale_proposal` (re-run review for a fresh `proposal_version`), `invalid_decision`, `dependency` (accept the dangling dependencies first), `not_initialised` (submit a write first), and `apply`; `review` reports an `evolution_in_progress` condition as a successful status with exit 0.
541
622
 
542
- **Envelope errors** (HTTP **200** with non-empty `errors[]`) are not HTTP 401/403; they typically exit **3** with `stage: "request"`.
623
+ **Envelope errors** (HTTP **200** with non-empty `errors[]`) are not HTTP 401/403; they typically exit **3** with `stage: "request"`. Two exceptions. `QUOTA_EXCEEDED` exits **6** with `stage: "quota"`, the same as it does on a real HTTP 402 — the keepalive flattening does not change the exit code — and this applies to every command that classifies the error, not just `write` and `read`. `WRITE_STILL_PROCESSING` exits **8** with `stage: "processing"` and is not a failure at all; unlike the quota rung this one is **`write`-only**, because its guidance is to poll `write-status`, which is meaningless for any other verb — elsewhere it falls through to **3**/`request`.
624
+
625
+ > **Known bug.** Some subcommands in the `instance`, `schema` and `xmd` groups — `instance get`, `schema get` and `xmd generate` among them — unwrap the response outside their error handling, so an envelope error never reaches the classifier and escapes as an uncaught exception: a traceback and shell exit **1**, which is not in the table above. Those report **1**, not the **6** this section describes, until that is fixed. It is not whole groups: `instance instructions`, for example, reaches the classifier and exits **6** as documented.
543
626
 
544
627
  **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.
545
628
 
629
+ **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
+
546
631
  **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.)
547
632
 
548
633
  **New in 1.1.0:** `auth login` records the deployment's own MCP origin in `.xmemrc.json` as `mcp_url` (omitted where the server reports none), and `mcp` both connects there by default and accepts it as the pair its key may reach. Requires 0.0.12 or newer to read such a file — see the note below.
@@ -604,6 +689,9 @@ xmemcli help xmd validate
604
689
  xmemcli help xmd validate-yaml
605
690
  xmemcli help xmd validate-json
606
691
  xmemcli help xmd convert
692
+ xmemcli help trace
693
+ xmemcli help trace ls
694
+ xmemcli help trace get
607
695
  xmemcli help auth login
608
696
  xmemcli help quota
609
697
  xmemcli help options
@@ -621,7 +709,7 @@ Nested topics work at any level that exists in the parser (`help org`, `help org
621
709
  | **Routine API key creation** | Post-onboarding: API Keys UI (CLI opens that flow via `/xmemcli`). |
622
710
  | **Billing & plans** | Console only |
623
711
  | **Users, roles, org admin** | Console only |
624
- | **Instance chat / trace inspection** | Console only |
712
+ | **Instance chat** | Console only (trace inspection is `xmemcli trace …`, see [Trace](#trace--the-consoles-operation-traces)) |
625
713
  | **Cluster create/delete** | CLI lists clusters; provisioning in Console |
626
714
  | **`read --extraction deep`** | Server has no read-depth knob today |
627
715
  | **Raw REST proxy** | Curated response fields only |
@@ -643,6 +731,7 @@ These apply **before** any subcommand (`xmemcli help options`).
643
731
 
644
732
  | `--rc-dir` | `XMEM_RC_DIR` | Where `auth login` writes `.xmemrc.json`; reads walk **up** toward `$HOME` |
645
733
  | `--binding-dir` | `XMEM_BINDING_DIR` | Where `binding` commands look for `.xmemory.json`; reads walk **up** toward `$HOME` and **merge** every scope |
734
+ | `--console-url` (accepted by `trace …` only; `$XMEM_CONSOLE_URL` is read by `auth status` too) | `XMEM_CONSOLE_URL` | Console origin serving the trace views (inferred for the public and local API pairs; required for any other API origin). A named Console that provably does not pair with the API origin is refused; the refusal names the setting that supplied it, since only `trace` accepts the flag |
646
735
  | `--json` | — | Force one JSON document on stdout |
647
736
  | `--verbose` | — | Add `trace_id` / `console_url` when present (never SQL, diff plans, or internals) |
648
737
 
@@ -664,6 +753,8 @@ and each delivery mechanism turned out to be a fresh bypass.
664
753
 
665
754
  **MCP origin resolution (`mcp` only):** `--mcp-url` → `$XMEM_MCP_URL` → the rc's `mcp_url` → default (`https://mcp.xmemory.ai`). The rc rung follows the same escape hatch as the API origin: a key named with `--api-key` / `$XMEM_API_KEY` never inherits an origin from a project file.
666
755
 
756
+ **Request identification (new in 1.3.0):** every outbound request carries `X-Xmemory-Client: xmemcli/<version> (python <version>; <system>-<machine>)`, e.g. `xmemcli/1.3.0 (python 3.12.4; Linux-x86_64)`. Each field is reduced to letters, digits, `.`, `_` and `-`, so nothing an OS or a patched interpreter reports can forge the note's own punctuation or fail the request when it is sent; a field with nothing usable left reads `unknown`. No hostname is included. The server uses this header to attribute traffic to a client. The CLI sets no `User-Agent` of its own, then or now: the installed `urllib` opener supplies its default, so the `User-Agent` the server sees is `Python-urllib/<version>` and the identity comes from the header above. Direct API requests from 1.2.0 and earlier send neither, so they record `client_name: python-urllib` -- indistinguishable from any third-party Python caller. Reading a drop in that bucket as third-party traffic leaving is therefore wrong; it is old CLIs upgrading. Three outcomes are distinct and must not be read as one figure: a recognised header, the `other` bucket a present but unrecognised header lands in, and an absent property, which means neither header named a client. Before 1.3.0 the email sign-in exchange was the only path that named itself, and it sent a bare `xmemcli/<version>` with no platform hint; it now sends the same header and the same value as every other path. What the server records for it still differs, though, and not because of the CLI: the console-facing calls (email sign-in, token redemption, whoami) reach the accounts API through the console, which rebuilds the request on its own HTTP client and forwards only Cookie, Content-Type, Authorization and Host. `X-Xmemory-Client` does not survive that hop, so those land as `console`. The attribution described above is what direct API calls record.
757
+
667
758
  **Instance resolution (read/write):** `--instance-id` → `$XMEM_INSTANCE_ID` → if exactly **one** instance is visible, auto-select with a **stderr warning** → else error.
668
759
 
669
760
  **Output:** Pipes and non-TTY stdout → JSON. Interactive TTY → one-line summaries unless `--json`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xmemcli"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "Command-line client for the xmemory HTTP API: PKCE auth, instances, read/write, schema and XMD tools."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4"
@@ -4,5 +4,5 @@ requires-python = ">=3.12, <4"
4
4
 
5
5
  [[package]]
6
6
  name = "xmemcli"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  source = { editable = "." }
@@ -1,3 +1,3 @@
1
1
  """Standalone xmemcli package (``xmemcli`` console script)."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.3.0"
@@ -51,7 +51,7 @@ from ._login import (
51
51
  stdin_is_interactive,
52
52
  validate_api_key_shape,
53
53
  )
54
- from ._version import CLI_NAME, __version__
54
+ from ._version import CLI_NAME, CLIENT_HEADER, client_identity
55
55
 
56
56
  # Console-origin endpoints. The five sign-in routes authenticate by cookie proofs alone; the
57
57
  # rest require the minted session cookie, which the opener's in-memory jar replays.
@@ -392,7 +392,7 @@ class EmailSignin:
392
392
  url = f"{self.console}{path}"
393
393
  safe_url = url.split("?", 1)[0]
394
394
  deadline = time.monotonic() + timeout_s
395
- headers = {"Accept": "application/json", "User-Agent": f"{CLI_NAME}/{__version__}"}
395
+ headers = {"Accept": "application/json", CLIENT_HEADER: client_identity()}
396
396
  data = None
397
397
  if body is not None:
398
398
  headers["Content-Type"] = "application/json"
@@ -859,9 +859,7 @@ def _acquire_api_key(
859
859
  raise
860
860
 
861
861
 
862
- def _cancel_and_report(
863
- session: EmailSignin, email: str, *, cross_device: bool, notify: Callable[[str], None]
864
- ) -> None:
862
+ def _cancel_and_report(session: EmailSignin, email: str, *, cross_device: bool, notify: Callable[[str], None]) -> None:
865
863
  """Cancel an aborted attempt and tell the user what that actually achieved.
866
864
 
867
865
  Only ``revoked`` on a cross-device attempt makes the emailed link unapprovable; every other
@@ -883,10 +881,7 @@ def _cancel_and_report(
883
881
  "elsewhere; check the Console if that was not you."
884
882
  )
885
883
  elif outcome == "nothing_to_revoke":
886
- notify(
887
- "Nothing durable was revoked — the emailed link may stay usable until it expires "
888
- "(about five minutes)."
889
- )
884
+ notify("Nothing durable was revoked — the emailed link may stay usable until it expires (about five minutes).")
890
885
  else:
891
886
  notify(
892
887
  "The cancellation could not be confirmed — the emailed link may stay approvable until it "