healthcloud-cli 3.7.1__tar.gz → 3.7.2__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.
Files changed (92) hide show
  1. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/PKG-INFO +65 -3
  2. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/README.md +64 -2
  3. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/errors.py +2 -15
  4. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/pretty.py +128 -1
  5. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/cli.py +17 -9
  6. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/contracts/mcp-tools.manifest.json +533 -3
  7. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/output.py +13 -0
  8. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/pyproject.toml +1 -1
  9. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/.gitignore +0 -0
  10. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/hatch_build.py +0 -0
  11. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/__init__.py +0 -0
  12. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/admins/__init__.py +0 -0
  13. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/admins/commands.py +0 -0
  14. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/admins/group.py +0 -0
  15. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/admins/operations.py +0 -0
  16. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/__init__.py +0 -0
  17. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/agents/__init__.py +0 -0
  18. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/agents/commands.py +0 -0
  19. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/agents/group.py +0 -0
  20. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/agents/operations.py +0 -0
  21. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/agents/projections.py +0 -0
  22. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/auth.py +0 -0
  23. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/commands.py +0 -0
  24. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/compatibility.py +0 -0
  25. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/context.py +0 -0
  26. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
  27. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/data_sources/commands.py +0 -0
  28. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/data_sources/group.py +0 -0
  29. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/data_sources/operations.py +0 -0
  30. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/data_sources/projections.py +0 -0
  31. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/departments/__init__.py +0 -0
  32. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/departments/commands.py +0 -0
  33. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/departments/group.py +0 -0
  34. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/departments/operations.py +0 -0
  35. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/departments/projections.py +0 -0
  36. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/domain_errors.py +0 -0
  37. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/group.py +0 -0
  38. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
  39. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/guardrails/commands.py +0 -0
  40. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/guardrails/group.py +0 -0
  41. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/guardrails/operations.py +0 -0
  42. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/guardrails/projections.py +0 -0
  43. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/organization/__init__.py +0 -0
  44. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/organization/commands.py +0 -0
  45. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/organization/group.py +0 -0
  46. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/organization/operations.py +0 -0
  47. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/organization/projections.py +0 -0
  48. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/output.py +0 -0
  49. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/policies/__init__.py +0 -0
  50. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/policies/commands.py +0 -0
  51. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/policies/group.py +0 -0
  52. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/policies/operations.py +0 -0
  53. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/policies/projections.py +0 -0
  54. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/registry.py +0 -0
  55. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/results.py +0 -0
  56. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/session.py +0 -0
  57. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/sessions/__init__.py +0 -0
  58. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/sessions/commands.py +0 -0
  59. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/sessions/group.py +0 -0
  60. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/sessions/operations.py +0 -0
  61. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/sessions/projections.py +0 -0
  62. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/skills/__init__.py +0 -0
  63. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/skills/commands.py +0 -0
  64. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/skills/group.py +0 -0
  65. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/skills/operations.py +0 -0
  66. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/skills/projections.py +0 -0
  67. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/target.py +0 -0
  68. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/teams/__init__.py +0 -0
  69. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/teams/commands.py +0 -0
  70. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/teams/group.py +0 -0
  71. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/teams/operations.py +0 -0
  72. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/teams/projections.py +0 -0
  73. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/tools/__init__.py +0 -0
  74. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/tools/commands.py +0 -0
  75. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/tools/group.py +0 -0
  76. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/tools/operations.py +0 -0
  77. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/tools/projections.py +0 -0
  78. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/tuning.py +0 -0
  79. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/ui.py +0 -0
  80. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/ams/windows_files.py +0 -0
  81. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/context.py +0 -0
  82. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/contracts/__init__.py +0 -0
  83. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/main.py +0 -0
  84. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/manifest.py +0 -0
  85. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/params.py +0 -0
  86. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/tenants/__init__.py +0 -0
  87. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/tenants/commands.py +0 -0
  88. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/tenants/group.py +0 -0
  89. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/tenants/operations.py +0 -0
  90. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/healthcloud_cli/util.py +0 -0
  91. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/scripts/__init__.py +0 -0
  92. {healthcloud_cli-3.7.1 → healthcloud_cli-3.7.2}/scripts/bundle_manifest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: healthcloud-cli
