xmemcli 0.0.13__tar.gz → 1.0.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: 0.0.13
3
+ Version: 1.0.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
@@ -97,7 +97,7 @@ xmemcli version
97
97
  xmemcli auth login
98
98
  ```
99
99
 
100
- Version: **0.0.13**.
100
+ Version: **1.0.0**.
101
101
 
102
102
 
103
103
  ---
@@ -151,7 +151,7 @@ AI-assisted IDE ── xmemcli read/write ──► CLI resolves key; never hand
151
151
  4. With the session, the CLI runs the Console onboarding's core sequence: `GET /api/check-user`; when the address has no account, `GET /api/suggest-slug` (server-derived org name, `--org-name` overrides; ignored for an existing account) and `POST /api/public-signup` — **note that creating a new organisation requires the deployment to allow public registration, which the hosted service currently does not**: an existing account gets a key, a new address gets the server's "organisation creation is not available" refusal (the Console's admin-fallback signup arm is not implemented here); then `GET /api/accounts/users/by-email`, `GET /api/accounts/api-roles` (the seeded default role, or the org's only role — an ambiguous set is refused, never guessed), and `POST /api/accounts/api-keys` with **no cluster list** — the key attaches to every cluster the org has at mint time. Clusters created later are attached by a server-side backfill that repairs only keys already linked to every other cluster in the org; an org with no clusters at mint time gets a key attached to nothing. The key is named **`CLI email login (<hostname>, <timestamp>)`** so every mint stays individually identifiable and revocable on the Console's API Keys page.
152
152
  5. The key is written to `.xmemrc.json` (0600), stdout carries the prefix only, and the session cookie dies with the process.
153
153
 
154
- The wait is bounded by `--timeout` — 600 seconds for this path by default, which is also a **ceiling**: nothing stays claimable past the server's pending window, so larger values are clamped. The emailed one-click link and six-digit code themselves expire at about five minutes; the wait continues past that because an approval given in time stays claimable, and the terminal says when the emailed artifacts have lapsed. The Console origin must be `https` (or a loopback host, for local development). The Console must also match the API origin the key will be used against: with a non-default `--api-url` the CLI only infers a Console where the layout determines one — a single-origin `/xmem` base names its `/console` sibling on the same host, and the local-dev convention pairs `:8000` with `:8002` — otherwise it refuses to guess (pass `--console-url` or set `$XMEM_CONSOLE_URL`). Because that inference exists, a single-origin deployment's rendered sign-in command names the API alone; every other layout still renders the `--api-url`/`--console-url` pair, since naming one without the other is worse than naming neither (`api/agent_setup_renderer.py`), and an explicitly named Console that provably mismatches the API origin (mixed loopback-ness, or a default-production half paired with a non-default one) is refused rather than trusted.
154
+ The wait is bounded by `--timeout` — 600 seconds for this path by default, which is also a **ceiling**: nothing stays claimable past the server's pending window, so larger values are clamped. The emailed one-click link and six-digit code themselves expire at about five minutes; the wait continues past that because an approval given in time stays claimable, and the terminal says when the emailed artifacts have lapsed. The Console origin must be `https` (or a loopback host, for local development). The Console must also match the API origin the key will be used against: with a non-default `--api-url` the CLI only infers a Console where the layout determines one — a single-origin `/xmem` base names its `/console` sibling on the same host, and that sibling beats `$XMEM_CONSOLE_URL` on the same call — and the local-dev convention pairs `:8000` with `:8002` — otherwise it refuses to guess (pass `--console-url` or set `$XMEM_CONSOLE_URL` when inference does not apply). Because that inference exists, a single-origin deployment's rendered sign-in command names the API alone; every other layout still renders the `--api-url`/`--console-url` pair, since naming one without the other is worse than naming neither (`api/agent_setup_renderer.py`), and an explicitly named Console that provably mismatches the API origin (mixed loopback-ness, or a default-production half paired with a non-default one) is refused rather than trusted.
155
155
 
156
156
  When the server offers no matching code on the request reply (cross-device hand-off disabled, no public Console origin configured, or a transient degradation), there is nothing for a headless starter to claim — the emailed link then signs in the *approving* browser — so the CLI falls back to the emailed **six-digit code typed into the terminal** (`POST /api/kingdom-signin-verify`), still a single human action. That fallback needs an interactive stdin; a non-interactive run cancels the attempt it just started (the mode is only known from the server's reply, after the email is on its way) and refuses with instructions — the typed code dies with that cancel, but on such a server the one-click link has nothing durable to revoke and stays redeemable in a browser until it expires (about five minutes), which the terminal states plainly.
157
157
 
@@ -163,7 +163,7 @@ Project-local **`.xmemrc.json`**: `{email, api_url, api_key}`. Found by walking
163
163
 
164
164
  Those three keys are required and must be strings. Anything else in the file is **ignored, not rejected** (since 0.0.12): the rc is the only durable credential store, so a file this CLI cannot load is a CLI that cannot run any command at all — which would make adding a field to the file a breaking change far wider than the feature that added it.
165
165
 
166
- 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. 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 a single-origin API base → default, and its foreign-origin guard accepts the `/xmem` ↔ `/mcp` sibling pair as one deployment.
166
+ 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` → sibling inference from the call's `--api-url` when it applies → `$XMEM_CONSOLE_URL` → sibling inference from `$XMEM_API_URL` → default — 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.
167
167
 
