healthcloud-cli 3.7.5__tar.gz → 3.7.7__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.5 → healthcloud_cli-3.7.7}/PKG-INFO +38 -13
  2. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/README.md +37 -12
  3. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/context.py +25 -9
  4. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/sessions/commands.py +2 -2
  5. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/sessions/operations.py +80 -22
  6. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/pyproject.toml +1 -1
  7. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/.gitignore +0 -0
  8. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/hatch_build.py +0 -0
  9. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/__init__.py +0 -0
  10. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/admins/__init__.py +0 -0
  11. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/admins/commands.py +0 -0
  12. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/admins/group.py +0 -0
  13. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/admins/operations.py +0 -0
  14. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/__init__.py +0 -0
  15. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/agents/__init__.py +0 -0
  16. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/agents/commands.py +0 -0
  17. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/agents/group.py +0 -0
  18. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/agents/operations.py +0 -0
  19. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/agents/projections.py +0 -0
  20. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/auth.py +0 -0
  21. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/commands.py +0 -0
  22. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/compatibility.py +0 -0
  23. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
  24. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/data_sources/commands.py +0 -0
  25. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/data_sources/group.py +0 -0
  26. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/data_sources/operations.py +0 -0
  27. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/data_sources/projections.py +0 -0
  28. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/departments/__init__.py +0 -0
  29. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/departments/commands.py +0 -0
  30. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/departments/group.py +0 -0
  31. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/departments/operations.py +0 -0
  32. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/departments/projections.py +0 -0
  33. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/domain_errors.py +0 -0
  34. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/errors.py +0 -0
  35. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/group.py +0 -0
  36. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
  37. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/guardrails/commands.py +0 -0
  38. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/guardrails/group.py +0 -0
  39. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/guardrails/operations.py +0 -0
  40. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/guardrails/projections.py +0 -0
  41. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/organization/__init__.py +0 -0
  42. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/organization/commands.py +0 -0
  43. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/organization/group.py +0 -0
  44. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/organization/operations.py +0 -0
  45. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/organization/projections.py +0 -0
  46. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/output.py +0 -0
  47. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/policies/__init__.py +0 -0
  48. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/policies/commands.py +0 -0
  49. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/policies/group.py +0 -0
  50. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/policies/operations.py +0 -0
  51. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/policies/projections.py +0 -0
  52. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/pretty.py +0 -0
  53. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/registry.py +0 -0
  54. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/results.py +0 -0
  55. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/session.py +0 -0
  56. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/sessions/__init__.py +0 -0
  57. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/sessions/group.py +0 -0
  58. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/sessions/projections.py +0 -0
  59. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/skills/__init__.py +0 -0
  60. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/skills/commands.py +0 -0
  61. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/skills/group.py +0 -0
  62. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/skills/operations.py +0 -0
  63. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/skills/projections.py +0 -0
  64. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/target.py +0 -0
  65. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/teams/__init__.py +0 -0
  66. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/teams/commands.py +0 -0
  67. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/teams/group.py +0 -0
  68. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/teams/operations.py +0 -0
  69. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/teams/projections.py +0 -0
  70. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/tools/__init__.py +0 -0
  71. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/tools/commands.py +0 -0
  72. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/tools/group.py +0 -0
  73. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/tools/operations.py +0 -0
  74. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/tools/projections.py +0 -0
  75. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/tuning.py +0 -0
  76. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/ui.py +0 -0
  77. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/ams/windows_files.py +0 -0
  78. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/cli.py +0 -0
  79. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/context.py +0 -0
  80. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/contracts/__init__.py +0 -0
  81. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
  82. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/main.py +0 -0
  83. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/manifest.py +0 -0
  84. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/output.py +0 -0
  85. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/params.py +0 -0
  86. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/tenants/__init__.py +0 -0
  87. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/tenants/commands.py +0 -0
  88. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/tenants/group.py +0 -0
  89. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/tenants/operations.py +0 -0
  90. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/healthcloud_cli/util.py +0 -0
  91. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/scripts/__init__.py +0 -0
  92. {healthcloud_cli-3.7.5 → healthcloud_cli-3.7.7}/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.5
3
+ Version: 3.7.7
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
@@ -1082,9 +1082,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1082
1082
 