3
- Version: 3.7.1
3
+ Version: 3.7.2
4
4
  Summary: Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest.
5
5
  Project-URL: Issues, https://github.com/healthcloudservices/healthcloud-services/issues
6
6
  Project-URL: Repository, https://github.com/healthcloudservices/healthcloud-services
@@ -316,6 +316,27 @@ otherwise it prompts (with hidden password input). It stores only the email,
316
316
  tenant/environment binding, access/refresh tokens and expiry in the strict
317
317
  owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
318
318
 
319
+ By default (`pretty`) these three commands print readable lines — never a raw
320
+ JSON blob and never a token:
321
+
322
+ ```text
323
+ $ hc login --environment dev --tenant-id TENANT_ID
324
+ saved session: dev / TENANT_ID
325
+ email: admin@example.test
326
+ expires_at: 2026-11-02T01:42:29Z
327
+ $ hc logout
328
+ saved session removed: true
329
+ server session revoked: false
330
+ ```
331
+
332
+ `hc login --status` prints the same three lines for the saved session, and a
333
+ second `hc logout` reports `saved session removed: false`. `--output json` (and
334
+ `--output raw`, which these commands treat as JSON) prints exactly the objects
335
+ they have always printed: `{"schema_version": 1, "ok": true, "session":
336
+ {"schema_version": 1, "environment", "tenant_id", "email", "expires_at"}}`
337
+ for `login`/`--status`, and `{"schema_version": 1, "ok": true, "removed":
338
+ true|false, "server_session_revoked": false}` for `logout`.
339
+
319
340
  For CI and agents, log in non-interactively: pass `--email` and pipe the password
320
341
  to `--password-stdin` (read as exactly one stdin line, never echoed, never a
321
342
  flag value — there is no `--password` option):
@@ -545,6 +566,16 @@ explicitly that
545
566
  fields — a disabled agent can still be selected by those paths — and that
546
567
  in-flight runs, WSS sessions and schedules are untouched by this route.
547
568
 
569
+ Default `pretty` output prints those facts as labelled lines — `transition`,
570
+ `requested`, `evidence` (`saved`, `side_effects`, `runtime`), `eligibility`,
571
+ `mutation_replayed: false` — and closes with a `verification:` block, so a
572
+ deactivate reads `transition: deactivate`, then
573
+ `evidence: / saved: / status: confirmed`, then `verification: / saved_state:
574
+ confirmed`, `side_effects: unverified`, `runtime: unverified`; it is never one
575
+ raw JSON line. `--output json` prints the full envelope above, byte-identical to
576
+ before. An unknown outcome still prints its one-line `error:` plus the
577
+ reconciliation on stderr.
578
+
548
579
  Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
549
580
  usage error, before login), an agent outside the explicit tenant, an already