168
168
  ## Identity — `/whoami`
169
169
 
@@ -193,7 +193,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
193
193
 
194
194
  # `xmemcli` — capabilities reference
195
195
 
196
- **Version:** `0.0.13` (see `xmemcli version`)
196
+ **Version:** `1.0.0` (see `xmemcli version`)
197
197
 
198
198
  `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.
199
199
 
@@ -517,9 +517,13 @@ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, …)
517
517
 
518
518
  **Compatibility change in 0.0.12:** an `.xmemrc.json` carrying a key this release does not recognise is now **accepted**, and the unknown key ignored; `email`, `api_url`, and `api_key` are still required and still have to be strings. Until 0.0.12 the key set was closed, which made adding a field to the file a breaking change for **every** command rather than for the one that would read it — a CLI that cannot load the rc cannot resolve a credential for anything. Nothing writes an extra key yet; this release exists so that a later one can.
519
519
 
520
- **Compatibility change in 0.0.13 (single-origin deployments):** an API base URL's path is now **preserved**, never stripped — `--api-url https://host/xmem` builds requests under `/xmem` instead of reducing to the bare origin. Where the base's path is exactly `/xmem`, the CLI infers its single-origin siblings on the same host: `auth login` finds the Console at `/console` without `--console-url`, and `xmemcli mcp` reaches `/mcp` without `--mcp-url` (explicit flags and `$XMEM_CONSOLE_URL` / `$XMEM_MCP_URL` still win, and the MCP origin guard accepts the `/xmem` ↔ `/mcp` pair as one deployment). Bare origins and every other path shape behave exactly as before. See `cli/`.
520
+ **Compatibility change in 0.0.13 (single-origin deployments):** an API base URL's path is now **preserved**, never stripped — `--api-url https://host/xmem` builds requests under `/xmem` instead of reducing to the bare origin. Where the base's path is exactly `/xmem`, the CLI infers its single-origin siblings on the same host: `auth login` finds the Console at `/console` without `--console-url`, and `xmemcli mcp` reaches `/mcp` without `--mcp-url` (explicit `--console-url` / `--mcp-url` and `$XMEM_CONSOLE_URL` / `$XMEM_MCP_URL` still win when inference does not apply; on a call that names `--api-url …/xmem`, sibling inference beats `$XMEM_CONSOLE_URL`; the MCP origin guard accepts the `/xmem` ↔ `/mcp` pair as one deployment). Bare origins and every other path shape behave exactly as before. See `cli/`.
521
521
 
522
- Two consequences worth knowing. A single-origin deployment's own setup instructions rely on this inference — they render `auth login --api-url https://host/xmem` with **no** `--console-url` — so **0.0.13 is the minimum on such a deployment**; an older CLI given that command signs in at the hosted Console instead. And the inference is keyed on the conventional mounts (`/xmem`, `/console`, `/mcp`). A deployment that mounts its services under other prefixes is not inferable: name `--console-url` and `--mcp-url` explicitly there, which is what its own rendered commands do.
522
+ Two consequences worth knowing. Connect instructions from the API assume **1.0.0** or newer everywhere. On a single-origin deployment they also rely on path inference — they render `auth login --api-url https://host/xmem` with **no** `--console-url` — and an older CLI given that command signs in at the hosted Console instead. The inference is keyed on the conventional mounts (`/xmem`, `/console`, `/mcp`). A deployment that mounts its services under other prefixes is not inferable: name `--console-url` and `--mcp-url` explicitly there, which is what its own rendered commands do.
523
+
524
+ **Console instance URLs (1.0.0+):** `xmemcli auth login https://host/console/instance/<uuid>` infers the API base (`/xmem`) and Console (`/console`) from that page URL on a single-origin host — no extra flags or env vars. The Connect tab paste prompt uses this form, and the CLI writes the inferred ``api_url`` into ``.xmemrc.json`` as soon as login starts.
525
+
526
+ **Compatibility change in 1.0.0:** console instance page URLs for login (above), immediate ``api_url`` staging in ``.xmemrc.json``, and **1.0.0** as the minimum version named by connect setup instructions.
523
527
 
524
528
  See also [`ACCOUNTS_AUTH.md`](../docs/auth/ACCOUNTS_AUTH.md) for the server-side 401/403/404 rules.
525
529
 
@@ -89,7 +89,7 @@ xmemcli version
89
89
  xmemcli auth login