1083
1083
  - `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
1084
1084
  of session ID, agent ID, operation, mode, status and audit timestamps) and a
1085
- `page` block (`{"kind": "limit", "limit": N, "returned_count": N,
1086
- "continuation": null}`). `get` emits `operation: "sessions.get"` and
1087
- `data.session`. The projections never include the session's protected
1085
+ `page` block. These routes page with a limit only, so the block always says
1086
+ which limit was in force: with `--limit N` it is
1087
+ `{"kind": "limit", "limit": N, "returned_count": N, "continuation": null}`,
1088
+ and without it the server's own default page was applied, which the block
1089
+ reports as `{"kind": "server_default", "server_default_limit": 50,
1090
+ "returned_count": N, "continuation": null}` (50 sessions, 100 messages) — so
1091
+ a truncated page is never mistaken for a complete list. `get` emits
1092
+ `operation: "sessions.get"` and `data.session`. The projections never include the session's protected
1088
1093
  `initial_prompt`, tenant or user identity, or arbitrary response fields.
1089
1094
  - Default `pretty` output prints `list` as a table with honest headers —
1090
1095
  `ID`, `AGENT ID`, `OPERATION`, `STATUS`, `EXPIRES AT` for sessions and `ID`,
@@ -1099,8 +1104,12 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1099
1104
  the envelope carries the server's authoritative readback:
1100
1105
  `operation: "sessions.create"` with `data.session`, and
1101
1106
  `operation: "sessions.messages.add"` with `data.message` (message ID, role,
1102
- mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
1103
- supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
1107
+ mode, plan ID, model, timestamps) plus `data.idempotency_key` — the key the
1108
+ server read back, when the returned message carries one. When the readback
1109
+ does not echo a key, the field is omitted: the envelope never asserts a
1110
+ key→message association the server did not confirm. An `--idempotency-key`
1111
+ longer than 128 characters (or an empty one) is refused locally with a
1112
+ `validation` error naming the flag, before anything is sent. Protected free text — `--initial-prompt` and `--content` — is sent to
1104
1113
  the server but never appears in the envelope, a refusal, a diagnostic or a log.
1105
1114
  - The protected free text can also arrive from an owner-only file, which keeps it
1106
1115
  out of the command line entirely (argv, the process table under
@@ -1129,18 +1138,24 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1129
1138
  - `--initial-prompt` becomes the session's first `user` message in the same
1130
1139
  server transaction; `hc ams sessions messages list` shows it afterwards.
1131
1140
  - `--mode implement` is not available from the server yet: `create` reports one
1132
- `unsupported` refusal (the server answers 501) and creates nothing. A message
1141
+ `unsupported` refusal (the server answers 501) and creates nothing, and a
1142
+ `messages add` the server answers 501 for is the same `unsupported` refusal
1143
+ with no message added. Both refusals report the 501 the server actually sent
1144
+ and name implement mode as the documented reason, rather than asserting the
1145
+ requested mode was the cause. A message
1133
1146
  whose `--mode` differs from its session's mode is refused by the server with
1134
- 409 and reported as one write-worded `conflict` refusal that says a message's
1135
- mode must match its session's mode and points at `hc ams sessions get`; no
1136
- message is added, and the read-era conflict wording is never used.
1147
+ 409 and reported as one write-worded `conflict` refusal that names the
1148
+ session, mentions a mode mismatch as one possible cause, and points at
1149
+ `hc ams sessions get`; no message is added, and the read-era conflict wording
1150
+ is never used.
1137
1151
  - A missing, expired or other user's session is a `not_found` refusal naming the
1138
1152
  session on `get`, `messages list` and `messages add`; on `create`, a 404 names
1139
1153
  the requested `--agent-id`.
1140
1154
  - Messages with an `--idempotency-key` are replay-safe: the server answers 201
1141
1155
  with the existing message instead of inserting a second one, and the command
1142
1156
  reports that message (its original ID) — a replay is never reported as a new
1143
- write, and the command itself never re-sends a write.
1157
+ write, and the command itself never re-sends a write. The envelope echoes the
1158
+ server's read-back key, never a key the server did not confirm.
1144
1159
  - If the transport times out or the connection drops mid-write, the write's
1145
1160
  outcome is unknown: the CLI reports one `transport` error whose message says so
1146
1161
  and points at `hc ams sessions list`/`hc ams sessions messages list` before
@@ -1149,8 +1164,15 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1149
1164
  landed write is never reported as stopped.
1150
1165
  - Reads refuse target files, root credentials or base URLs, raw output, blank or
1151
1166
  dot-segment session IDs, out-of-range limits, and installed SDKs without the
1152
- session list/get and message list/add methods before tenant-admin login or any
1153
- read. The existing `hc ams plan|show|apply|restore` tuning commands and their
1167
+ session list/get and message list methods before tenant-admin login or any
1168
+ read. `messages list` needs only `messages.list` — the write method `add` is
1169
+ not required for the read. The writes check their own methods in the same
1170
+ preflight — each command requires exactly the write method it calls, so
1171
+ `create` refuses an installed SDK without `sessions.create` and `messages add`
1172
+ one without `messages.add` before tenant-admin login and before any read or
1173
+ write, and neither is refused for the write the other command performs. A single-object route whose 2xx body is not a JSON
1174
+ object is refused as a `validation` error, never reported as an internal
1175
+ failure. The existing `hc ams plan|show|apply|restore` tuning commands and their
1154
1176
  signatures are unchanged.
1155
1177
  - These session and message commands do not attach files, apply or roll back
1156
1178
  configuration, or send session history to a model. Plan review is its own
@@ -1241,6 +1263,9 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
1241
1263
  message write, no `PUT` and no apply. The server runs the planner once. It
1242
1264
  requires a saved `hc login` session and reports
1243
1265
  `operation: "sessions.plans.generate"` with `data.plan` and `data.out`.
1266
+ An `--idempotency-key` longer than 128 characters (or an empty one) is
1267
+ refused locally with a `validation` error naming the flag, before anything
1268
+ is sent.
1244
1269
  `get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