550
581
  in-effect state, an unsupported `archived`/`draft` target (named as having no
@@ -615,6 +646,12 @@ server audit log records an `UPDATE` with the payload. Runtime enforcement of
615
646
  the new references is unobservable: no serving readback route exists, so this
616
647
  command never claims it.
617
648
 
649
+ Default `pretty` output prints the same facts as labelled lines: `agent_id`,
650
+ `changed` (each field with `before`/`after`/`added`/`removed`),
651
+ `config_version: {before, after}`, `evidence` (`saved`, `side_effects`,
652
+ `runtime`), `mutation_replayed: false`, and a closing `verification:` block.
653
+ `--output json` prints the full envelope described here, unchanged.
654
+
618
655
  `tool_ids` and `data_source_ids` address **unscoped global catalogs** by design;
619
656
  the server checks tenant scope only for the department, team, skill, policy and
620
657
  guardrail references. This command does not imply otherwise, and it does not
@@ -689,6 +726,13 @@ of restore and has no `--force`, no `--dry-run`, no interactive prompt and no
689
726
  `--expected-config-version` — the route has no `If-Match` or version check, so a
690
727
  version flag would advertise protection the server does not offer.
691
728
 
729
+ Default `pretty` output prints the result as labelled lines next to the stderr
730
+ disclosure: `agent_id`, `deleted: true`, `target`, `disclosure`, `evidence`
731
+ (`deleted`/`side_effects`/`runtime`), `mutation_replayed: false`, and a closing
732
+ `verification:` block. `--output json` prints the full envelope, unchanged. The
733
+ reviewed target and disclosure above are still printed to stderr in `pretty`
734
+ mode only; `--output json`/`raw` keep stderr machine-parsable.
735
+
692
736
  Refusals claim nothing and send nothing extra: a missing `--confirm` is an exit
693
737
  2 usage error before login or any request; an agent outside the explicit tenant,
694
738
  the tenant identity mismatch and a delete target that no longer exists are
@@ -997,6 +1041,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
997
1041
  "continuation": null}`). `get` emits `operation: "sessions.get"` and
998
1042
  `data.session`. The projections never include the session's protected
999
1043
  `initial_prompt`, tenant or user identity, or arbitrary response fields.
1044
+ - Default `pretty` output prints `list` as a table with honest headers —
1045
+ `ID`, `AGENT ID`, `OPERATION`, `STATUS`, `EXPIRES AT` for sessions and `ID`,
1046
+ `ROLE`, `MODE`, `PLAN ID`, `CREATED AT` for messages — followed by an entry
1047
+ count (`1 entry` / `N entries`), and `(no entries)` for an empty list. These
1048
+ projections carry no `name`, so no `NAME` column is invented and the `id` is
1049
+ always shown. `get`, `create` and `messages add` print `key: value` lines.
1050
+ `--output json` prints the full envelope exactly as before; pretty output is
1051
+ additive and never changes the envelope.
1000
1052
  - `create` and `messages add` require a saved `hc login` session (env-var
1001
1053
  credentials are not accepted for writes). Each write is sent exactly once and
1002
1054
  the envelope carries the server's authoritative readback:
@@ -1169,6 +1221,12 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
1169
1221
  (create: reference counts by kind) and finding counts. No prompt,
1170
1222
  instructions, greeting, summary, message content or plan value is printed to
1171
1223
  stdout, stderr, a diagnostic or an error string.
1224
+ - Default `pretty` output renders `generate`, `get`, `apply`, `rollback` and
1225
+ `reconcile` as labelled lines — `plan:` with the bounded metadata, then
1226
+ `evidence:` (`saved`/`side_effects`/`runtime`) and `mutation_replayed: false`
1227
+ where the command reports them, and a closing `verification:` block for the
1228
+ writes. A write is never printed as a raw JSON blob in the default mode.
1229
+ `--output json` prints the full envelope, unchanged.
1172
1230
  - The prompt is protected free text and never comes from argv: `--prompt-file`
1173
1231
  reads one owner-only regular text file (no symlinks, at most 20000
1174
1232
  characters) and sends it once. `--prompt-file` is optional — the server falls
@@ -1423,8 +1481,12 @@ and `hc ams organization hierarchy`) render in two modes (`--output` on the root
1423
1481
  `hc` command; `pretty` is the default):
1424
1482
 
1425
1483
  - **`pretty`** (default): a compact human view. Lists print an `ID`/`NAME`
1426
- table with an entry count; a single record prints `key: value` lines; the
1427
- organization hierarchy prints an indented tree. Each failed AMS command
1484
+ table with an entry count; a collection whose rows have no `name` (sessions,
1485
+ messages, attachments) prints a table of that command's own columns plus the
1486
+ count, and `(no entries)` when empty. A single record prints `key: value`
1487
+ lines, the organization hierarchy prints an indented tree, and write or
1488
+ reconciliation evidence prints labelled `key: value` lines (the write's
1489
+ `verification:` block included) instead of JSON. Each failed AMS command
1428
1490
  prints one stderr line, `error: <message> (<category>)` (flag/argument
1429
1491
  usage errors from Click print Click's standard usage message instead).
1430
1492
  - **`json`**: the stable result envelope (below) on stdout. When stdout is not
@@ -298,6 +298,27 @@ otherwise it prompts (with hidden password input). It stores only the email,
298
298
  tenant/environment binding, access/refresh tokens and expiry in the strict
299
299
  owner-only `~/.healthcloud/cli/ams-session.json`; it never stores the password.
300
300
 
301
+ By default (`pretty`) these three commands print readable lines — never a raw
302
+ JSON blob and never a token:
303
+
304
+ ```text
305
+ $ hc login --environment dev --tenant-id TENANT_ID
306
+ saved session: dev / TENANT_ID
307
+ email: admin@example.test
308
+ expires_at: 2026-11-02T01:42:29Z
309
+ $ hc logout
310
+ saved session removed: true
311
+ server session revoked: false
312
+ ```
313
+
314
+ `hc login --status` prints the same three lines for the saved session, and a
315
+ second `hc logout` reports `saved session removed: false`. `--output json` (and
316
+ `--output raw`, which these commands treat as JSON) prints exactly the objects
317
+ they have always printed: `{"schema_version": 1, "ok": true, "session":
318
+ {"schema_version": 1, "environment", "tenant_id", "email", "expires_at"}}`
319
+ for `login`/`--status`, and `{"schema_version": 1, "ok": true, "removed":
320
+ true|false, "server_session_revoked": false}` for `logout`.
321
+
301
322
  For CI and agents, log in non-interactively: pass `--email` and pipe the password
302
323
  to `--password-stdin` (read as exactly one stdin line, never echoed, never a
303
324
  flag value — there is no `--password` option):
@@ -527,6 +548,16 @@ explicitly that
527
548
  fields — a disabled agent can still be selected by those paths — and that
528
549
  in-flight runs, WSS sessions and schedules are untouched by this route.
529
550
 
551
+ Default `pretty` output prints those facts as labelled lines — `transition`,
552
+ `requested`, `evidence` (`saved`, `side_effects`, `runtime`), `eligibility`,
553
+ `mutation_replayed: false` — and closes with a `verification:` block, so a
554
+ deactivate reads `transition: deactivate`, then
555
+ `evidence: / saved: / status: confirmed`, then `verification: / saved_state:
556
+ confirmed`, `side_effects: unverified`, `runtime: unverified`; it is never one
557
+ raw JSON line. `--output json` prints the full envelope above, byte-identical to
558
+ before. An unknown outcome still prints its one-line `error:` plus the
559
+ reconciliation on stderr.
560
+
530
561
  Pre-checks refuse with exit 1 and **no write**: a missing `--confirm` (exit 2
531
562
  usage error, before login), an agent outside the explicit tenant, an already
532
563
  in-effect state, an unsupported `archived`/`draft` target (named as having no
@@ -597,6 +628,12 @@ server audit log records an `UPDATE` with the payload. Runtime enforcement of
597
628
  the new references is unobservable: no serving readback route exists, so this
598
629
  command never claims it.
599
630
 
631
+ Default `pretty` output prints the same facts as labelled lines: `agent_id`,
632
+ `changed` (each field with `before`/`after`/`added`/`removed`),
633
+ `config_version: {before, after}`, `evidence` (`saved`, `side_effects`,
634
+ `runtime`), `mutation_replayed: false`, and a closing `verification:` block.
635
+ `--output json` prints the full envelope described here, unchanged.
636
+
600
637
  `tool_ids` and `data_source_ids` address **unscoped global catalogs** by design;
601
638
  the server checks tenant scope only for the department, team, skill, policy and
602
639
  guardrail references. This command does not imply otherwise, and it does not
@@ -671,6 +708,13 @@ of restore and has no `--force`, no `--dry-run`, no interactive prompt and no
671
708
  `--expected-config-version` — the route has no `If-Match` or version check, so a
672
709
  version flag would advertise protection the server does not offer.
673
710
 
711
+ Default `pretty` output prints the result as labelled lines next to the stderr
712
+ disclosure: `agent_id`, `deleted: true`, `target`, `disclosure`, `evidence`
713
+ (`deleted`/`side_effects`/`runtime`), `mutation_replayed: false`, and a closing
714
+ `verification:` block. `--output json` prints the full envelope, unchanged. The
715
+ reviewed target and disclosure above are still printed to stderr in `pretty`
716
+ mode only; `--output json`/`raw` keep stderr machine-parsable.
717
+
674
718
  Refusals claim nothing and send nothing extra: a missing `--confirm` is an exit
675
719
  2 usage error before login or any request; an agent outside the explicit tenant,
676
720
  the tenant identity mismatch and a delete target that no longer exists are
@@ -979,6 +1023,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
979
1023
  "continuation": null}`). `get` emits `operation: "sessions.get"` and