90
90
  ```
91
91
 
92
- Version: **0.0.13**.
92
+ Version: **1.0.0**.
93
93
 
94
94
 
95
95
  ---
@@ -143,7 +143,7 @@ AI-assisted IDE ── xmemcli read/write ──► CLI resolves key; never hand
143
143
  4. With the session, the CLI runs the Console onboarding's core sequence: `GET /api/check-user`; when the address has no account, `GET /api/suggest-slug` (server-derived org name, `--org-name` overrides; ignored for an existing account) and `POST /api/public-signup` — **note that creating a new organisation requires the deployment to allow public registration, which the hosted service currently does not**: an existing account gets a key, a new address gets the server's "organisation creation is not available" refusal (the Console's admin-fallback signup arm is not implemented here); then `GET /api/accounts/users/by-email`, `GET /api/accounts/api-roles` (the seeded default role, or the org's only role — an ambiguous set is refused, never guessed), and `POST /api/accounts/api-keys` with **no cluster list** — the key attaches to every cluster the org has at mint time. Clusters created later are attached by a server-side backfill that repairs only keys already linked to every other cluster in the org; an org with no clusters at mint time gets a key attached to nothing. The key is named **`CLI email login (<hostname>, <timestamp>)`** so every mint stays individually identifiable and revocable on the Console's API Keys page.
144
144
  5. The key is written to `.xmemrc.json` (0600), stdout carries the prefix only, and the session cookie dies with the process.
145
145
 
146
- The wait is bounded by `--timeout` — 600 seconds for this path by default, which is also a **ceiling**: nothing stays claimable past the server's pending window, so larger values are clamped. The emailed one-click link and six-digit code themselves expire at about five minutes; the wait continues past that because an approval given in time stays claimable, and the terminal says when the emailed artifacts have lapsed. The Console origin must be `https` (or a loopback host, for local development). The Console must also match the API origin the key will be used against: with a non-default `--api-url` the CLI only infers a Console where the layout determines one — a single-origin `/xmem` base names its `/console` sibling on the same host, and the local-dev convention pairs `:8000` with `:8002` — otherwise it refuses to guess (pass `--console-url` or set `$XMEM_CONSOLE_URL`). Because that inference exists, a single-origin deployment's rendered sign-in command names the API alone; every other layout still renders the `--api-url`/`--console-url` pair, since naming one without the other is worse than naming neither (`api/agent_setup_renderer.py`), and an explicitly named Console that provably mismatches the API origin (mixed loopback-ness, or a default-production half paired with a non-default one) is refused rather than trusted.
146
+ The wait is bounded by `--timeout` — 600 seconds for this path by default, which is also a **ceiling**: nothing stays claimable past the server's pending window, so larger values are clamped. The emailed one-click link and six-digit code themselves expire at about five minutes; the wait continues past that because an approval given in time stays claimable, and the terminal says when the emailed artifacts have lapsed. The Console origin must be `https` (or a loopback host, for local development). The Console must also match the API origin the key will be used against: with a non-default `--api-url` the CLI only infers a Console where the layout determines one — a single-origin `/xmem` base names its `/console` sibling on the same host, and that sibling beats `$XMEM_CONSOLE_URL` on the same call — and the local-dev convention pairs `:8000` with `:8002` — otherwise it refuses to guess (pass `--console-url` or set `$XMEM_CONSOLE_URL` when inference does not apply). Because that inference exists, a single-origin deployment's rendered sign-in command names the API alone; every other layout still renders the `--api-url`/`--console-url` pair, since naming one without the other is worse than naming neither (`api/agent_setup_renderer.py`), and an explicitly named Console that provably mismatches the API origin (mixed loopback-ness, or a default-production half paired with a non-default one) is refused rather than trusted.
147
147
 
148
148
  When the server offers no matching code on the request reply (cross-device hand-off disabled, no public Console origin configured, or a transient degradation), there is nothing for a headless starter to claim — the emailed link then signs in the *approving* browser — so the CLI falls back to the emailed **six-digit code typed into the terminal** (`POST /api/kingdom-signin-verify`), still a single human action. That fallback needs an interactive stdin; a non-interactive run cancels the attempt it just started (the mode is only known from the server's reply, after the email is on its way) and refuses with instructions — the typed code dies with that cancel, but on such a server the one-click link has nothing durable to revoke and stays redeemable in a browser until it expires (about five minutes), which the terminal states plainly.
149
149
 
@@ -155,7 +155,7 @@ Project-local **`.xmemrc.json`**: `{email, api_url, api_key}`. Found by walking
155
155
 
156
156
  Those three keys are required and must be strings. Anything else in the file is **ignored, not rejected** (since 0.0.12): the rc is the only durable credential store, so a file this CLI cannot load is a CLI that cannot run any command at all — which would make adding a field to the file a breaking change far wider than the feature that added it.
157
157
 
158
- 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. 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 a single-origin API base → default, and its foreign-origin guard accepts the `/xmem` ↔ `/mcp` sibling pair as one deployment.
158
+ 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` → sibling inference from the call's `--api-url` when it applies → `$XMEM_CONSOLE_URL` → sibling inference from `$XMEM_API_URL` → default — 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.
159
159
 
160
160
  ## Identity — `/whoami`
