xmemcli 0.0.3__tar.gz → 0.0.4__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.4
2
2
  Name: xmemcli
3
- Version: 0.0.3
3
+ Version: 0.0.4
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
@@ -70,9 +70,15 @@ xmemcli write-status "$ID1" "$ID2"
70
70
 
71
71
  Requires **Python 3.12+**. Stdlib only — no runtime dependencies.
72
72
 
73
+ Preferred:
74
+
73
75
  ```bash
74
76
  uv tool install xmemcli
75
- # or
77
+ ```
78
+
79
+ When uv is not available:
80
+
81
+ ```bash
76
82
  pip install xmemcli
77
83
  ```
78
84
 
@@ -83,7 +89,7 @@ xmemcli version
83
89
  xmemcli auth login
84
90
  ```
85
91
 
86
- Version: **0.0.3**.
92
+ Version: **0.0.4**.
87
93
 
88
94
 
89
95
  ---
@@ -153,7 +159,7 @@ Fixed port **9636** ("xmem" on phone keypad) allows SSH/devcontainer forwarding;
153
159
 
154
160
  # `xmemcli` — capabilities reference
155
161
 
156
- **Version:** `0.0.3` (see `xmemcli version`)
162
+ **Version:** `0.0.4` (see `xmemcli version`)
157
163
 
158
164
  `xmemcli` is the command-line client for the **xmemory HTTP API**. It is **human-governed for key acquisition**; an AI-assisted IDE may run reads, writes, and inventory once credentials exist. Humans acquire API keys and change quota limits in the **Console UI**.
159
165
 
@@ -212,7 +218,7 @@ XMD (xmemory Data) is the typed schema format for instances. These commands mirr
212
218
  | Apply migration | **`schema update`** `[instance_id]` | `--schema-file`, optional `--migration-plan`, `--confirm-destructive`. |
213
219
  | Preview migration | **`schema dry-run`** `[instance_id]` | Same flags as `schema update`; returns planned DDL without applying. |
214
220
  | List migrations | **`schema migrations list`** `[instance_id]` | Newest first. `--limit`, `--before-id`, `--include-yaml`. |
215
- | Fetch one migration | **`schema migrations get`** `<migration_id>` | Instance from `--instance-id` / `$XMEM_INSTANCE_ID`. `--include-yaml`. |
221
+ | Fetch one migration | **`schema migrations get`** `[instance_id]` `<migration_id>` | Optional positional instance id; else `--instance-id` / `$XMEM_INSTANCE_ID`. `--include-yaml`. |
216
222
  | Enhance bound schema | **`xmd enhance --from-instance`** `<description>` | Uses live schema; requires instance id. |
217
223
  | Synthesize schema | **`xmd generate`** `<description>` | Cluster-scoped LLM synthesis. `-o` writes YAML. |
218
224
  | Enhance from file | **`xmd enhance`** `[schema_file]` `<description>` | From a file, or `--from-instance` for bound enhance. `-o`, `--plan-output`. |
@@ -239,7 +245,7 @@ xmemcli schema update INST_ID --schema-file next.yml --migration-plan plan.json
239
245
 
240
246
  # 4. Inspect history
241
247
  xmemcli schema migrations list INST_ID
242
- xmemcli schema migrations get MIGRATION_UUID
248
+ xmemcli schema migrations get INST_ID MIGRATION_UUID
243
249
  ```
244
250
 
245
251
  #### Commands
@@ -250,7 +256,7 @@ xmemcli schema migrations get MIGRATION_UUID
250
256
  | **`schema update`** `[instance_id]` | Replace schema from `--schema-file`. Optional `--schema-type`, `--migration-plan`, `--confirm-destructive`. |
251
257
  | **`schema dry-run`** `[instance_id]` | Preview migration (same schema/plan flags as update). |
252
258
  | **`schema migrations list`** `[instance_id]` | Applied migrations, newest first. `--limit` (default 50), `--before-id`, `--include-yaml`. |
253
- | **`schema migrations get`** `<migration_id>` | One record; instance from global instance flags. `--include-yaml`. |
259
+ | **`schema migrations get`** `[instance_id]` `<migration_id>` | One record. Optional positional instance id; else global instance flags. `--include-yaml`. |
254
260
  | **`xmd generate`** `<description>` | LLM-synthesize XMD. Optional `-o`. Requires cluster resolution. |
255
261
  | **`xmd enhance`** `[schema_file]` `<description>` | LLM-augment schema from file, or **`--from-instance`** for live schema. `-o`, `--plan-output`. |
256
262
  | **`xmd validate`** `<schema_file>` | Validate; YAML vs JSON inferred from `.json` extension. |
@@ -258,7 +264,7 @@ xmemcli schema migrations get MIGRATION_UUID
258
264
  | **`xmd validate-json`** `<schema_file>` | Force JSON Schema validation endpoint. |
259
265
  | **`xmd convert`** `<json_schema_file>` | JSON Schema → XMD YAML via the API (`validate_json_schema`). Optional `-o`, `--root-object`. |
260
266
 
261
- Instance id for `schema *` commands: positional `[instance_id]`, else **`--instance-id`** / **`$XMEM_INSTANCE_ID`** (except `migrations get`, which takes only `migration_id`).
267
+ Instance id for `schema *` commands: positional `[instance_id]`, else **`--instance-id`** / **`$XMEM_INSTANCE_ID`**.
262
268
 
263
269
  ---
264
270
 
@@ -286,10 +292,23 @@ Instance id for `schema *` commands: positional `[instance_id]`, else **`--insta
286
292
  | **`quota`** | Usage / limit / window start for the key's cluster (derived from instance → cluster). |
287
293
 
288
294
  - **Default:** exit `0` when quota is readable; stderr **warning** when usage is high.
289
- - **`--fail-if-under THRESHOLD`** — exit non-zero when **remaining** quota is below that share of the limit (`0.1` or `10%`). See `xmemcli help quota`.
295
+ - **`--fail-if-under THRESHOLD`** — exit **7** when **remaining** quota is below that share of the limit (`0.1` or `10%`). Distinct from exit **6** (API quota exhausted). See `xmemcli help quota`.
290
296
 
291
297
  Quota **increases** are Console-only — see [What `xmemcli` does not do](#what-xmemcli-does-not-do).
292
298
 
299
+ ### Exit codes
300
+
301
+ | Code | Meaning |
302
+ |------|---------|
303
+ | **0** | Success |
304
+ | **2** | Usage error (bad flags, unknown `help` topic, missing required args) |
305
+ | **3** | HTTP / request failure (includes unwritable `-o` paths) |
306
+ | **4** | Auth error (missing or rejected API key) |
307
+ | **5** | Timeout |
308
+ | **6** | Quota exhausted (API quota-exceeded response) |
309
+ | **7** | Quota headroom below `--fail-if-under` |
310
+ | **130** | Interrupted (Ctrl+C) |
311
+
293
312
  ---
294
313
 
295
314
  ## Help
@@ -304,6 +323,8 @@ Quota **increases** are Console-only — see [What `xmemcli` does not do](#what-
304
323
  | **`xmemcli help <topic>`** | Usage, options, and subcommands for that topic |
305
324
  | **`xmemcli <cmd> … -h`** / **`--help`** | Same as `help <topic>` for that command path |
306
325
 
326
+ Unknown help topics exit **2** (same as other usage errors).
327
+
307
328
  **Every command in this document has help.** Examples:
308
329
 
309
330
  ```text