1245
1270
  `operation: "sessions.plans.get"`; a reload is immutable, so it returns the
1246
1271
  same `plan_hash` and diff however the session changed afterwards. `get` is a
@@ -1064,9 +1064,14 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1064
1064
 
1065
1065
  - `list` emits `operation: "sessions.list"`, `data.sessions` (bounded projections
1066
1066
  of session ID, agent ID, operation, mode, status and audit timestamps) and a
1067
- `page` block (`{"kind": "limit", "limit": N, "returned_count": N,
1068
- "continuation": null}`). `get` emits `operation: "sessions.get"` and
1069
- `data.session`. The projections never include the session's protected
1067
+ `page` block. These routes page with a limit only, so the block always says
1068
+ which limit was in force: with `--limit N` it is
1069
+ `{"kind": "limit", "limit": N, "returned_count": N, "continuation": null}`,
1070
+ and without it the server's own default page was applied, which the block
1071
+ reports as `{"kind": "server_default", "server_default_limit": 50,
1072
+ "returned_count": N, "continuation": null}` (50 sessions, 100 messages) — so
1073
+ a truncated page is never mistaken for a complete list. `get` emits
1074
+ `operation: "sessions.get"` and `data.session`. The projections never include the session's protected
1070
1075
  `initial_prompt`, tenant or user identity, or arbitrary response fields.
1071
1076
  - Default `pretty` output prints `list` as a table with honest headers —
1072
1077
  `ID`, `AGENT ID`, `OPERATION`, `STATUS`, `EXPIRES AT` for sessions and `ID`,
@@ -1081,8 +1086,12 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1081
1086
  the envelope carries the server's authoritative readback:
1082
1087
  `operation: "sessions.create"` with `data.session`, and
1083
1088
  `operation: "sessions.messages.add"` with `data.message` (message ID, role,
1084
- mode, plan ID, model, timestamps) plus `data.idempotency_key` when a key was
1085
- supplied. Protected free text — `--initial-prompt` and `--content` — is sent to
1089
+ mode, plan ID, model, timestamps) plus `data.idempotency_key` — the key the
1090
+ server read back, when the returned message carries one. When the readback
1091
+ does not echo a key, the field is omitted: the envelope never asserts a
1092
+ key→message association the server did not confirm. An `--idempotency-key`
1093
+ longer than 128 characters (or an empty one) is refused locally with a
1094
+ `validation` error naming the flag, before anything is sent. Protected free text — `--initial-prompt` and `--content` — is sent to
1086
1095
  the server but never appears in the envelope, a refusal, a diagnostic or a log.