161
161
 
@@ -185,7 +185,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
185
185
 
186
186
  # `xmemcli` — capabilities reference
187
187
 
188
- **Version:** `0.0.13` (see `xmemcli version`)
188
+ **Version:** `1.0.0` (see `xmemcli version`)
189
189
 
190
190
  `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.
191
191
 
@@ -509,9 +509,13 @@ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, …)
509
509
 
510
510
  **Compatibility change in 0.0.12:** an `.xmemrc.json` carrying a key this release does not recognise is now **accepted**, and the unknown key ignored; `email`, `api_url`, and `api_key` are still required and still have to be strings. Until 0.0.12 the key set was closed, which made adding a field to the file a breaking change for **every** command rather than for the one that would read it — a CLI that cannot load the rc cannot resolve a credential for anything. Nothing writes an extra key yet; this release exists so that a later one can.
511
511
 
512
- **Compatibility change in 0.0.13 (single-origin deployments):** an API base URL's path is now **preserved**, never stripped — `--api-url https://host/xmem` builds requests under `/xmem` instead of reducing to the bare origin. Where the base's path is exactly `/xmem`, the CLI infers its single-origin siblings on the same host: `auth login` finds the Console at `/console` without `--console-url`, and `xmemcli mcp` reaches `/mcp` without `--mcp-url` (explicit flags and `$XMEM_CONSOLE_URL` / `$XMEM_MCP_URL` still win, and the MCP origin guard accepts the `/xmem` ↔ `/mcp` pair as one deployment). Bare origins and every other path shape behave exactly as before. See `cli/`.
512
+ **Compatibility change in 0.0.13 (single-origin deployments):** an API base URL's path is now **preserved**, never stripped — `--api-url https://host/xmem` builds requests under `/xmem` instead of reducing to the bare origin. Where the base's path is exactly `/xmem`, the CLI infers its single-origin siblings on the same host: `auth login` finds the Console at `/console` without `--console-url`, and `xmemcli mcp` reaches `/mcp` without `--mcp-url` (explicit `--console-url` / `--mcp-url` and `$XMEM_CONSOLE_URL` / `$XMEM_MCP_URL` still win when inference does not apply; on a call that names `--api-url …/xmem`, sibling inference beats `$XMEM_CONSOLE_URL`; the MCP origin guard accepts the `/xmem` ↔ `/mcp` pair as one deployment). Bare origins and every other path shape behave exactly as before. See `cli/`.
513
513
 
514
- Two consequences worth knowing. A single-origin deployment's own setup instructions rely on this inference — they render `auth login --api-url https://host/xmem` with **no** `--console-url` — so **0.0.13 is the minimum on such a deployment**; an older CLI given that command signs in at the hosted Console instead. And the inference is keyed on the conventional mounts (`/xmem`, `/console`, `/mcp`). A deployment that mounts its services under other prefixes is not inferable: name `--console-url` and `--mcp-url` explicitly there, which is what its own rendered commands do.
514
+ Two consequences worth knowing. Connect instructions from the API assume **1.0.0** or newer everywhere. On a single-origin deployment they also rely on path inference — they render `auth login --api-url https://host/xmem` with **no** `--console-url` — and an older CLI given that command signs in at the hosted Console instead. The inference is keyed on the conventional mounts (`/xmem`, `/console`, `/mcp`). A deployment that mounts its services under other prefixes is not inferable: name `--console-url` and `--mcp-url` explicitly there, which is what its own rendered commands do.
515
+
516
+ **Console instance URLs (1.0.0+):** `xmemcli auth login https://host/console/instance/<uuid>` infers the API base (`/xmem`) and Console (`/console`) from that page URL on a single-origin host — no extra flags or env vars. The Connect tab paste prompt uses this form, and the CLI writes the inferred ``api_url`` into ``.xmemrc.json`` as soon as login starts.
517
+
518
+ **Compatibility change in 1.0.0:** console instance page URLs for login (above), immediate ``api_url`` staging in ``.xmemrc.json``, and **1.0.0** as the minimum version named by connect setup instructions.
515
519
 
516
520
  See also [`ACCOUNTS_AUTH.md`](../docs/auth/ACCOUNTS_AUTH.md) for the server-side 401/403/404 rules.
517
521
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xmemcli"
3
- version = "0.0.13"
3
+ version = "1.0.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 = "0.0.13"
7
+ version = "1.0.0"
8
8
  source = { editable = "." }
@@ -1,3 +1,3 @@
1
1
  """Standalone xmemcli package (``xmemcli`` console script)."""
2
2
 
3
- __version__ = "0.0.13"
3
+ __version__ = "1.0.0"
@@ -22,6 +22,7 @@ import errno
22
22
  import hashlib
23
23
  import json
24
24
  import os
25
+ import re
25
26
  import secrets
26
27
  import select
27
28
  import sys
@@ -84,6 +85,55 @@ SINGLE_ORIGIN_API_PATH = "/xmem"
84
85
  SINGLE_ORIGIN_CONSOLE_PATH = "/console"