@@ -348,7 +369,7 @@ Nested topics work at any level that exists in the parser (`help org`, `help org
348
369
  | **Cluster create/delete** | CLI lists clusters; provisioning in Console |
349
370
  | **`read --extraction deep`** | Server has no read-depth knob today |
350
371
  | **Raw REST proxy** | Curated response fields only |
351
- | **Instance deletion** | **Not available in the CLI** — explicitly disabled and reserved for manual human use in the Console. |
372
+ | **Instance deletion** | **Human operators only** — a hidden `instance delete` exists for interactive TTY use (type `yes` to confirm). It is omitted from help and must not be used by agents or automation. Console UI remains the primary human path. |
352
373
 
353
374
  On HTTP **402** (quota exhausted), stop and ask a human to extend quota in the Console.
354
375
 
@@ -62,9 +62,15 @@ xmemcli write-status "$ID1" "$ID2"
62
62
 
63
63
  Requires **Python 3.12+**. Stdlib only — no runtime dependencies.
64
64
 
65
+ Preferred:
66
+
65
67
  ```bash
66
68
  uv tool install xmemcli
67
- # or
69
+ ```
70
+
71
+ When uv is not available:
72
+
73
+ ```bash
68
74
  pip install xmemcli
69
75
  ```
70
76
 
@@ -75,7 +81,7 @@ xmemcli version
75
81
  xmemcli auth login
76
82
  ```
77
83
 
78
- Version: **0.0.3**.
84
+ Version: **0.0.4**.
79
85
 
80
86
 
81
87
  ---
@@ -145,7 +151,7 @@ Fixed port **9636** ("xmem" on phone keypad) allows SSH/devcontainer forwarding;
145
151
 
146
152
  # `xmemcli` — capabilities reference
147
153
 
148
- **Version:** `0.0.3` (see `xmemcli version`)
154
+ **Version:** `0.0.4` (see `xmemcli version`)
149
155
 
150
156
  `xmemcli` is the command-line client for the **xmemory HTTP API**. It is **human-governed for key acquisition**; an AI-assisted IDE may run reads, writes, and inventory once credentials exist. Humans acquire API keys and change quota limits in the **Console UI**.
151
157
 
@@ -204,7 +210,7 @@ XMD (xmemory Data) is the typed schema format for instances. These commands mirr
204
210
  | Apply migration | **`schema update`** `[instance_id]` | `--schema-file`, optional `--migration-plan`, `--confirm-destructive`. |
205
211
  | Preview migration | **`schema dry-run`** `[instance_id]` | Same flags as `schema update`; returns planned DDL without applying. |
206
212
  | List migrations | **`schema migrations list`** `[instance_id]` | Newest first. `--limit`, `--before-id`, `--include-yaml`. |
207
- | Fetch one migration | **`schema migrations get`** `<migration_id>` | Instance from `--instance-id` / `$XMEM_INSTANCE_ID`. `--include-yaml`. |
213
+ | Fetch one migration | **`schema migrations get`** `[instance_id]` `<migration_id>` | Optional positional instance id; else `--instance-id` / `$XMEM_INSTANCE_ID`. `--include-yaml`. |
208
214
  | Enhance bound schema | **`xmd enhance --from-instance`** `<description>` | Uses live schema; requires instance id. |
209
215
  | Synthesize schema | **`xmd generate`** `<description>` | Cluster-scoped LLM synthesis. `-o` writes YAML. |
210
216
  | Enhance from file | **`xmd enhance`** `[schema_file]` `<description>` | From a file, or `--from-instance` for bound enhance. `-o`, `--plan-output`. |
@@ -231,7 +237,7 @@ xmemcli schema update INST_ID --schema-file next.yml --migration-plan plan.json
231
237
 
232
238
  # 4. Inspect history
233
239
  xmemcli schema migrations list INST_ID
234
- xmemcli schema migrations get MIGRATION_UUID
240
+ xmemcli schema migrations get INST_ID MIGRATION_UUID
235
241
  ```
236
242
 
237
243
  #### Commands
@@ -242,7 +248,7 @@ xmemcli schema migrations get MIGRATION_UUID
242
248
  | **`schema update`** `[instance_id]` | Replace schema from `--schema-file`. Optional `--schema-type`, `--migration-plan`, `--confirm-destructive`. |
243
249
  | **`schema dry-run`** `[instance_id]` | Preview migration (same schema/plan flags as update). |
244
250
  | **`schema migrations list`** `[instance_id]` | Applied migrations, newest first. `--limit` (default 50), `--before-id`, `--include-yaml`. |
245
- | **`schema migrations get`** `<migration_id>` | One record; instance from global instance flags. `--include-yaml`. |
251
+ | **`schema migrations get`** `[instance_id]` `<migration_id>` | One record. Optional positional instance id; else global instance flags. `--include-yaml`. |
246
252
  | **`xmd generate`** `<description>` | LLM-synthesize XMD. Optional `-o`. Requires cluster resolution. |
247
253
  | **`xmd enhance`** `[schema_file]` `<description>` | LLM-augment schema from file, or **`--from-instance`** for live schema. `-o`, `--plan-output`. |
248
254
  | **`xmd validate`** `<schema_file>` | Validate; YAML vs JSON inferred from `.json` extension. |
@@ -250,7 +256,7 @@ xmemcli schema migrations get MIGRATION_UUID
250
256
  | **`xmd validate-json`** `<schema_file>` | Force JSON Schema validation endpoint. |
251
257
  | **`xmd convert`** `<json_schema_file>` | JSON Schema → XMD YAML via the API (`validate_json_schema`). Optional `-o`, `--root-object`. |
252
258
 
253
- Instance id for `schema *` commands: positional `[instance_id]`, else **`--instance-id`** / **`$XMEM_INSTANCE_ID`** (except `migrations get`, which takes only `migration_id`).
259
+ Instance id for `schema *` commands: positional `[instance_id]`, else **`--instance-id`** / **`$XMEM_INSTANCE_ID`**.
254
260
 
255
261
  ---
256
262
 
@@ -278,10 +284,23 @@ Instance id for `schema *` commands: positional `[instance_id]`, else **`--insta
278
284
  | **`quota`** | Usage / limit / window start for the key's cluster (derived from instance → cluster). |
279
285
 
280
286
  - **Default:** exit `0` when quota is readable; stderr **warning** when usage is high.
281
- - **`--fail-if-under THRESHOLD`** — exit non-zero when **remaining** quota is below that share of the limit (`0.1` or `10%`). See `xmemcli help quota`.
287
+ - **`--fail-if-under THRESHOLD`** — exit **7** when **remaining** quota is below that share of the limit (`0.1` or `10%`). Distinct from exit **6** (API quota exhausted). See `xmemcli help quota`.
282
288
 
283
289
  Quota **increases** are Console-only — see [What `xmemcli` does not do](#what-xmemcli-does-not-do).
284
290
 
291
+ ### Exit codes
292
+
293
+ | Code | Meaning |
294
+ |------|---------|
295
+ | **0** | Success |
296
+ | **2** | Usage error (bad flags, unknown `help` topic, missing required args) |
297
+ | **3** | HTTP / request failure (includes unwritable `-o` paths) |
298
+ | **4** | Auth error (missing or rejected API key) |
299
+ | **5** | Timeout |
300
+ | **6** | Quota exhausted (API quota-exceeded response) |
301
+ | **7** | Quota headroom below `--fail-if-under` |
302
+ | **130** | Interrupted (Ctrl+C) |
303
+
285
304
  ---
286
305
 
287
306
  ## Help
@@ -296,6 +315,8 @@ Quota **increases** are Console-only — see [What `xmemcli` does not do](#what-
296
315
  | **`xmemcli help <topic>`** | Usage, options, and subcommands for that topic |
297
316
  | **`xmemcli <cmd> … -h`** / **`--help`** | Same as `help <topic>` for that command path |
298
317
 
318
+ Unknown help topics exit **2** (same as other usage errors).
319
+
299
320
  **Every command in this document has help.** Examples:
300
321
 
301
322
  ```text
@@ -340,7 +361,7 @@ Nested topics work at any level that exists in the parser (`help org`, `help org
340
361
  | **Cluster create/delete** | CLI lists clusters; provisioning in Console |
341
362
  | **`read --extraction deep`** | Server has no read-depth knob today |
342
363
  | **Raw REST proxy** | Curated response fields only |
343
- | **Instance deletion** | **Not available in the CLI** — explicitly disabled and reserved for manual human use in the Console. |
364
+ | **Instance deletion** | **Human operators only** — a hidden `instance delete` exists for interactive TTY use (type `yes` to confirm). It is omitted from help and must not be used by agents or automation. Console UI remains the primary human path. |
344
365
 
345
366
  On HTTP **402** (quota exhausted), stop and ask a human to extend quota in the Console.
346
367
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xmemcli"
3
- version = "0.0.3"
3
+ version = "0.0.4"
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.3"
7
+ version = "0.0.4"
8
8
  source = { editable = "." }
@@ -1,3 +1,3 @@
1
1
  """Standalone xmemcli package (``xmemcli`` console script)."""
2
2
 
3
- __version__ = "0.0.3"
3
+ __version__ = "0.0.4"
@@ -8,7 +8,7 @@ import argparse
8
8
  import os
9
9
  import re
10
10
  import sys
11
- from typing import Any, IO, NoReturn
11
+ from typing import Any, IO, NoReturn, override
12
12
 
13
13
  from ._version import CLI_NAME, __version__
14
14
 
@@ -33,6 +33,17 @@ _INTERNAL_DESTS = frozenset(
33
33
  )
34
34
  # argparse alias names we hide from the auto-generated overview (canonical names only).
35
35
  _HIDDEN_COMMAND_NAMES = frozenset(
36
+ {
37
+ "write_status",
38
+ "writestatus",
39
+ "async_write_status",
40
+ "asyncwritestatus",
41
+ "async-write-status",
42
+ "delete",
43
+ }
44
+ )
45
+ # Silent argv aliases for write-status spellings (also hidden from help subcommand lists).
46
+ _WRITE_STATUS_ALIAS_NAMES = frozenset(
36
47
  {
37
48
  "write_status",
38
49
  "writestatus",
@@ -184,7 +195,10 @@ def _option_label(action: argparse.Action) -> str:
184
195
  return f"{longest} …"
185
196
  return longest
186
197
  if action.metavar:
187
- return action.metavar
198
+ meta = action.metavar
199
+ if isinstance(meta, tuple):
200
+ return " ".join(meta)
201
+ return meta
188
202
  return action.dest
189
203
 
190
204
 
@@ -499,7 +513,7 @@ def _render_unknown_document(doc: dict[str, Any], stream: IO[str], style: _Style
499
513
 
500
514
 
501
515
  def render_help_document(doc: dict[str, Any], *, stream: IO[str] | None = None) -> None:
502
- """Render a :func:`build_help_document` payload for human terminals."""
516
+ """Render a `build_help_document` payload for human terminals."""
503
517
  out = stream if stream is not None else sys.stdout
504
518
  style = _Style(_use_color(out))
505
519
  kind = doc.get("kind")
@@ -556,13 +570,14 @@ def _format_cli_parse_error(message: str) -> str:
556
570
 
557
571
 
558
572
  class XmemCliArgumentParser(argparse.ArgumentParser):
559
- """ArgumentParser that raises :class:`CliParseError` instead of dumping usage."""
573
+ """ArgumentParser that raises `CliParseError` instead of dumping usage."""
560
574
 
561
575
  def __init__(self, *args: Any, **kwargs: Any) -> None:
562
576
  kwargs.setdefault("usage", argparse.SUPPRESS)
563
577
  kwargs.setdefault("add_help", False)
564
578
  super().__init__(*args, **kwargs)
565
579
 
580
+ @override
566
581
  def error(self, message: str) -> NoReturn:
567
582
  raise CliParseError(_format_cli_parse_error(message))
568
583
 
@@ -703,12 +718,13 @@ def _choice_help(sub: argparse._SubParsersAction, name: str, child: argparse.Arg
703
718
  def _parser_title(root: argparse.ArgumentParser, path: tuple[str, ...], parser: argparse.ArgumentParser) -> str:
704
719
  if parser.description:
705
720
  return parser.description
706
- if not path:
721
+ if len(path) == 0:
707
722
  return ""
708
- parent = navigate_parser(root, path[:-1]) if len(path) > 1 else root
723
+ *parent_path, leaf = path
724
+ parent = navigate_parser(root, tuple(parent_path)) if parent_path else root
709
725
  sub = _subparsers_action(parent) if parent is not None else None
710
726
  if sub is not None:
711
- return _choice_help(sub, path[-1], parser)
727
+ return _choice_help(sub, leaf, parser)
712
728
  return " ".join(path)
713
729
 
714
730
 
@@ -1,6 +1,6 @@
1
1
  """Instance lifecycle and XMD/schema workflow commands for ``xmemcli``.
2
2
 
3
- Imported lazily from ``xmem._build_parser`` so ``xmem`` finishes loading before we bind helpers.
3
+ Bound from ``xmem._build_parser`` after core ``xmem`` helpers are defined.
4
4
  """
5
5
 
6
6
  import argparse
@@ -12,7 +12,7 @@ from typing import Any, Protocol
12
12
  from urllib.parse import urlencode
13
13
 
14
14
  from ._output import CliOutput, OutputKind, make_error
15
- from ._xmem_bridge import curate_instance, http_to_exit, xmem_module
15
+ from ._xmem_bridge import curate_instance, xmem_module
16
16
 
17
17
  try:
18
18
  import yaml as _yaml
@@ -26,6 +26,23 @@ def _yaml_dump_or_json(value: Any) -> str:
26
26
  return json.dumps(value, indent=2) + "\n"
27
27
 
28
28
 
29
+ def _write_text_file(
30
+ path: str | Path,
31
+ content: str,
32
+ *,
33
+ stage: str = "output",
34
+ ) -> tuple[CliOutput, int] | None:
35
+ """Write *content* to *path*; return ``(error, exit_code)`` on ``OSError``."""
36
+ target = Path(path)
37
+ try:
38
+ target.parent.mkdir(parents=True, exist_ok=True)
39
+ target.write_text(content, encoding="utf-8")
40
+ except OSError as exc:
41
+ m = xmem_module()
42
+ return (make_error(f"cannot write {target}: {exc}", stage=stage), m.EXIT_HTTP_ERROR)
43
+ return None
44
+
45
+
29
46
  def _infer_schema_type(path: str | None, explicit: str | None) -> str:
30
47
  if explicit:
31
48
  return explicit
@@ -214,6 +231,12 @@ def _get_with_query(url: str, api_key: str, params: dict[str, Any], *, timeout_s
214
231
 
215
232
 
216
233
  def cmd_instance_get(args: argparse.Namespace) -> tuple[CliOutput, int]:
234
+ """Return one instance by id.
235
+
236
+ Uses cluster-less ``GET /instances/{id}`` by default; the API resolves
237
+ ``cluster_id`` from the instance record and checks access in one round trip.
238
+ When ``--cluster`` is set, uses ``GET /clusters/{cluster}/instances/{id}``.
239
+ """
217
240
  m = xmem_module()
218
241
  api_key, _ = m._require(args, need_instance=False)
219
242
  base = m._api_url(args)
@@ -221,45 +244,26 @@ def cmd_instance_get(args: argparse.Namespace) -> tuple[CliOutput, int]:
221
244
 
222
245
  explicit_cluster = getattr(args, "cluster", None)
223
246
  if explicit_cluster:
224
- cluster_ids = [explicit_cluster]
247
+ url = f"{base}/clusters/{explicit_cluster}/instances/{instance_id}"
225
248
  else:
226
- try:
227
- clusters_env = m._get(f"{base}/clusters?verbose=true", api_key)
228
- except m.ApiError as e:
229
- return (make_error(f"/clusters failed: {e}", status=e.status), http_to_exit(e.status))
230
- cluster_ids = [c["id"] for c in (clusters_env.get("items") or []) if isinstance(c, dict) and c.get("id")]
249
+ url = f"{base.rstrip('/')}/instances/{instance_id}"
231
250
 
232
- last_auth_error: m.ApiError | None = None
233
- for cid in cluster_ids:
234
- try:
235
- envelope = m._get(f"{base}/clusters/{cid}/instances/{instance_id}", api_key)
236
- except m.ApiError as e:
237
- if e.status == 404:
238
- continue
239
- if e.status in (401, 403):
240
- last_auth_error = e
241
- continue
242
- return (make_error(str(e), status=e.status, stage="request"), m.EXIT_HTTP_ERROR)
243
- item = m._unwrap_items(envelope)
244
- return (CliOutput(OutputKind.INSTANCE_GET, curate_instance(item, args.verbose)), m.EXIT_OK)
251
+ try:
252
+ envelope = m._get(url, api_key)
253
+ except m.ApiError as e:
254
+ if e.status == 404:
255
+ msg = (
256
+ f"instance {instance_id} not found in cluster {explicit_cluster}"
257
+ if explicit_cluster
258
+ else f"instance {instance_id} not found or not visible to this key"
259
+ )
260
+ return (make_error(msg, status=e.status, stage="request"), m.EXIT_HTTP_ERROR)
261
+ if e.status in (401, 403):
262
+ return (make_error(str(e), status=e.status, stage="auth"), m.EXIT_AUTH_ERROR)
263
+ return (make_error(str(e), status=e.status, stage="request"), m.EXIT_HTTP_ERROR)
245
264
 
246
- if last_auth_error is not None:
247
- return (
248
- make_error(
249
- f"instance {instance_id} exists in a cluster this key cannot access",
250
- status=last_auth_error.status,
251
- stage="auth",
252
- ),
253
- m.EXIT_AUTH_ERROR,
254
- )
255
- return (
256
- make_error(
257
- f"instance {instance_id} not found in any cluster visible to this key",
258
- status=404,
259
- stage="request",
260
- ),
261
- m.EXIT_HTTP_ERROR,
262
- )
265
+ item = m._unwrap_items(envelope)
266
+ return (CliOutput(OutputKind.INSTANCE_GET, curate_instance(item, args.verbose)), m.EXIT_OK)
263
267
 
264
268
 
265
269
  def _require_interactive_yes(prompt: str) -> tuple[bool, str | None]:
@@ -267,7 +271,7 @@ def _require_interactive_yes(prompt: str) -> tuple[bool, str | None]:
267
271
  if not sys.stdin.isatty():
268
272
  return False, "instance delete requires an interactive terminal; type yes when prompted (do not pipe yes)"
269
273
  try:
270
- # DELIBERATELY print: confirmation prompt must not pollute stdout in machine mode.
274
+ # DELIBERATELY write to stderr: confirmation prompt must not pollute stdout in machine mode.
271
275
  print(prompt, end="", file=sys.stderr, flush=True)
272
276
  answer = input().strip().lower()
273
277
  except EOFError:
@@ -282,8 +286,7 @@ def cmd_instance_delete(args: argparse.Namespace) -> tuple[CliOutput, int]:
282
286
 
283
287
  Partial by design: human operators at a real TTY must type ``yes``; piped or
284
288
  non-interactive stdin is refused. Agents, harnesses, and automation must use
285
- ``DELETE /instances/{id}`` over HTTP instead — see ``cli/HARNESS-FIRST-TEST.md``
286
- step 41.
289
+ ``DELETE /instances/{id}`` over HTTP instead — see harness step 41 in ``HARNESS-FIRST-TEST.md``.
287
290
  """
288
291
  m = xmem_module()
289
292
  api_key, _ = m._require(args, need_instance=False)
@@ -432,7 +435,9 @@ def cmd_xmd_generate(args: argparse.Namespace) -> tuple[CliOutput, int]:
432
435
  item = m._unwrap_items(envelope)
433
436
  schema = item.get("data_schema") if isinstance(item, dict) else None
434
437
  if args.output and schema is not None:
435
- Path(args.output).write_text(_yaml_dump_or_json(schema), encoding="utf-8")
438
+ write_err = _write_text_file(args.output, _yaml_dump_or_json(schema), stage="generate")
439
+ if write_err is not None:
440
+ return write_err
436
441
  return (CliOutput(OutputKind.XMD_SCHEMA, item), m.EXIT_OK)
437
442
 
438
443
 
@@ -503,11 +508,15 @@ def cmd_xmd_enhance(args: argparse.Namespace) -> tuple[CliOutput, int]:
503
508
  return (make_error(str(e), status=e.status, stage="auth"), m.EXIT_AUTH_ERROR)
504
509
  return (make_error(str(e), status=e.status, stage="request"), m.EXIT_HTTP_ERROR)
505
510
  item = m._unwrap_items(envelope)
506
- migration_plan = item.get("migration_plan") if isinstance(item, dict) else None
507
511
  if args.output and isinstance(item, dict) and item.get("data_schema") is not None:
508
- Path(args.output).write_text(_yaml_dump_or_json(item["data_schema"]), encoding="utf-8")
512
+ write_err = _write_text_file(args.output, _yaml_dump_or_json(item["data_schema"]), stage="enhance")
513
+ if write_err is not None:
514
+ return write_err
509
515
  if args.plan_output and isinstance(item, dict):
510
- Path(args.plan_output).write_text(json.dumps(item.get("migration_plan") or {}, indent=2), encoding="utf-8")
516
+ plan_body = json.dumps(item.get("migration_plan") or {}, indent=2)
517
+ write_err = _write_text_file(args.plan_output, plan_body, stage="enhance")
518
+ if write_err is not None:
519
+ return write_err
511
520
  return (CliOutput(OutputKind.XMD_ENHANCE, item), m.EXIT_OK)
512
521
 
513
522
 
@@ -638,7 +647,9 @@ def cmd_schema_get(args: argparse.Namespace) -> tuple[CliOutput, int]:
638
647
  item = m._unwrap_items(envelope)
639
648
  schema = item.get("data_schema") if isinstance(item, dict) else None
640
649
  if args.output and schema is not None:
641
- Path(args.output).write_text(_yaml_dump_or_json(schema), encoding="utf-8")
650
+ write_err = _write_text_file(args.output, _yaml_dump_or_json(schema), stage="schema")
651
+ if write_err is not None:
652
+ return write_err
642
653
  return (CliOutput(OutputKind.XMD_SCHEMA, item), m.EXIT_OK)
643
654
 
644
655
 
@@ -87,10 +87,6 @@ def redirect_uri_for_port(port: int) -> str:
87
87
  return f"http://127.0.0.1:{port}{CALLBACK_PATH}"
88
88
 
89
89
 
90
- def redirect_uri() -> str:
91
- return redirect_uri_for_port(CALLBACK_PORT)
92
-
93
-
94
90
  def new_verifier() -> str:
95
91
  return secrets.token_urlsafe(32)
96
92
 
@@ -151,8 +147,17 @@ def write_login_handoff_prompt(
151
147
  "",
152
148
  f"Authorize {CLI_NAME} on this machine.",
153
149
  "",
154
- _login_prompt_line("Browser", "Open this URL and click \"Use this key in the CLI\":"),
150
+ _login_prompt_line("Browser", "Open this URL in your browser:"),
155
151
  f"{body_indent}{login_url}",
152
+ "",
153
+ _login_prompt_line(
154
+ "Onboarding",
155
+ "During first-instance setup the Console auto-delivers the key — no extra clicks.",
156
+ ),
157
+ _login_prompt_line(
158
+ "After onboarding",
159
+ "Create a key on API Keys and click \"Use this key in the CLI\".",
160
+ ),
156
161
  ]
157
162
  if offer_paste:
158
163
  lines.extend(
@@ -168,7 +173,6 @@ def write_login_handoff_prompt(
168
173
  [
169
174
  "",
170
175
  _login_prompt_line("Manual", "Set $XMEM_API_KEY or `.xmemrc.json`"),
171
- f"{body_indent}See AUTH-FLOW.md.",
172
176
  "",
173
177
  ]
174
178
  )
@@ -331,8 +335,7 @@ def redeem_token(
331
335
  continue
332
336
  hint = (
333
337
  "Could not reach the accounts API token endpoint. "
334
- "Set $XMEM_CONSOLE_URL to your Console (e.g. dk-web…) and ensure it can reach the API "
335
- "(ACCOUNTS_API_URL on the Console server)."
338
+ "Set $XMEM_CONSOLE_URL to your Console origin and retry, or pass --console-url."
336
339
  )
337
340
  detail = "; ".join(errors[-3:]) if errors else "no endpoints tried"
338
341
  raise LoginError(f"token redeem failed: {detail}. {hint}")
@@ -380,7 +383,7 @@ def _read_pasted_api_key_no_echo(*, stop: threading.Event) -> str | None:
380
383
  return None
381
384
  finally:
382
385
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
383
- # DELIBERATELY print: newline after raw-mode paste must not touch stdout (machine JSON).
386
+ # DELIBERATELY write to stderr: newline after raw-mode paste must not touch stdout (machine JSON).
384
387
  sys.stderr.write("\n")
385
388
  sys.stderr.flush()
386
389
 
@@ -19,9 +19,9 @@ keep strictly separated:
19
19
 
20
20
  To keep the dispatch logic in one place and force every new subcommand to
21
21
  declare which family of output it produces, the commands no longer print
22
- directly. They build a :class:`CliOutput` (a discriminated payload tagged
23
- with an :class:`OutputKind`), and the entry point in ``cli/xmem.py`` decides
24
- how to render it via :func:`emit`. The pretty-print branch lives in
22
+ directly. They build a `CliOutput` (a discriminated payload tagged
23
+ with an `OutputKind`), and the entry point in ``cli/xmem.py`` decides
24
+ how to render it via `emit`. The pretty-print branch lives in
25
25
  ``cli/_pretty.py`` so the formatting code can grow without cluttering the
26
26
  HTTP and argparse plumbing.
27
27
 
@@ -33,13 +33,13 @@ Invariants
33
33
  - Pretty-print is only ever reached when ``stdout.isatty()`` is true AND
34
34
  ``--json`` was not passed. Anything else gets JSON.
35
35
  - ``$XMEM_FORCE_TTY_OUTPUT_NOT_JSON`` is a **deliberate, documented escape hatch**
36
- for manual E2E runbooks only (see :data:`ENV_FORCE_TTY_OUTPUT_NOT_JSON`). It does
36
+ for manual E2E runbooks only (see `ENV_FORCE_TTY_OUTPUT_NOT_JSON`). It does
37
37
  not change server behavior, credentials, or API payloads — only how ``emit`` chooses
38
38
  between JSON and pretty text. Agents, CI, and piped scripts must never set it;
39
39
  they use ``--json`` or a real TTY. If unset (the normal case), the non-TTY⇒JSON
40
40
  rule is unchanged.
41
41
  - A failed command still emits exactly one JSON document on stdout when JSON
42
- mode is in effect — :class:`OutputKind.ERROR` + a non-zero exit code. We
42
+ mode is in effect — `OutputKind.ERROR` + a non-zero exit code. We
43
43
  do not silently dump human prose to stderr in machine mode.
44
44
  - The pretty-print renderers may fall back to JSON if they can't extract a
45
45
  useful summary from the payload; that is intentional and keeps human
@@ -48,11 +48,11 @@ Invariants
48
48
  Wire format
49
49
  -----------
50
50
 
51
- The JSON document emitted by :func:`emit` is the CURATED payload built by
51
+ The JSON document emitted by `emit` is the CURATED payload built by
52
52
  the subcommand (see the ``_curate_*`` helpers in ``cli/xmem.py``) — the
53
53
  fields the user asked for, with server internals (SQL, diff plans, raw
54
54
  extraction trees) stripped in both output modes. We do NOT wrap it in a
55
- second envelope keyed by :class:`OutputKind` — adding a layer the consumer
55
+ second envelope keyed by `OutputKind` — adding a layer the consumer
56
56
  didn't ask for would make scripts more fragile, not less. The kind is only
57
57
  used internally for pretty-print dispatch.
58
58
  """
@@ -98,7 +98,7 @@ class OutputKind(StrEnum):
98
98
 
99
99
  #: Read in ``mode=xresponse``. Curated payload: the reader_result
100
100
  #: subtree shaped as an XResponse (objects + relations). Pretty-print
101
- #: renders as JSON for the same reason as :attr:`READ_RAW`.
101
+ #: renders as JSON for the same reason as `READ_RAW`.
102
102
  READ_XRESPONSE = "read_xresponse"
103
103
 
104
104
  #: Output of ``xmemcli org list keys``. Curated payload: a list of
@@ -109,18 +109,18 @@ class OutputKind(StrEnum):
109
109
  #: Output of ``xmemcli org list clusters``.
110
110
  CLUSTER_LIST = "cluster_list"
111
111
 
112
- #: Output of ``xmem auth status``. Curated payload:
112
+ #: Output of ``xmemcli auth status``. Curated payload:
113
113
  #: ``{authenticated: bool, key_prefix, email?}`` — whose key the CLI is
114
114
  #: holding, resolved via ``/whoami``.
115
115
  AUTH_STATUS = "auth_status"
116
116
 
117
- #: Output of ``xmem auth login`` / ``xmem auth logout``.
117
+ #: Output of ``xmemcli auth login`` / ``xmemcli auth logout``.
118
118
  LOGIN = "login"
119
119
 
120
- #: Output of ``xmem version``.
120
+ #: Output of ``xmemcli version``.
121
121
  VERSION = "version"
122
122
 
123
- #: Local-only release health (``xmem status``) — no network.
123
+ #: Local-only release health (``xmemcli status``) — no network.
124
124
  STATUS = "status"
125
125
 
126
126
  #: Output of ``xmemcli quota``. Curated payload: usage windows.
@@ -129,31 +129,31 @@ class OutputKind(StrEnum):
129
129
  #: Output of ``xmemcli org list instances`` (and ``instance list`` alias).
130
130
  INSTANCE_LIST = "instance_list"
131
131
 
132
- #: Output of ``xmem instance get``.
132
+ #: Output of ``xmemcli instance get``.
133
133
  INSTANCE_GET = "instance_get"
134
134
 
135
- #: Output of ``xmem instance create``.
135
+ #: Output of ``xmemcli instance create``.
136
136
  INSTANCE_CREATE = "instance_create"
137
137
 
138
- #: Output of ``xmem instance delete``.
138
+ #: Output of ``xmemcli instance delete``.
139
139
  INSTANCE_DELETE = "instance_delete"
140
140
 
141
- #: Output of ``xmem schema update``.
141
+ #: Output of ``xmemcli schema update``.
142
142
  SCHEMA_UPDATE = "schema_update"
143
143
 
144
- #: Output of ``xmem schema dry-run``.
144
+ #: Output of ``xmemcli schema dry-run``.
145
145
  SCHEMA_DRY_RUN = "schema_dry_run"
146
146
 
147
- #: Output of ``xmem schema migrations list`` / ``get``.
147
+ #: Output of ``xmemcli schema migrations list`` / ``get``.
148
148
  SCHEMA_MIGRATIONS = "schema_migrations"
149
149
 
150
- #: Output of ``xmem xmd generate`` / ``enhance`` / ``validate`` / ``schema get``.
150
+ #: Output of ``xmemcli xmd generate`` / ``enhance`` / ``validate`` / ``schema get``.
151
151
  XMD_SCHEMA = "xmd_schema"
152
152
 
153
- #: Output of ``xmem xmd enhance`` when a migration plan is included.
153
+ #: Output of ``xmemcli xmd enhance`` when a migration plan is included.
154
154
  XMD_ENHANCE = "xmd_enhance"
155
155
 
156
- #: Output of ``xmem xmd convert``.
156
+ #: Output of ``xmemcli xmd convert``.
157
157
  XMD_CONVERT = "xmd_convert"
158
158
 
159
159
  #: Structured help document from ``build_help_document`` (overview, options,
@@ -163,7 +163,7 @@ class OutputKind(StrEnum):
163
163
 
164
164
  #: A failure surfaced as structured output. ``payload`` carries
165
165
  #: ``{"error": str, "status": int|null, "stage": str}`` — see
166
- #: :func:`make_error`.
166
+ #: `make_error`.
167
167
  ERROR = "error"
168
168
 
169
169
 
@@ -183,7 +183,7 @@ class CliOutput(NamedTuple):
183
183
 
184
184
 
185
185
  def make_error(message: str, *, status: int | None = None, stage: str = "request") -> CliOutput:
186
- """Construct an :class:`OutputKind.ERROR` payload.
186
+ """Construct an `OutputKind.ERROR` payload.
187
187
 
188
188
  ``status`` is the HTTP status code when the failure originated from a
189
189
  server response; ``stage`` is a free-form label naming the step that
@@ -219,8 +219,8 @@ def stdout_is_tty() -> bool:
219
219
  don't implement it.
220
220
 
221
221
  ``$XMEM_FORCE_TTY_OUTPUT_NOT_JSON`` — when set to a truthy value, pretend stdout is a
222
- TTY so :func:`emit` uses pretty one-liners on piped/captured stdout. Manual Case 4
223
- only; see :data:`ENV_FORCE_TTY_OUTPUT_NOT_JSON`. Does not apply when ``force_json``
222
+ TTY so `emit` uses pretty one-liners on piped/captured stdout. Manual Case 4
223
+ only; see `ENV_FORCE_TTY_OUTPUT_NOT_JSON`. Does not apply when ``force_json``
224
224
  is true (``--json``).
225
225
  """
226
226
  override = _env_tty_override()
@@ -249,7 +249,7 @@ def emit(
249
249
  ``True`` when ``--json`` was passed. Pretty-print is suppressed
250
250
  unconditionally and the renderer emits exactly one JSON document.
251
251
  is_tty
252
- Override for :func:`stdout_is_tty`. Tests can pass an explicit
252
+ Override for `stdout_is_tty`. Tests can pass an explicit
253
253
  boolean to exercise both branches without monkey-patching.
254
254
  stream
255
255
  Defaults to ``sys.stdout``. Tests pass ``io.StringIO()`` here to
@@ -1,20 +1,20 @@
1
- """Human-friendly renderers for :mod:`_output` payloads.
1
+ """Human-friendly renderers for `cli._output` payloads.
2
2
 
3
3
  This module is **only** reached when the operator is sitting at a real
4
4
  terminal (``sys.stdout.isatty()``) AND did not pass ``--json``. Every other
5
- invocation gets strict JSON via :func:`_output.emit` — see that module's
5
+ invocation gets strict JSON via `cli._output.emit` — see that module's
6
6
  docstring for the rationale.
7
7
 
8
8
  The renderers are deliberately small, opinionated, and one-shot: they print
9
9
  a single short summary (often a single line) keyed off the
10
- :class:`~cli._output.OutputKind`. The principle is "give the operator the
10
+ `OutputKind`. The principle is "give the operator the
11
11
  headline they would have wanted to read aloud", not "render the entire
12
12
  payload". If the renderer can't extract a useful headline, it falls through
13
13
  to a JSON dump of the relevant sub-tree so the user still sees something
14
14
  truthful rather than a guess.
15
15
 
16
- Adding a new :class:`~cli._output.OutputKind` requires adding a renderer
17
- here keyed by the enum value in :data:`_RENDERERS`. The default handler
16
+ Adding a new `OutputKind` requires adding a renderer
17
+ here keyed by the enum value in `_RENDERERS`. The default handler
18
18
  emits the JSON payload, which is a safe (if uninspired) fallback.
19
19
  """
20
20
 
@@ -31,10 +31,10 @@ Renderer = Callable[[Any, IO[str]], None]
31
31
 
32
32
 
33
33
  def render(kind: str | Enum, payload: Any, stream: IO[str]) -> None:
34
- """Dispatch to a renderer by :class:`OutputKind`.
34
+ """Dispatch to a renderer by `OutputKind`.
35
35
 
36
36
  Unknown kinds (or any future kind without a registered renderer) fall
37
- through to :func:`_default_handler`, which dumps the payload as JSON.
37
+ through to `_default_handler`, which dumps the payload as JSON.
38
38
  """
39
39
  dispatch_key = kind.value if isinstance(kind, Enum) else kind
40
40
  handler = _RENDERERS.get(dispatch_key, _default_handler)
@@ -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.3"
4
+ __version__ = "0.0.4"
@@ -4,10 +4,10 @@ This file is the CLI entry point. It deliberately uses only the standard
4
4
  library so the demo runs anywhere a Python 3.12+ interpreter exists without
5
5
  ``pip install``. The interesting structural decision is that **commands do
6
6
  not format output themselves** — every subcommand builds a
7
- :class:`_output.CliOutput` and returns ``(output, exit_code)``. The
8
- ``main`` dispatch then emits via :func:`_output.emit`, which picks
7
+ `cli._output.CliOutput` and returns ``(output, exit_code)``. The
8
+ ``main`` dispatch then emits via `cli._output.emit`, which picks
9
9
  between strict JSON (machine consumption) and a one-line human summary
10
- (interactive terminal) per the rules documented in :mod:`_output`.
10
+ (interactive terminal) per the rules documented in `cli._output`.
11
11
 
12
12
  Endpoints exercised (under ``$XMEM_API_URL`` unless noted; default
13
13
  ``https://api.xmemory.ai``):
@@ -120,6 +120,8 @@ EXIT_TIMEOUT = 5
120
120
  # ``QuotaExceededKind`` discriminator on the wire body so the printed
121
121
  # message can match (TRIAL ENDED / DAILY QUOTA / MONTHLY QUOTA).
122
122
  EXIT_QUOTA_EXCEEDED = 6
123
+ # Local quota headroom gate (`quota --fail-if-under`) — distinct from API 429 / exhausted quota.
124
+ EXIT_QUOTA_THRESHOLD = 7
123
125
  # Conventional shell exit for SIGINT (128 + 2) — no stack trace, not a usage error.
124
126
  EXIT_INTERRUPTED = 130
125
127
 
@@ -471,7 +473,7 @@ class UsageError(Exception):
471
473
  an unset instance id, or the localhost-key-at-prod guard.
472
474
 
473
475
  It carries an ``OutputKind.ERROR`` payload so ``main`` renders it through
474
- :func:`emit` like any other result. That keeps the machine-mode invariant:
476
+ `emit` like any other result. That keeps the machine-mode invariant:
475
477
  a failed command still emits **exactly one** JSON document on stdout, never
476
478
  bare prose to stderr (see ``cli/_output.py``).
477
479
  """
@@ -845,6 +847,7 @@ def cmd_auth_status(args: argparse.Namespace) -> tuple[CliOutput, int]:
845
847
  "logged_in": True,
846
848
  "email": auth_api_key.get("email"),
847
849
  "key_prefix": api_key[:8],
850
+ "onboarding": bool(auth_api_key.get("onboarding")),
848
851
  },
849
852
  ),
850
853
  EXIT_OK,
@@ -985,7 +988,7 @@ def _resolve_cluster_id(
985
988
 
986
989
  Order: operator override → instance context → sole visible cluster.
987
990
  Agent-facing errors steer toward ``--instance-id``, not cluster ID
988
- (see ``maxims/API-CLIENTS.yml`` — *One addressing axis for agents*).
991
+ Agent-facing errors steer toward ``--instance-id``, not cluster ID (one addressing axis for agents).
989
992
  """
990
993
  override = _operator_cluster_override(args)
991
994
  if override:
@@ -1191,7 +1194,7 @@ def cmd_quota_get(args: argparse.Namespace) -> tuple[CliOutput, int]:
1191
1194
  and not isinstance(remaining, bool)
1192
1195
  and (remaining / limit) < fail_threshold
1193
1196
  ):
1194
- exit_code = EXIT_QUOTA_EXCEEDED
1197
+ exit_code = EXIT_QUOTA_THRESHOLD
1195
1198
  return (CliOutput(OutputKind.QUOTA, curated), exit_code)
1196
1199
 
1197
1200
 
@@ -1225,33 +1228,32 @@ def cmd_status(args: argparse.Namespace) -> tuple[CliOutput, int]:
1225
1228
  return (CliOutput(OutputKind.STATUS, payload), EXIT_OK)
1226
1229
 
1227
1230
 
1228
- from ._instance_schema import ( # noqa: E402 — after ``xmem`` helpers; avoids import cycle at module start
1229
- cmd_instance_create,
1230
- cmd_instance_delete,
1231
- cmd_instance_get,
1232
- cmd_schema_dry_run,
1233
- cmd_schema_get,
1234
- cmd_schema_migrations_get,
1235
- cmd_schema_migrations_list,
1236
- cmd_schema_update,
1237
- cmd_xmd_convert,
1238
- cmd_xmd_enhance,
1239
- cmd_xmd_generate,
1240
- cmd_xmd_validate,
1241
- cmd_xmd_validate_json,
1242
- cmd_xmd_validate_yaml,
1243
- )
1244
- from ._org_cluster import ( # noqa: E402
1245
- cmd_org_list_clusters,
1246
- cmd_org_list_instances,
1247
- cmd_org_list_keys,
1248
- )
1249
-
1250
-
1251
1231
  # ── argparse plumbing ──────────────────────────────────────────────────────
1252
1232
 
1253
1233
 
1254
1234
  def _build_parser() -> argparse.ArgumentParser:
1235
+ from ._instance_schema import (
1236
+ cmd_instance_create,
1237
+ cmd_instance_delete,
1238
+ cmd_instance_get,
1239
+ cmd_schema_dry_run,
1240
+ cmd_schema_get,
1241
+ cmd_schema_migrations_get,
1242
+ cmd_schema_migrations_list,
1243
+ cmd_schema_update,
1244
+ cmd_xmd_convert,
1245
+ cmd_xmd_enhance,
1246
+ cmd_xmd_generate,
1247
+ cmd_xmd_validate,
1248
+ cmd_xmd_validate_json,
1249
+ cmd_xmd_validate_yaml,
1250
+ )
1251
+ from ._org_cluster import (
1252
+ cmd_org_list_clusters,
1253
+ cmd_org_list_instances,
1254
+ cmd_org_list_keys,
1255
+ )
1256
+
1255
1257
  p = _help.XmemCliArgumentParser(
1256
1258
  prog=CLI_NAME,
1257
1259
  description="xmemcli — CLI for the xmemory HTTP API (read/write, instance ops, key list).",
@@ -1263,7 +1265,7 @@ def _build_parser() -> argparse.ArgumentParser:
1263
1265
  "--rc-dir",
1264
1266
  help=f"Directory for {RC_FILENAME} (auth login writes here; reads walk up to $HOME)",
1265
1267
  )
1266
- # Operator-only override; omitted from --help (maxims/API-CLIENTS.yml — One addressing axis for agents).
1268
+ # Operator-only cluster override; omitted from --help (not agent-facing).
1267
1269
  p.add_argument("--cluster-id", help=argparse.SUPPRESS)
1268
1270
  p.add_argument(
1269
1271
  "--json",
@@ -1528,6 +1530,12 @@ def _build_parser() -> argparse.ArgumentParser:
1528
1530
  help="Fetch one migration record by id (MCP get_schema_migration)",
1529
1531
  add_help=False,
1530
1532
  )
1533
+ pschmg.add_argument(
1534
+ "instance_id_arg",
1535
+ metavar="instance_id",
1536
+ nargs="?",
1537
+ help=f"Instance id (default ${ENV_INSTANCE_ID} or --instance-id)",
1538
+ )
1531
1539
  pschmg.add_argument(
1532
1540
  "migration_id_arg",
1533
1541
  metavar="migration_id",
@@ -1632,13 +1640,7 @@ def _build_parser() -> argparse.ArgumentParser:
1632
1640
  return p
1633
1641
 
1634
1642
 
1635
- _WRITE_STATUS_ALIASES = frozenset({
1636
- "write_status",
1637
- "writestatus",
1638
- "async_write_status",
1639
- "asyncwritestatus",
1640
- "async-write-status",
1641
- })
1643
+ _WRITE_STATUS_ALIASES = _help._WRITE_STATUS_ALIAS_NAMES
1642
1644
 
1643
1645
  # Silent auth shortcuts — normalized before parse; not listed in the overview.
1644
1646
  _AUTH_LOGIN_ALIASES = frozenset({"login", "signin"})
@@ -1679,6 +1681,17 @@ def _auto_resolve_single_instance_id(args: argparse.Namespace, api_key: str) ->
1679
1681
  return instance_id, warning
1680
1682
 
1681
1683
 
1684
+ def _normalize_org_list_instances_argv(stripped: list[str]) -> list[str] | None:
1685
+ """Map silent ``instances`` / ``list instances`` spellings to ``org list instances``."""
1686
+ if not stripped:
1687
+ return None
1688
+ if stripped[0] == "instances":
1689
+ return ["org", "list", "instances", *stripped[1:]]
1690
+ if len(stripped) >= 2 and stripped[0] == "list" and stripped[1] == "instances":
1691
+ return ["org", "list", "instances", *stripped[2:]]
1692
+ return None
1693
+
1694
+
1682
1695
  def _normalize_auth_argv(stripped: list[str]) -> list[str] | None:
1683
1696
  """Map silent login/logout spellings to ``auth login`` / ``auth logout``."""
1684
1697
  if not stripped:
@@ -1726,36 +1739,17 @@ def _normalize_argv(argv: list[str]) -> list[str]:
1726
1739
  return ["write-status", *stripped[1:]]
1727
1740
  if len(stripped) >= 2 and stripped[0] == "quota" and stripped[1] == "get":
1728
1741
  return ["quota", *stripped[2:]]
1742
+ org_list_instances = _normalize_org_list_instances_argv(stripped)
1743
+ if org_list_instances is not None:
1744
+ return org_list_instances
1729
1745
  auth_normalized = _normalize_auth_argv(stripped)
1730
1746
  if auth_normalized is not None:
1731
1747
  return auth_normalized
1732
1748
  return stripped
1733
1749
 
1734
1750
 
1735
- def _global_prefix(argv: list[str]) -> list[str]:
1736
- """Return leading global flags from ``argv`` (preserving order)."""
1737
- out: list[str] = []
1738
- i = 0
1739
- while i < len(argv):
1740
- tok = argv[i]
1741
- if tok in _help.GLOBAL_STORE_TRUE_FLAGS:
1742
- out.append(tok)
1743
- i += 1
1744
- continue
1745
- if tok in _help.GLOBAL_OPTION_FLAGS:
1746
- out.append(tok)
1747
- if i + 1 < len(argv) and not argv[i + 1].startswith("-"):
1748
- out.append(argv[i + 1])
1749
- i += 2
1750
- else:
1751
- i += 1
1752
- continue
1753
- break
1754
- return out
1755
-
1756
-
1757
1751
  def _parse_argv(argv: list[str]) -> list[str]:
1758
- """``argv`` for :func:`argparse.ArgumentParser.parse_args` — globals + normalized command."""
1752
+ """``argv`` for `argparse.ArgumentParser.parse_args` — globals + normalized command."""
1759
1753
  globals_part, stripped = _help.collect_global_flags(argv)
1760
1754
  command = _normalize_argv(stripped)
1761
1755
  if not command:
@@ -1768,12 +1762,13 @@ def _emit_help(
1768
1762
  path: tuple[str, ...],
1769
1763
  *,
1770
1764
  machine_json: bool,
1771
- ) -> None:
1765
+ ) -> dict[str, Any]:
1772
1766
  doc = _help.build_help_document(parser, path)
1773
1767
  if machine_json:
1774
1768
  emit(CliOutput(OutputKind.HELP, doc), force_json=True)
1775
1769
  else:
1776
1770
  _help.render_help_document(doc)
1771
+ return doc
1777
1772
 
1778
1773
 
1779
1774
  def main(argv: list[str] | None = None) -> int:
@@ -1786,7 +1781,9 @@ def main(argv: list[str] | None = None) -> int:
1786
1781
  return EXIT_OK
1787
1782
  help_path = _help.help_target(command)
1788
1783
  if help_path is not None:
1789
- _emit_help(parser, help_path, machine_json=machine_json)
1784
+ doc = _emit_help(parser, help_path, machine_json=machine_json)
1785
+ if doc.get("kind") == "unknown":
1786
+ return EXIT_USAGE
1790
1787
  return EXIT_OK
1791
1788
  try:
1792
1789
  args = parser.parse_args(_parse_argv(argv_list))
@@ -1809,7 +1806,11 @@ def main(argv: list[str] | None = None) -> int:
1809
1806
  except UsageError as exc:
1810
1807
  # Pre-flight resolution errors (missing key, malformed rc, …) still go
1811
1808
  # through emit() so machine mode gets a single JSON ERROR document.
1812
- emit(_with_cli_warnings(exc.output, args, force_json=machine_json or args.json), force_json=machine_json or args.json)
1809
+ force_json = machine_json or args.json
1810
+ emit(
1811
+ _with_cli_warnings(exc.output, args, force_json=force_json),
1812
+ force_json=force_json,
1813
+ )
1813
1814
  return exc.exit_code
1814
1815
  except KeyboardInterrupt:
1815
1816
  if machine_json or args.json:
File without changes
File without changes
File without changes
File without changes
File without changes