980
1024
  `data.session`. The projections never include the session's protected
981
1025
  `initial_prompt`, tenant or user identity, or arbitrary response fields.
1026
+ - Default `pretty` output prints `list` as a table with honest headers —
1027
+ `ID`, `AGENT ID`, `OPERATION`, `STATUS`, `EXPIRES AT` for sessions and `ID`,
1028
+ `ROLE`, `MODE`, `PLAN ID`, `CREATED AT` for messages — followed by an entry
1029
+ count (`1 entry` / `N entries`), and `(no entries)` for an empty list. These
1030
+ projections carry no `name`, so no `NAME` column is invented and the `id` is
1031
+ always shown. `get`, `create` and `messages add` print `key: value` lines.
1032
+ `--output json` prints the full envelope exactly as before; pretty output is
1033
+ additive and never changes the envelope.
982
1034
  - `create` and `messages add` require a saved `hc login` session (env-var
983
1035
  credentials are not accepted for writes). Each write is sent exactly once and
984
1036
  the envelope carries the server's authoritative readback:
@@ -1151,6 +1203,12 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
1151
1203
  (create: reference counts by kind) and finding counts. No prompt,
1152
1204
  instructions, greeting, summary, message content or plan value is printed to
1153
1205
  stdout, stderr, a diagnostic or an error string.