85
86
  SINGLE_ORIGIN_MCP_PATH = "/mcp"
86
87
 
88
+ _CONSOLE_INSTANCE_PATH_PREFIX = f"{SINGLE_ORIGIN_CONSOLE_PATH}/instance/"
89
+ _INSTANCE_ID_RE = re.compile(
90
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
91
+ re.IGNORECASE,
92
+ )
93
+
94
+
95
+ class ConsoleInstanceLink(NamedTuple):
96
+ """A Console instance detail page on a single-origin deployment."""
97
+
98
+ console_url: str
99
+ api_url: str
100
+ instance_id: str
101
+
102
+
103
+ def parse_console_instance_url(url: str) -> ConsoleInstanceLink | None:
104
+ """Derive login targets from ``…/console/instance/<uuid>`` on one hostname.
105
+
106
+ Query and fragment are ignored. Only the conventional single-origin mount
107
+ ``/console/instance/<uuid>`` is recognised — a five-host Console URL must
108
+ still name ``--api-url`` and ``--console-url`` explicitly.
109
+ """
110
+ raw = url.strip()
111
+ if not raw:
112
+ return None
113
+ try:
114
+ parsed = urlsplit(raw)
115
+ _ = parsed.port
116
+ except ValueError:
117
+ return None
118
+ if parsed.scheme not in ("http", "https") or not parsed.hostname:
119
+ return None
120
+ if parsed.username or parsed.password:
121
+ return None
122
+ path = parsed.path.rstrip("/")
123
+ if not path.startswith(_CONSOLE_INSTANCE_PATH_PREFIX):
124
+ return None
125
+ instance_id = path[len(_CONSOLE_INSTANCE_PATH_PREFIX) :]
126
+ if not instance_id or "/" in instance_id:
127
+ return None
128
+ if _INSTANCE_ID_RE.fullmatch(instance_id) is None:
129
+ return None
130
+ origin = f"{parsed.scheme}://{parsed.netloc}"
131
+ return ConsoleInstanceLink(
132
+ console_url=f"{origin}{SINGLE_ORIGIN_CONSOLE_PATH}",
133
+ api_url=f"{origin}{SINGLE_ORIGIN_API_PATH}",
134
+ instance_id=instance_id,
135
+ )
136
+
87
137
  API_KEY_PREFIX = "xmem_"
88
138
 
89
139
  #: Hostnames treated as loopback wherever the CLI relaxes an https-only rule or infers the
@@ -192,22 +242,30 @@ def console_url_from_api_url(api_url: str) -> str | None:
192
242
  def console_url_for_api(*, console_url: str | None = None, api_url: str | None = None) -> str:
193
243
  """Resolve the Console origin for PKCE login and token/whoami proxies.
194
244
 
195
- Explicit wins (flag, then ``$XMEM_CONSOLE_URL``); then inference from the API base the
196
- caller is actually signing in against, then from ``$XMEM_API_URL``; then the default.
197
- ``api_url`` outranks the environment because it is the resolved target of this very call
198
- — an exported ``$XMEM_API_URL`` must not steer the Console away from an explicit
199
- ``--api-url``.
245
+ Explicit ``--console-url`` wins. When the caller names an API base for this login,
246
+ that base alone decides whether a Console can be inferred — ``$XMEM_CONSOLE_URL`` and
247
+ ``$XMEM_API_URL`` are not consulted on that path, so a copy-pasted
248
+ ``--api-url https://host/xmem`` cannot be steered away by the reader's shell profile.
249
+ With no ``api_url``, the environment and then the default apply as before.
200
250
  """
201
251
  if console_url:
202
252
  return console_url.rstrip("/")
253
+ if api_url:
254
+ inferred = console_url_from_api_url(api_url)
255
+ if inferred:
256
+ return inferred
257
+ from_env = os.environ.get(_ENV_CONSOLE_URL, "").strip()
258
+ if from_env:
259
+ return from_env.rstrip("/")
260
+ return DEFAULT_CONSOLE_URL
203
261
  from_env = os.environ.get(_ENV_CONSOLE_URL, "").strip()
204
262
  if from_env:
205
263
  return from_env.rstrip("/")
206
- for candidate in (api_url, os.environ.get(_ENV_API_URL, "").strip()):
207
- if candidate:
208
- inferred = console_url_from_api_url(candidate)
209
- if inferred:
210
- return inferred
264
+ env_api = os.environ.get(_ENV_API_URL, "").strip()
265
+ if env_api:
266
+ inferred = console_url_from_api_url(env_api)
267
+ if inferred:
268
+ return inferred
211
269
  return DEFAULT_CONSOLE_URL
212
270
 
213
271
 
@@ -29,6 +29,7 @@ import argparse
29
29
  import io
30
30
  import ipaddress
31
31
  import json
32
+ import os
32
33
  import sys
33
34
  import urllib.parse
34
35
  import urllib.error