1087
1096
  - The protected free text can also arrive from an owner-only file, which keeps it
1088
1097
  out of the command line entirely (argv, the process table under
@@ -1111,18 +1120,24 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1111
1120
  - `--initial-prompt` becomes the session's first `user` message in the same
1112
1121
  server transaction; `hc ams sessions messages list` shows it afterwards.
1113
1122
  - `--mode implement` is not available from the server yet: `create` reports one
1114
- `unsupported` refusal (the server answers 501) and creates nothing. A message
1123
+ `unsupported` refusal (the server answers 501) and creates nothing, and a
1124
+ `messages add` the server answers 501 for is the same `unsupported` refusal
1125
+ with no message added. Both refusals report the 501 the server actually sent
1126
+ and name implement mode as the documented reason, rather than asserting the
1127
+ requested mode was the cause. A message
1115
1128
  whose `--mode` differs from its session's mode is refused by the server with
1116
- 409 and reported as one write-worded `conflict` refusal that says a message's
1117
- mode must match its session's mode and points at `hc ams sessions get`; no
1118
- message is added, and the read-era conflict wording is never used.
1129
+ 409 and reported as one write-worded `conflict` refusal that names the
1130
+ session, mentions a mode mismatch as one possible cause, and points at
1131
+ `hc ams sessions get`; no message is added, and the read-era conflict wording
1132
+ is never used.
1119
1133
  - A missing, expired or other user's session is a `not_found` refusal naming the
1120
1134
  session on `get`, `messages list` and `messages add`; on `create`, a 404 names
1121
1135
  the requested `--agent-id`.
1122
1136
  - Messages with an `--idempotency-key` are replay-safe: the server answers 201
1123
1137
  with the existing message instead of inserting a second one, and the command
1124
1138
  reports that message (its original ID) — a replay is never reported as a new
1125
- write, and the command itself never re-sends a write.
1139
+ write, and the command itself never re-sends a write. The envelope echoes the
1140
+ server's read-back key, never a key the server did not confirm.
1126
1141
  - If the transport times out or the connection drops mid-write, the write's
1127
1142
  outcome is unknown: the CLI reports one `transport` error whose message says so
1128
1143
  and points at `hc ams sessions list`/`hc ams sessions messages list` before
@@ -1131,8 +1146,15 @@ uv run --frozen hc ams sessions messages add --environment dev --tenant-id TENAN
1131
1146
  landed write is never reported as stopped.
1132
1147
  - Reads refuse target files, root credentials or base URLs, raw output, blank or
1133
1148
  dot-segment session IDs, out-of-range limits, and installed SDKs without the
1134
- session list/get and message list/add methods before tenant-admin login or any
1135
- read. The existing `hc ams plan|show|apply|restore` tuning commands and their
1149
+ session list/get and message list methods before tenant-admin login or any
1150
+ read. `messages list` needs only `messages.list` — the write method `add` is
1151
+ not required for the read. The writes check their own methods in the same
1152
+ preflight — each command requires exactly the write method it calls, so
1153
+ `create` refuses an installed SDK without `sessions.create` and `messages add`
1154
+ one without `messages.add` before tenant-admin login and before any read or
1155
+ write, and neither is refused for the write the other command performs. A single-object route whose 2xx body is not a JSON
1156
+ object is refused as a `validation` error, never reported as an internal
1157
+ failure. The existing `hc ams plan|show|apply|restore` tuning commands and their
1136
1158
  signatures are unchanged.
1137
1159
  - These session and message commands do not attach files, apply or roll back
1138
1160
  configuration, or send session history to a model. Plan review is its own
@@ -1223,6 +1245,9 @@ uv run --frozen hc ams sessions plans reconcile --environment dev --tenant-id TE
1223
1245
  message write, no `PUT` and no apply. The server runs the planner once. It
1224
1246
  requires a saved `hc login` session and reports
1225
1247
  `operation: "sessions.plans.generate"` with `data.plan` and `data.out`.
1248
+ An `--idempotency-key` longer than 128 characters (or an empty one) is
1249
+ refused locally with a `validation` error naming the flag, before anything
1250
+ is sent.
1226
1251
  `get` sends exactly one `GET /vibecoder/plans/PLAN_ID` and reports
1227
1252
  `operation: "sessions.plans.get"`; a reload is immutable, so it returns the
1228
1253
  same `plan_hash` and diff however the session changed afterwards. `get` is a
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Callable
5
+ from collections.abc import Callable, Sequence
6
6
  from contextlib import AbstractContextManager
7
7
  from dataclasses import dataclass
8
8
  from pathlib import Path
@@ -236,8 +236,20 @@ def preflight_policies(invocation: AmsContext, target: TenantTarget) -> None:
236
236
  sdk.close()
237
237
 
238
238
 
239
- def preflight_sessions(invocation: AmsContext, target: TenantTarget) -> None:
240
- """Validate local session-read compatibility before login or any read."""
239
+ def preflight_sessions(
240
+ invocation: AmsContext,
241
+ target: TenantTarget,
242
+ *,
243
+ write_methods: Sequence[str] = (),
244
+ ) -> None:
245
+ """Validate local session compatibility before login or any request.
246
+
247
+ ``write_methods`` names exactly the write methods the calling command will
248
+ call, as ``"<resource>.<method>"`` (``sessions.create``, ``messages.add``),
249
+ so a write command is refused here — before tenant-admin login and before any
250
+ read or write — only for a method it actually needs; a write it never
251
+ performs is never required of the installed SDK.
252
+ """
241
253
 
242
254
  _check_root(invocation)
243
255
  if invocation.target_file is not None:
@@ -245,12 +257,16 @@ def preflight_sessions(invocation: AmsContext, target: TenantTarget) -> None:
245
257
  sdk = _public_sdk(target.environment, target.tenant_id)
246
258
  try:
247
259
  resource = getattr(getattr(sdk, "ams_admin", None), "sessions", None)
248
- if (
249
- resource is None
250
- or not callable(getattr(resource, "list", None))
251
- or not callable(getattr(resource, "get", None))
252
- or not callable(getattr(getattr(resource, "messages", None), "list", None))
253
- ):
260
+ messages = getattr(resource, "messages", None)
261
+ resources = {"sessions": resource, "messages": messages}
262
+ required = (resource, "list"), (resource, "get"), (messages, "list")
263
+ missing = any(not callable(getattr(holder, method, None)) for holder, method in required)
264
+ for qualified in write_methods:
265
+ owner, _, method = qualified.partition(".")
266
+ holder = resources.get(owner)
267
+ if not method or not callable(getattr(holder, method, None)):
268
+ missing = True
269
+ if missing:
254
270
  raise click.ClickException(
255
271
  "SDK missing ams_admin sessions; use the reviewed SDK version required by this CLI."
256
272
  )
@@ -125,7 +125,7 @@ def create_create_command(dependencies: AmsDependencies) -> click.Command:
125
125
  _require_login()
126
126
  target = resolve_tenant_target(environment, tenant_id)
127
127
  target_data.update(environment=target.environment, tenant_id=target.tenant_id)
128
- preflight_sessions(invocation, target)
128
+ preflight_sessions(invocation, target, write_methods=("sessions.create",))
129
129
  return create_session(
130
130
  dependencies,
131
131
  target,
@@ -561,7 +561,7 @@ def create_messages_add_command(dependencies: AmsDependencies) -> click.Command:
561
561
  _require_login()
562
562
  target = resolve_tenant_target(environment, tenant_id)
563
563
  target_data.update(environment=target.environment, tenant_id=target.tenant_id)
564
- preflight_sessions(invocation, target)
564
+ preflight_sessions(invocation, target, write_methods=("messages.add",))
565
565
  return add_message(
566
566
  dependencies,
567
567
  target,
@@ -25,7 +25,11 @@ from .projections import (
25
25
  )
26
26
 
27
27
  SESSION_LIMIT_MAX = 100
28
+ # The server's own defaults when ``--limit`` is omitted: an omitted limit is a
29
+ # real, truncating page, so the envelope reports which default was applied.
30
+ SESSION_LIMIT_DEFAULT = 50
28
31
  MESSAGE_LIMIT_MAX = 200
32
+ MESSAGE_LIMIT_DEFAULT = 100
29
33
  # Mirror the server's own bounds so an obviously unacceptable file is never
30
34
  # put on the wire; the server's refusal still wins when it disagrees.
31
35
  ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024
@@ -87,6 +91,28 @@ def _session_id(value: str) -> str:
87
91
  return value
88
92
 
89
93
 
94
+ IDEMPOTENCY_KEY_MAX_CHARS = 128
95
+
96
+
97
+ def _idempotency_key(value, refuse):
98
+ """Mirror the SDK's 128-character bound locally (the help text advertises it).
99
+
100
+ Refused before login or any request, so the bound holds regardless of the
101
+ installed SDK version. A non-empty string within the bound passes through
102
+ unchanged; the caller keeps its own refusal class (message write vs plan
103
+ review).
104
+ """
105
+ if value is None:
106
+ return None
107
+ if not isinstance(value, str) or not value or len(value) > IDEMPOTENCY_KEY_MAX_CHARS:
108
+ raise refuse(
109
+ "validation",
110
+ f"--idempotency-key must be a non-empty string of at most {IDEMPOTENCY_KEY_MAX_CHARS} "
111
+ "characters; nothing was sent.",
112
+ )
113
+ return value
114
+
115
+
90
116
  def _limit(value: int | None, maximum: int, name: str) -> int | None:
91
117
  if value is None:
92
118
  return None
@@ -540,6 +566,26 @@ def _identity(session, target: TenantTarget, session_id: str | None) -> None:
540
566
  )
541
567
 
542
568
 
569
+ def _page(limit: int | None, server_default: int, returned: int) -> dict:
570
+ """The page block, honest about an omitted ``--limit``.
571
+
572
+ These routes page with a limit only: an omitted limit is not "unpaged", it
573
+ is the server's own default page, which can truncate the list. The block
574
+ therefore reports either the limit that was sent, or the server default
575
+ that was applied instead, so a consumer can always tell whether more rows
576
+ may exist.
577
+ """
578
+
579
+ if limit is None:
580
+ return {
581
+ "kind": "server_default",
582
+ "server_default_limit": server_default,
583
+ "returned_count": returned,
584
+ "continuation": None,
585
+ }
586
+ return {"kind": "limit", "limit": limit, "returned_count": returned, "continuation": None}
587
+
588
+
543
589
  def list_sessions(dependencies: AmsDependencies, target: TenantTarget, *, limit: int | None = None) -> dict:
544
590
  """`hc ams sessions list`: the caller's non-expired sessions, newest first."""
545
591
 
@@ -554,12 +600,7 @@ def list_sessions(dependencies: AmsDependencies, target: TenantTarget, *, limit:
554
600
  _target(target),
555
601
  {
556
602
  "sessions": projections,
557
- "page": {
558
- "kind": "limit",
559
- "limit": limit,
560
- "returned_count": len(projections),
561
- "continuation": None,
562
- },
603
+ "page": _page(limit, SESSION_LIMIT_DEFAULT, len(projections)),
563
604
  },
564
605
  read_only=True,
565
606
  )
@@ -572,7 +613,9 @@ def get_session(dependencies: AmsDependencies, target: TenantTarget, session_id:
572
613
  with dependencies.authenticated_sdk_factory(target) as sdk:
573
614
  session = _sessions(sdk).get(session_id)
574
615
  if session is None:
575
- raise SessionReadRefused("validation", "Session/tenant identity mismatch; stopped.")
616
+ # A 200 with a JSON null body is the server saying "no such session
617
+ # here" — not an identity mismatch.
618
+ raise SessionReadRefused("not_found", f"Session '{session_id}' was not found in this tenant.")
576
619
  _identity(session, target, session_id)
577
620
  return succeeded(
578
621
  "sessions.get", _target(target, session_id), {"session": session_projection(session)}, read_only=True
@@ -622,20 +665,33 @@ def _message_conflict(error: BaseException, session_id: str) -> SessionWriteRefu
622
665
  return None
623
666
  return SessionWriteRefused(
624
667
  "conflict",
625
- f"The message was refused for session '{session_id}': the server reported a conflict. A "
626
- "message's mode must match its session's mode; read the session with `hc ams sessions get` "
627
- "before retrying. No message was added.",
668
+ f"The message was refused for session '{session_id}': the server reported a conflict — for "
669
+ "example, a message's mode not matching its session's mode. Read the session with "
670
+ "`hc ams sessions get` before retrying. No message was added.",
628
671
  )
629
672
 
630
673
 
631
674
  def _not_implemented(error: BaseException, *, session: bool) -> SessionWriteRefused | None:
675
+ """A 501 is the server saying it does not serve this write yet.
676
+
677
+ Both routes report the 501 the server actually sent rather than asserting
678
+ the requested mode was the cause; implement mode is named as the documented
679
+ reason, not as a certainty.
680
+ """
681
+
632
682
  if getattr(error, "status_code", None) != 501:
633
683
  return None
634
- what = "Sessions in" if session else "Messages in"
684
+ if session:
685
+ return SessionWriteRefused(
686
+ "unsupported",
687
+ "The server does not serve this session write yet (it answered 501, not implemented); the "
688
+ "write was refused and no changes were made. Implement mode is the documented reason: "
689
+ "implement-mode authoring is not available from this server yet.",
690
+ )
635
691
  return SessionWriteRefused(
636
692
  "unsupported",
637
- f"{what} implement mode are not available from this server yet; the write was refused and "
638
- "no changes were made.",
693
+ "The server does not serve this message write yet (it answered 501, not implemented); no "
694
+ "message was added. Implement-mode messages are not available from this server yet.",
639
695
  )
640
696
 
641
697
 
@@ -709,7 +765,7 @@ def list_messages(
709
765
  limit = _limit(limit, MESSAGE_LIMIT_MAX, "--limit")
710
766
  with dependencies.authenticated_sdk_factory(target) as sdk:
711
767
  try:
712
- messages = _messages(sdk).list(session_id, **({} if limit is None else {"limit": limit}))
768
+ messages = _messages(sdk, ("list",)).list(session_id, **({} if limit is None else {"limit": limit}))
713
769
  except HealthCloudHTTPError as error:
714
770
  if getattr(error, "status_code", None) == 404:
715
771
  raise SessionReadRefused(
@@ -724,12 +780,7 @@ def list_messages(
724
780
  _target(target, session_id),
725
781
  {
726
782
  "messages": projections,
727
- "page": {
728
- "kind": "limit",
729
- "limit": limit,
730
- "returned_count": len(projections),
731
- "continuation": None,
732
- },
783
+ "page": _page(limit, MESSAGE_LIMIT_DEFAULT, len(projections)),
733
784
  },
734
785
  read_only=True,
735
786
  )
@@ -944,6 +995,7 @@ def generate_plan(
944
995
  session_id = _session_id(session_id)
945
996
  operation = _plan_operation(operation)
946
997
  prompt = None if prompt_file is None else _prompt_file(prompt_file)
998
+ idempotency_key = _idempotency_key(idempotency_key, PlanReviewRefused)
947
999
  with private_output(out) as write, dependencies.authenticated_sdk_factory(target) as sdk:
948
1000
  resource = _plans(sdk, ("generate",), write=True)
949
1001
  try:
@@ -1038,6 +1090,7 @@ def add_message(
1038
1090
  refuse=SessionWriteRefused,
1039
1091
  required=True,
1040
1092
  )
1093
+ idempotency_key = _idempotency_key(idempotency_key, SessionWriteRefused)
1041
1094
  with dependencies.authenticated_sdk_factory(target) as sdk:
1042
1095
  resource = _messages(sdk, ("add",), write=True)
1043
1096
  try:
@@ -1067,8 +1120,13 @@ def add_message(
1067
1120
  "anything else.",
1068
1121
  )
1069
1122
  data: dict = {"message": message_projection(message)}
1070
- if idempotency_key is not None:
1071
- data["idempotency_key"] = idempotency_key
1123
+ # Echo the server's readback key when the message carries one, so the
1124
+ # envelope never asserts a key→message association the server did not
1125
+ # confirm. When the readback omits the key entirely the field is omitted
1126
+ # too — the caller can see the key they passed in their own command line.
1127
+ readback_key = getattr(message, "idempotency_key", None)
1128
+ if readback_key is not None:
1129
+ data["idempotency_key"] = readback_key
1072
1130
  return succeeded("sessions.messages.add", _target(target, session_id), data, read_only=False)
1073
1131
 
1074
1132
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "healthcloud-cli"
7
- version = "3.7.5"
7
+ version = "3.7.7"
8
8
  description = "Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"