1206
+ - Default `pretty` output renders `generate`, `get`, `apply`, `rollback` and
1207
+ `reconcile` as labelled lines — `plan:` with the bounded metadata, then
1208
+ `evidence:` (`saved`/`side_effects`/`runtime`) and `mutation_replayed: false`
1209
+ where the command reports them, and a closing `verification:` block for the
1210
+ writes. A write is never printed as a raw JSON blob in the default mode.
1211
+ `--output json` prints the full envelope, unchanged.
1154
1212
  - The prompt is protected free text and never comes from argv: `--prompt-file`
1155
1213
  reads one owner-only regular text file (no symlinks, at most 20000
1156
1214
  characters) and sends it once. `--prompt-file` is optional — the server falls
@@ -1405,8 +1463,12 @@ and `hc ams organization hierarchy`) render in two modes (`--output` on the root
1405
1463
  `hc` command; `pretty` is the default):
1406
1464
 
1407
1465
  - **`pretty`** (default): a compact human view. Lists print an `ID`/`NAME`
1408
- table with an entry count; a single record prints `key: value` lines; the
1409
- organization hierarchy prints an indented tree. Each failed AMS command
1466
+ table with an entry count; a collection whose rows have no `name` (sessions,
1467
+ messages, attachments) prints a table of that command's own columns plus the
1468
+ count, and `(no entries)` when empty. A single record prints `key: value`
1469
+ lines, the organization hierarchy prints an indented tree, and write or
1470
+ reconciliation evidence prints labelled `key: value` lines (the write's
1471
+ `verification:` block included) instead of JSON. Each failed AMS command
1410
1472
  prints one stderr line, `error: <message> (<category>)` (flag/argument
1411
1473
  usage errors from Click print Click's standard usage message instead).
1412
1474
  - **`json`**: the stable result envelope (below) on stdout. When stdout is not
@@ -10,7 +10,7 @@ from typing import Any
10
10
  import click
11
11
  from pydantic import ValidationError
12
12
 
13
- from ..context import CliConfig
13
+ from ..output import output_mode
14
14
  from .domain_errors import DomainReadRefused
15
15
  from .pretty import error_line, success_text, unknown_details
16
16
  from .results import failed
@@ -135,19 +135,6 @@ def emit_compact_json(value: dict, *, err: bool = False) -> None:
135
135
  click.echo(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True), err=err)