@@ -556,7 +557,13 @@ def _is_single_origin_pair(api_origin: str, mcp_origin: str, m: Any) -> bool:
556
557
  return _canonical_origin(expected, m) == _canonical_origin(mcp_origin, m)
557
558
 
558
559
 
559
- def _refuse_foreign_origin(api_origin: str | None, mcp_origin: str, m: Any) -> str | None:
560
+ def _refuse_foreign_origin(
561
+ api_origin: str | None,
562
+ mcp_origin: str,
563
+ m: Any,
564
+ *,
565
+ effective_api_base: str | None = None,
566
+ ) -> str | None:
560
567
  """Why this credential may not be sent to this MCP origin, or ``None``.
561
568
 
562
569
  The key in ``.xmemrc.json`` records the API origin it belongs to, and the CLI holds
@@ -566,6 +573,9 @@ def _refuse_foreign_origin(api_origin: str | None, mcp_origin: str, m: Any) -> s
566
573
  user did not write — so the config is not allowed to vouch for itself.
567
574
 
568
575
  ``api_origin`` is ``None`` when the operator named the credential on this call.
576
+ ``effective_api_base`` is the flag/env/rc-resolved API base used to infer MCP when
577
+ no explicit MCP was named — an rc that recorded the bare host while ``--api-url
578
+ …/xmem`` names the mount for this call still pairs with the ``/mcp`` sibling.
569
579
  """
570
580
  if api_origin is None:
571
581
  # Named on the command line or in the environment: the operator supplied this
@@ -578,6 +588,13 @@ def _refuse_foreign_origin(api_origin: str | None, mcp_origin: str, m: Any) -> s
578
588
  return None
579
589
  if _is_single_origin_pair(api_origin, mcp_origin, m):
580
590
  return None
591
+ if (
592
+ effective_api_base
593
+ and effective_api_base != api_origin
594
+ and _is_single_origin_pair(effective_api_base, mcp_origin, m)
595
+ and _canonical_origin(effective_api_base, m) == api
596
+ ):
597
+ return None
581
598
  # The originals, not the canonical forms: normalising rebuilds an origin as
582
599
  # `scheme://host:port`, which for IPv6 drops the brackets that made the address
583
600
  # parseable — so `http://[::1]:8000` canonicalises to something no URL parser can
@@ -633,13 +650,17 @@ def cmd_mcp(args: argparse.Namespace) -> int:
633
650
  )
634
651
  _eprint(unavailable)
635
652
 
636
- # Resolved once, with the API base as inference context: on a single-origin
637
- # deployment (an rc minted against `https://host/xmem`, or that base named on the
638
- # call) the MCP endpoint is the `/mcp` sibling of the same host, and no flag or
639
- # variable should be needed to say so. `_explicit_api_url` is the rc-free reading
640
- # on purpose — a malformed rc already produced `unavailable` above and must not
641
- # take this resolution down with it.
642
- mcp_origin = m._mcp_url(args, api_url=api_origin or m._explicit_api_url(args))
653
+ # Explicit MCP wins without parsing an API base at all — a malformed `--api-url` must
654
+ # not take down a valid `--mcp-url`. Otherwise the effective API base for sibling
655
+ # inference follows flag → env → rc precedence (`_api_url`), not the credential-bound
656
+ # origin alone: an rc at the bare host plus `--api-url …/xmem` must still reach `/mcp`.
657
+ explicit_mcp = (getattr(args, "mcp_url", None) or os.environ.get(m.ENV_MCP_URL) or "").strip()
658
+ if explicit_mcp:
659
+ mcp_origin = explicit_mcp
660
+ effective_api_base: str | None = None
661
+ else:
662
+ effective_api_base = m._api_url(args)
663
+ mcp_origin = m._mcp_url(args, api_url=effective_api_base)
643
664
 
644
665
  # Is it a URL at all — asked first, and for every caller. The guard below is skipped
645
666
  # when the operator names the key, and that used to skip the only look this origin
@@ -656,7 +677,9 @@ def cmd_mcp(args: argparse.Namespace) -> int:
656
677
  # anywhere else needs a credential the operator named on the call. `api_origin` comes
657
678
  # from the same read as `api_key`, so the two always describe one file.
658
679
  if api_key is not None and unavailable is None:
659
- unavailable = _refuse_foreign_origin(api_origin, mcp_origin, m)
680
+ unavailable = _refuse_foreign_origin(
681
+ api_origin, mcp_origin, m, effective_api_base=effective_api_base
682
+ )
660
683
  if unavailable is not None:
661
684
  _eprint(unavailable)
662
685
 
@@ -153,6 +153,24 @@ def write_rc(target_dir: Path, *, email: str, api_url: str, api_key: str) -> Pat
153
153
  return path
154
154
 
155
155
 
156
+ def stage_login_api_url(start_dir: Path, api_url: str) -> Path:
157
+ """Record ``api_url`` in the project rc before PKCE or email approval finishes.
158
+
159
+ When a rc already exists, its ``api_url`` is updated in place and the credential
160
+ is left untouched. Otherwise a placeholder rc is written (empty email/key) so
161
+ ``_api_url`` names this deployment while login is still in progress.
162
+ """
163
+ start_dir.mkdir(parents=True, exist_ok=True)
164
+ found = find_rc(start_dir)
165
+ if found is not None:
166
+ try:
167
+ record = load_rc(found)
168
+ except RcError:
169
+ return write_rc(start_dir, email="", api_url=api_url, api_key="")
170
+ return write_rc(found.parent, email=record.email, api_url=api_url, api_key=record.api_key)
171
+ return write_rc(start_dir, email="", api_url=api_url, api_key="")
172
+
173
+
156
174
  def remove_rc(path: Path) -> bool:
157
175
  """Delete the ``.xmemrc.json`` at ``path``. Returns ``True`` iff it existed."""
158
176
  try:
@@ -1,4 +1,4 @@
1
1
  """Release metadata for the xmemcli CLI (copied into standalone builds)."""
2
2
 
3
3
  CLI_NAME = "xmemcli"
4
- __version__ = "0.0.13"
4
+ __version__ = "1.0.0"
@@ -141,6 +141,7 @@ from ._rc import ( # noqa: E402 (sibling-script import)
141
141
  load_rc,
142
142
  remove_rc,
143
143
  resolve_rc,
144
+ stage_login_api_url,
144
145
  write_rc,
145
146
  )
146
147
  from ._text import oneline
@@ -653,8 +654,68 @@ def _get(url: str, api_key: str, timeout_s: float = 30.0) -> dict[str, Any]:
653
654
  return _read_json_response(_build_request(url, "GET", api_key), timeout_s)
654
655
 
655
656
 
657
+ def _connect_url_from_login_args(args: argparse.Namespace) -> str | None:
658
+ """Console instance page URL from a positional arg or a mistaken ``--api-url``."""
659
+ connect = getattr(args, "connect_url", None)
660
+ if isinstance(connect, str) and connect.strip():
661
+ return connect.strip()
662
+ flag_api = getattr(args, "api_url", None)
663
+ if isinstance(flag_api, str) and flag_api.strip():
664
+ candidate = flag_api.strip()
665
+ if _login.parse_console_instance_url(candidate) is not None:
666
+ return candidate
667
+ return None
668
+
669
+
670
+ def _auth_login_api_base(
671
+ args: argparse.Namespace,
672
+ *,
673
+ rc_origin: str | None = None,
674
+ ) -> tuple[str, str | None, tuple[CliOutput, int] | None]:
675
+ """Resolve the API base (and optional instance id) for ``auth login``."""
676
+ connect = _connect_url_from_login_args(args)
677
+ if connect:
678
+ link = _login.parse_console_instance_url(connect)
679
+ if link is None:
680
+ return (
681
+ "",
682
+ None,
683
+ (
684
+ make_error(
685
+ f"not a single-origin Console instance URL ({connect!r}); expected "
686
+ f"…{_login.SINGLE_ORIGIN_CONSOLE_PATH}/instance/<uuid>",
687
+ stage="usage",
688
+ ),
689
+ EXIT_USAGE,
690
+ ),
691
+ )
692
+ return link.api_url, link.instance_id, None
693
+ explicit_api = _explicit_api_url(args) or os.environ.get(ENV_API_URL, "").strip()
694
+ api_origin = explicit_api or rc_origin or DEFAULT_API_URL
695
+ base = _normalize_api_base_url(api_origin) or api_origin.rstrip("/")
696
+ return base, None, None
697
+
698
+
699
+ def _stage_login_api_url_if_connecting(args: argparse.Namespace, api_url: str) -> None:
700
+ """When login was aimed by a Console instance page URL, persist ``api_url`` immediately."""
701
+ if _connect_url_from_login_args(args) is None:
702
+ return
703
+ stage_login_api_url(_rc_dir(args), api_url)
704
+
705
+
706
+ def _console_url_flag_for_args(args: argparse.Namespace) -> str | None:
707
+ """``--console-url`` only — not ``$XMEM_CONSOLE_URL``.
708
+
709
+ ``console_url_for_api`` applies the environment after sibling inference from
710
+ ``--api-url``; pre-merging the env here would defeat that ordering on a
711
+ copy-pasted single-origin login command.
712
+ """
713
+ explicit = getattr(args, "console_url", None)
714
+ return explicit.strip() if isinstance(explicit, str) and explicit.strip() else None
715
+
716
+
656
717
  def _console_url_for_args(args: argparse.Namespace) -> str | None:
657
- explicit = getattr(args, "console_url", None) or os.environ.get(ENV_CONSOLE_URL)
718
+ explicit = _console_url_flag_for_args(args) or os.environ.get(ENV_CONSOLE_URL)
658
719
  return explicit.strip() if isinstance(explicit, str) and explicit.strip() else None
659
720
 
660
721
 
@@ -888,12 +949,20 @@ def _normalize_api_base_url(url: str) -> str | None:
888
949
  ``_normalize_api_origin`` exists for origin comparison and deliberately drops the path;