136
136
 
137
137
 
138
- def _output_mode() -> str:
139
- """The root ``--output`` mode for the running command; pretty is the default.
140
-
141
- Raw is refused for AMS before any read runs, but its refusals keep the
142
- structured JSON error envelope, so raw renders like json.
143
- """
144
-
145
- ctx = click.get_current_context(silent=True)
146
- cfg = ctx.find_object(CliConfig) if ctx is not None else None
147
- mode = getattr(cfg, "output", None)
148
- return "json" if mode in ("json", "raw") else "pretty"
149
-
150
-
151
138
  def _render_success(envelope: dict, mode: str) -> None:
152
139
  if mode != "json":
153
140
  click.echo(success_text(envelope))
@@ -190,7 +177,7 @@ def run_domain(operation: str, target: dict, callback: Callable[[], Any], *, rea
190
177
  ``verification`` block in the error envelope.
191
178
  """
192
179
 
193
- mode = _output_mode()
180
+ mode = output_mode()
194
181
 
195
182
  def _fail(error: BaseException) -> None:
196
183
  """Classify ``error``, render the error envelope, and exit bounded."""
@@ -107,13 +107,109 @@ def _is_list(data: dict) -> bool:
107
107
  )
108
108
 
109
109
 
110
+ # A collection whose rows carry an ``id`` but no ``name`` (sessions, messages,
111
+ # attachments) still renders as a table; these are the honest columns per
112
+ # collection. A collection without a spec here falls back to ``id`` plus the
113
+ # scalar keys every row shares, so no column is invented.
114
+ _LIST_FIELDS = {
115
+ "sessions": ("id", "agent_id", "operation", "status", "expires_at"),
116
+ "messages": ("id", "role", "mode", "plan_id", "created_at"),
117
+ "attachments": ("id", "filename", "content_type", "size_bytes", "index_status"),
118
+ }
119
+
120
+
110
121
  def _is_item(data: dict) -> bool:
111
122
  values = list(data.values())
112
123
  return len(data) == 1 and isinstance(values[0], dict)
113
124
 
114
125
 
126
+ def _id_list(data: dict) -> tuple[str, list[dict]] | None:
127
+ """The ``(collection, rows)`` of a list whose rows have an ``id`` only.
128
+
129
+ Catalog lists already match ``_is_list`` (they add a ``name``); this covers
130
+ the projections that do not name their row (sessions, messages, ...) without
131
+ loosening ``_is_list`` and changing existing catalog output.
132
+ """
133
+
134
+ collections = [(key, value) for key, value in data.items() if key != "page"]
135
+ if len(collections) != 1:
136
+ return None
137
+ key, rows = collections[0]
138
+ if not isinstance(rows, list):
139
+ return None
140
+ if not all(isinstance(row, dict) and isinstance(row.get("id"), str) for row in rows):
141
+ return None
142
+ return key, rows
143
+
144
+
145
+ def _columns(collection: str, rows: list[dict]) -> tuple[str, ...]:
146
+ fields = _LIST_FIELDS.get(collection)
147
+ if fields is not None:
148
+ return fields
149
+ shared = set(rows[0])
150
+ for row in rows[1:]:
151
+ shared &= set(row)
152
+ return ("id", *sorted(shared - {"id"}))
153
+
154
+
155
+ def _columns_table_lines(rows: list[dict], fields: tuple[str, ...]) -> list[str]:
156
+ headers = [field.replace("_", " ").upper() for field in fields]
157
+ cells = [[_scalar(row.get(field)) for field in fields] for row in rows]
158
+ widths = [max(len(headers[index]), *(len(cell[index]) for cell in cells)) for index in range(len(fields))]
159
+
160
+ def _line(values: list[str]) -> str:
161
+ # The trailing column is never padded: no trailing whitespace.
162
+ if len(values) == 1:
163
+ return values[0]
164
+ padded = [value.ljust(width) for value, width in zip(values[:-1], widths[:-1], strict=True)]
165
+ return " ".join(padded) + " " + values[-1]
166
+
167
+ lines = [_line(headers)]
168
+ lines.extend(_line(cell) for cell in cells)
169
+ count = len(rows)
170
+ lines.append("")
171
+ lines.append(f"{count} entry" if count == 1 else f"{count} entries")
172
+ return lines
173
+
174
+
175
+ # Domains whose multi-key ``data`` is write or reconciliation evidence. A
176
+ # non-AMS envelope (``admins.*``, ``tenants.*``) keeps the compact-JSON fallback:
177
+ # their pretty output is a separate story, not this one's to change.
178
+ _EVIDENCE_DOMAINS = frozenset(
179
+ {
180
+ "agents",
181
+ "data-sources",
182
+ "departments",
183
+ "guardrails",
184
+ "organization",
185
+ "policies",
186
+ "sessions",
187
+ "skills",
188
+ "teams",
189
+ "tools",
190
+ }
191
+ )
192
+
193
+
194
+ def _is_evidence(envelope: dict, data: dict) -> bool:
195
+ operation = envelope.get("operation")
196
+ if not isinstance(operation, str) or operation.split(".", 1)[0] not in _EVIDENCE_DOMAINS:
197
+ return False
198
+ return len([key for key in data if key != "page"]) > 1
199
+
200
+
201
+ def _evidence_lines(envelope: dict, data: dict) -> list[str]:
202
+ lines = _detail_lines(data)
203
+ verification = envelope.get("verification")
204
+ if isinstance(verification, dict) and verification:
205
+ lines.append("")
206
+ lines.append("verification:")
207
+ lines.extend(_detail_lines(verification, 1))
208
+ return lines
209
+
210
+
115
211
  def success_text(envelope: dict) -> str:
116
- """Render a succeeded read envelope as compact human text."""
212
+ """Render a succeeded envelope as compact human text."""
117
213
  data = envelope.get("data")
118
214
  if not isinstance(data, dict) or not data:
119
215
  return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
@@ -123,11 +219,42 @@ def success_text(envelope: dict) -> str:
123
219
  if _is_list(data):
124
220
  rows = [value for key, value in data.items() if key != "page"][0]
125
221
  return "\n".join(_table_lines(rows)) if rows else "(no entries)"
222
+ identified = _id_list(data)
223
+ if identified is not None:
224
+ collection, rows = identified
225
+ if not rows:
226
+ return "(no entries)"
227
+ return "\n".join(_columns_table_lines(rows, _columns(collection, rows)))
126
228
  if _is_item(data):
127
229
  return "\n".join(_item_lines(list(data.values())[0]))
230
+ if _is_evidence(envelope, data):
231
+ return "\n".join(_evidence_lines(envelope, data))
128
232
  return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
129
233
 
130
234
 
235
+ _SESSION_FIELDS = ("email", "expires_at")
236
+
237
+
238
+ def saved_session_text(payload: dict) -> str:
239
+ """Pretty text for ``hc login`` and ``hc login --status``; never a token."""
240
+ session = payload.get("session")
241
+ if not isinstance(session, dict):
242
+ return "(no saved session)"
243
+ lines = [f"saved session: {_value(session.get('environment'))} / {_value(session.get('tenant_id'))}"]
244
+ lines.extend(f"{field}: {_value(session[field])}" for field in _SESSION_FIELDS if field in session)
245
+ return "\n".join(lines)
246
+
247
+
248
+ def logout_text(payload: dict) -> str:
249
+ """Pretty text for ``hc logout``: what was removed, and what was not undone."""
250
+ removed = payload.get("removed") is True
251
+ lines = [f"saved session removed: {'true' if removed else 'false'}"]
252
+ if isinstance(payload.get("server_session_revoked"), bool):
253
+ revoked = payload["server_session_revoked"]
254
+ lines.append(f"server session revoked: {'true' if revoked else 'false'}")
255
+ return "\n".join(lines)
256
+
257
+
131
258
  def error_line(category: str, message: str) -> str:
132
259
  """One-line stderr rendering of a failed read envelope."""
133
260
  return f"error: {_text(message)} ({_text(category)})"
@@ -23,9 +23,10 @@ import click
23
23
  from . import manifest as manifest_module
24
24
  from .admins import create_admins_group
25
25
  from .ams.group import create_ams_group
26
+ from .ams.pretty import logout_text, saved_session_text
26
27
  from .ams.session import delete_session, load_session, new_session, save_session
27
28
  from .context import CliConfig, resolve_config
28
- from .output import dispatch, dump_rpc_result, read_stdin
29
+ from .output import dispatch, dump_rpc_result, output_mode, read_stdin
29
30
  from .params import ParamSpec, build_param_specs, make_click_option, merge_arguments, validate_final
30
31
  from .tenants import create_tenants_group
31
32
  from .util import (
@@ -332,7 +333,7 @@ def login_cmd(
332
333
  session = load_session()
333
334
  if session is None:
334
335
  raise click.ClickException("No saved AMS session; run hc login.")
335
- click.echo(json.dumps({"schema_version": 1, "ok": True, "session": session.public_value()}, sort_keys=True))
336
+ _emit_session({"schema_version": 1, "ok": True, "session": session.public_value()})
336
337
  return
337
338
  if environment is None or tenant_id is None or not tenant_id.strip():
338
339
  raise click.UsageError("Login requires --environment and a nonblank --tenant-id.")
@@ -377,19 +378,26 @@ def login_cmd(
377
378
  raise
378
379
  except Exception:
379
380
  raise click.ClickException("Tenant-admin login failed; no session was saved.") from None
380
- click.echo(json.dumps({"schema_version": 1, "ok": True, "session": session.public_value()}, sort_keys=True))
381
+ _emit_session({"schema_version": 1, "ok": True, "session": session.public_value()})
382
+
383
+
384
+ def _emit_session(payload: dict) -> None:
385
+ """Render the saved-session object: readable text by default, today's JSON for json."""
386
+ if output_mode() == "pretty":
387
+ click.echo(saved_session_text(payload))
388
+ return
389
+ click.echo(json.dumps(payload, sort_keys=True))
381
390
 
382
391
 
383
392
  @click.command("logout")
384
393
  def logout_cmd() -> None:
385
394
  """Remove the validated local tenant-admin session (local only; tokens stay valid until expiry)."""
386
395
  removed = delete_session()
387
- click.echo(
388
- json.dumps(
389
- {"schema_version": 1, "ok": True, "removed": removed, "server_session_revoked": False},
390
- sort_keys=True,
391
- )
392
- )
396
+ payload = {"schema_version": 1, "ok": True, "removed": removed, "server_session_revoked": False}
397
+ if output_mode() == "pretty":
398
+ click.echo(logout_text(payload))
399
+ return
400
+ click.echo(json.dumps(payload, sort_keys=True))
393
401
 
394
402
 
395
403
  hc.add_command(login_cmd)