889
950
  this form exists for building requests, where a single-origin deployment's ``/xmem``
890
951
  mount is load-bearing — stripping it would aim every call at the Console's root instead
891
- of the API. Query and fragment are still dropped: a base URL carries neither.
952
+ of the API. Query, fragment, and userinfo refuse here rather than being stripped: the
953
+ single-origin sibling helpers treat those components as making a URL non-inferable, and
954
+ normalizing them away would let inference run on a shape the caller never supplied.
892
955
  """
893
- origin = _normalize_api_origin(url)
956
+ raw = url.strip()
957
+ if not raw:
958
+ return None
959
+ parsed = urlsplit(raw)
960
+ if parsed.username or parsed.password or parsed.query or parsed.fragment:
961
+ return None
962
+ origin = _normalize_api_origin(raw)
894
963
  if origin is None:
895
964
  return None
896
- path = urlsplit(url.strip()).path.rstrip("/")
965
+ path = parsed.path.rstrip("/")
897
966
  return f"{origin}{path}" if path else origin
898
967
 
899
968
 
@@ -1495,11 +1564,12 @@ def _cmd_auth_login_email(args: argparse.Namespace) -> tuple[CliOutput, int]:
1495
1564
  EXIT_HTTP_ERROR,
1496
1565
  )
1497
1566
 
1498
- explicit_api = _explicit_api_url(args)
1499
1567
  rc_origin = rc.api_url.strip() if rc is not None and rc.api_url.strip() else None
1500
- api_origin = explicit_api or rc_origin or DEFAULT_API_URL
1501
- base = _normalize_api_base_url(api_origin) or api_origin.rstrip("/")
1502
- console, refusal = _email_console_origin(args, api_origin=api_origin)
1568
+ base, instance_hint, usage_error = _auth_login_api_base(args, rc_origin=rc_origin)
1569
+ if usage_error is not None:
1570
+ return usage_error
1571
+ _stage_login_api_url_if_connecting(args, base)
1572
+ console, refusal = _email_console_origin(args, api_origin=base)
1503
1573
  if refusal is not None:
1504
1574
  return refusal
1505
1575
  if console is None:
@@ -1711,6 +1781,8 @@ def _cmd_auth_login_email(args: argparse.Namespace) -> tuple[CliOutput, int]:
1711
1781
  }
1712
1782
  if result.org_name:
1713
1783
  payload["org_name"] = result.org_name
1784
+ if instance_hint:
1785
+ payload["instance_id"] = instance_hint
1714
1786
  return (CliOutput(OutputKind.LOGIN, payload), EXIT_OK)
1715
1787
 
1716
1788
 
@@ -1731,10 +1803,11 @@ def cmd_auth_login(args: argparse.Namespace) -> tuple[CliOutput, int]:
1731
1803
  make_error("--org-name requires --email (it names a new account's organisation)", stage="usage"),
1732
1804
  EXIT_USAGE,
1733
1805
  )
1734
- explicit_api = _explicit_api_url(args) or os.environ.get(ENV_API_URL, "").strip()
1735
- console = _console_url_for_args(args)
1736
- api_origin = explicit_api or DEFAULT_API_URL
1737
- base = _normalize_api_base_url(api_origin) or api_origin.rstrip("/")
1806
+ base, instance_hint, usage_error = _auth_login_api_base(args)
1807
+ if usage_error is not None:
1808
+ return usage_error
1809
+ _stage_login_api_url_if_connecting(args, base)
1810
+ console = _console_url_flag_for_args(args)
1738
1811
  verifier = _login.new_verifier()
1739
1812
  interactive = stdout_is_tty()
1740
1813
  offer_paste = interactive and _login.paste_from_stdin_supported()
@@ -1780,6 +1853,8 @@ def cmd_auth_login(args: argparse.Namespace) -> tuple[CliOutput, int]:
1780
1853
  "key_prefix": result.key_prefix,
1781
1854
  "rc_file": str(rc_path),
1782
1855
  }
1856
+ if instance_hint:
1857
+ payload["instance_id"] = instance_hint
1783
1858
  return (CliOutput(OutputKind.LOGIN, payload), EXIT_OK)
1784
1859
 
1785
1860
 
@@ -2671,6 +2746,14 @@ def _build_parser() -> argparse.ArgumentParser:
2671
2746
  "--org-name",
2672
2747
  help="Organisation name when --email creates a new account (default: derived from the address)",
2673
2748
  )
2749
+ pal.add_argument(
2750
+ "connect_url",
2751
+ nargs="?",
2752
+ help=(
2753
+ "Console instance page URL (…/console/instance/<uuid> on a single-origin host); "
2754
+ "the API and Console origins are inferred from it"
2755
+ ),
2756
+ )
2674
2757
  pal.set_defaults(func=cmd_auth_login)
2675
2758
  palo = pa_sub.add_parser("logout", help="Remove the credential from the active .xmemrc.json", add_help=False)
2676
2759
  palo.set_defaults(func=cmd_auth_logout)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes