pushmetrics-cli 0.1.0__py3-none-any.whl

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 (86) hide show
  1. pm/__init__.py +1 -0
  2. pm/__main__.py +4 -0
  3. pm/_alias.py +164 -0
  4. pm/auth/__init__.py +0 -0
  5. pm/auth/device_flow.py +187 -0
  6. pm/auth/exchange.py +79 -0
  7. pm/auth/jwt_cache.py +163 -0
  8. pm/auth/secret_files.py +58 -0
  9. pm/auth/session.py +440 -0
  10. pm/auth/store.py +160 -0
  11. pm/auth/token.py +289 -0
  12. pm/cli.py +282 -0
  13. pm/client/__init__.py +68 -0
  14. pm/client/access_tokens.py +54 -0
  15. pm/client/base.py +229 -0
  16. pm/client/blocks.py +461 -0
  17. pm/client/databases.py +147 -0
  18. pm/client/favorites.py +14 -0
  19. pm/client/folders.py +10 -0
  20. pm/client/integrations.py +14 -0
  21. pm/client/organizations.py +20 -0
  22. pm/client/recipients.py +92 -0
  23. pm/client/reports.py +148 -0
  24. pm/client/roles.py +100 -0
  25. pm/client/runs.py +134 -0
  26. pm/client/saved_blocks.py +13 -0
  27. pm/client/tableau.py +108 -0
  28. pm/client/users.py +71 -0
  29. pm/client/webhooks.py +43 -0
  30. pm/client/workspaces.py +34 -0
  31. pm/commands/__init__.py +0 -0
  32. pm/commands/_block_factory.py +762 -0
  33. pm/commands/_helpers.py +178 -0
  34. pm/commands/auth.py +222 -0
  35. pm/commands/block.py +26 -0
  36. pm/commands/chart.py +20 -0
  37. pm/commands/db.py +291 -0
  38. pm/commands/email.py +13 -0
  39. pm/commands/explore.py +20 -0
  40. pm/commands/favorites.py +77 -0
  41. pm/commands/folders.py +113 -0
  42. pm/commands/help.py +643 -0
  43. pm/commands/integrations.py +95 -0
  44. pm/commands/metrics_view.py +195 -0
  45. pm/commands/organizations.py +56 -0
  46. pm/commands/recipient.py +226 -0
  47. pm/commands/reports.py +364 -0
  48. pm/commands/roles.py +102 -0
  49. pm/commands/runs.py +214 -0
  50. pm/commands/saved_blocks.py +97 -0
  51. pm/commands/sftp.py +19 -0
  52. pm/commands/slack.py +13 -0
  53. pm/commands/slides.py +216 -0
  54. pm/commands/sql.py +17 -0
  55. pm/commands/tableau.py +148 -0
  56. pm/commands/token.py +107 -0
  57. pm/commands/users.py +262 -0
  58. pm/commands/webhooks.py +132 -0
  59. pm/commands/workspace.py +156 -0
  60. pm/config.py +192 -0
  61. pm/errors.py +228 -0
  62. pm/log.py +63 -0
  63. pm/models/__init__.py +31 -0
  64. pm/models/access_tokens.py +25 -0
  65. pm/models/blocks.py +21 -0
  66. pm/models/databases.py +15 -0
  67. pm/models/favorites.py +14 -0
  68. pm/models/folders.py +14 -0
  69. pm/models/integrations.py +20 -0
  70. pm/models/organizations.py +13 -0
  71. pm/models/recipients.py +41 -0
  72. pm/models/reports.py +16 -0
  73. pm/models/roles.py +22 -0
  74. pm/models/runs.py +56 -0
  75. pm/models/saved_blocks.py +15 -0
  76. pm/models/users.py +36 -0
  77. pm/models/webhooks.py +23 -0
  78. pm/models/workspaces.py +16 -0
  79. pm/output.py +92 -0
  80. pm/types.py +130 -0
  81. pm/validators.py +499 -0
  82. pushmetrics_cli-0.1.0.dist-info/METADATA +221 -0
  83. pushmetrics_cli-0.1.0.dist-info/RECORD +86 -0
  84. pushmetrics_cli-0.1.0.dist-info/WHEEL +5 -0
  85. pushmetrics_cli-0.1.0.dist-info/entry_points.txt +2 -0
  86. pushmetrics_cli-0.1.0.dist-info/top_level.txt +1 -0
pm/commands/help.py ADDED
@@ -0,0 +1,643 @@
1
+ """`pm help` — terse, agent-tuned help pages (pm-tool plan §2.3 / tasks A.1).
2
+
3
+ Click already auto-renders verbose help via ``pm --help`` / ``pm <group>
4
+ --help`` — that's the human-facing fallback. ``pm help`` is the
5
+ *agent-facing* surface: every page is ≤600 chars (vs Click's ~1500–3000
6
+ char rendering), structured as "Verbs → Body fields → Example", and
7
+ filtered by the ``PM_AGENT_DENIED_ACTIONS`` env var so denied actions
8
+ don't even appear in help (defense in depth alongside dispatch-layer
9
+ denial in pm_tool.py per pm-consolidation).
10
+
11
+ Why short pages matter
12
+ ----------------------
13
+ The agent burns input tokens on every help fetch. A 3000-char Click
14
+ ``--help`` rendering pushes ~750 tokens through every time. A 500-char
15
+ ``pm help`` page is ~125 tokens. At the scale of hundreds of agent
16
+ help fetches per workspace per day, the delta compounds.
17
+
18
+ Suppression contract
19
+ --------------------
20
+ ``PM_AGENT_DENIED_ACTIONS`` (CSV of ``<noun>.<verb>`` canonical_actions)
21
+ is the single policy source. ``pm_tool.py`` reads it for dispatch
22
+ denial; this module reads it for help rendering. Setting it to
23
+ ``block.delete,sftp.create,sftp.send`` (the default ``single_tool`` list)
24
+ hides those verbs from ``pm help block`` and ``pm help sftp``.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ from typing import Dict, List, Optional
30
+
31
+ import click
32
+
33
+
34
+ # ── Group-name → backend noun (for canonical_action mapping) ─────────
35
+ #
36
+ # Duplicates the map in prefect/queryme/agent/canonical_actions.py
37
+ # because the CLI is HTTP-only (cli/tests/test_no_backend_imports.py)
38
+ # and can't pull from the prefect agent. Keep these two maps in sync
39
+ # manually — small static data, low churn risk.
40
+
41
+ _GROUP_TO_NOUN: Dict[str, str] = {
42
+ "sql": "sql",
43
+ "email": "email",
44
+ "slack": "slack",
45
+ "sftp": "sftp",
46
+ "chart": "plotly",
47
+ "explore": "explore",
48
+ "metrics-view": "metrics_view",
49
+ "tableau": "tableau",
50
+ "slides": "google_slides",
51
+ "block": "block",
52
+ }
53
+
54
+
55
+ # ── Per-group pages ──────────────────────────────────────────────────
56
+ #
57
+ # Each entry has `verbs` (ordered dict mapping CLI verb → one-line
58
+ # description) plus a `page` template that includes verb list + body
59
+ # fields + example. Verbs are filtered against PM_AGENT_DENIED_ACTIONS
60
+ # before rendering — the page template gets the filtered verb dict
61
+ # back.
62
+
63
+ _GROUP_PAGES: Dict[str, Dict] = {
64
+ "sql": {
65
+ "verbs": {
66
+ "create": "Create a SQL block (returns uid; does NOT run).",
67
+ "list": "List. [--name <text>] [--limit <n>].",
68
+ "get": "Show one. <uid>.",
69
+ "run": "Execute by <uid>, or transient override via --stdin against an assigned scratch (no persist).",
70
+ "results": "Past execution result(s). <uid> [--last|--result-id].",
71
+ },
72
+ "body_fields": (
73
+ " sql (req) SQL query string\n"
74
+ " database_id (req) UID of the database to run against\n"
75
+ " limit (opt) row limit"
76
+ ),
77
+ "example": (
78
+ " echo '{\"sql\":\"SELECT 1\",\"database_id\":\"<db_uid>\"}' \\\n"
79
+ " | pm sql run --stdin --json"
80
+ ),
81
+ "summary": "Create + run SQL blocks (block_type=sql).",
82
+ },
83
+ "email": {
84
+ "verbs": {
85
+ "create": "Create; does not send.",
86
+ "list": "List. [--name <text>] [--limit <n>].",
87
+ "get": "Show one. <uid>.",
88
+ "send": "Send <uid>, or a --stdin body once.",
89
+ "results": "Past sends. <uid> [--last|--result-id].",
90
+ },
91
+ "body_fields": (
92
+ " subject (req)\n"
93
+ " body (req) HTML/text\n"
94
+ " integration_id (req) email integration uid\n"
95
+ " to_ (req) string or list — note trailing _\n"
96
+ " cc, bcc (opt) string or list"
97
+ ),
98
+ "example": (
99
+ " echo '{\"subject\":\"Hi\",\"body\":\"x\",\"integration_id\":\"<int>\",\"to_\":\"a@b.com\"}' \\\n"
100
+ " | pm email send --stdin --json"
101
+ ),
102
+ "summary": "Create + send email blocks.",
103
+ },
104
+ "slack": {
105
+ "verbs": {
106
+ "create": "Create a slack block.",
107
+ "list": "List. [--name <text>] [--limit <n>].",
108
+ "get": "Show one. <uid>.",
109
+ "send": "Send by <uid>, or transient override via --stdin against an assigned scratch (no persist).",
110
+ "results": "Past send result(s).",
111
+ },
112
+ "body_fields": (
113
+ " integration_id (req) slack integration uid\n"
114
+ " channels (req) LIST of channel names or DM ids\n"
115
+ " message (req)"
116
+ ),
117
+ "example": (
118
+ " echo '{\"integration_id\":\"<int>\",\"channels\":[\"#alerts\"],\"message\":\"deploy ok\"}' \\\n"
119
+ " | pm slack send --stdin --json"
120
+ ),
121
+ "summary": "Create + send Slack message blocks.",
122
+ },
123
+ "sftp": {
124
+ "verbs": {
125
+ "create": "Create an SFTP block.",
126
+ "list": "List. [--name <text>] [--limit <n>].",
127
+ "get": "Show one. <uid>.",
128
+ "send": "Upload by <uid>, or transient override via --stdin against an assigned scratch (no persist).",
129
+ "results": "Past upload result(s).",
130
+ },
131
+ "body_fields": (
132
+ " integration_id (req)\n"
133
+ " path (req) destination path on the SFTP server\n"
134
+ " attachments (opt) list of URLs/refs (e.g. csv_download_url\n"
135
+ " from prior run_sql) to upload"
136
+ ),
137
+ "example": (
138
+ " pm sftp send <uid> --json"
139
+ ),
140
+ "summary": "Create + send SFTP upload blocks.",
141
+ },
142
+ "chart": {
143
+ "verbs": {
144
+ "create": "Create + auto-render. --name=<n> --stdin.",
145
+ "list": "List. [--name <text>] [--limit <n>].",
146
+ "get": "Show one. <uid>.",
147
+ "update": "<uid> --stdin (replaces the Plotly spec).",
148
+ "run": "Re-render an existing chart block. <uid>.",
149
+ "results": "Past render result(s).",
150
+ },
151
+ "body_fields": (
152
+ " properties (req) {data, layout} per Plotly schema\n"
153
+ " image_filename (opt)"
154
+ ),
155
+ "example": (
156
+ " pm chart create --name=q4-rev --stdin < spec.json"
157
+ ),
158
+ "summary": (
159
+ "Create + render Plotly chart blocks (block_type=plotly).\n"
160
+ "NOTE: creating a chart also renders the PNG as a side effect."
161
+ ),
162
+ },
163
+ "explore": {
164
+ "verbs": {
165
+ "create": "Create an explore block. --name=<n> --stdin.",
166
+ "list": "List. [--name <text>] [--limit <n>].",
167
+ "get": "Show one. <uid>.",
168
+ "update": "<uid> --stdin (replaces the saved-view definition).",
169
+ },
170
+ "body_fields": " (free-form — saved-view definition)",
171
+ "example": (
172
+ " pm explore create --name=daily-funnel --stdin < view.json\n"
173
+ " pm explore list --json"
174
+ ),
175
+ "summary": (
176
+ "Create explore (saved-view) blocks.\n"
177
+ "NOTE: no execute verb — the UI renders these."
178
+ ),
179
+ },
180
+ "metrics-view": {
181
+ "verbs": {
182
+ "create": "Create.",
183
+ "list": "List. [--name <text>] [--limit <n>].",
184
+ "get": "Show one. <uid>.",
185
+ "update": "<uid> --stdin.",
186
+ "query": "Semantic query. <uid> [flags or --stdin].",
187
+ "schema": "Available measures/dimensions. <uid>.",
188
+ },
189
+ "body_fields": (
190
+ " --measure NAME repeatable, ≥1 required\n"
191
+ " --dimension NAME repeatable\n"
192
+ " --time-range last_30d | 2026-01-01:2026-04-01\n"
193
+ " --limit N, --offset N, --no-cache\n"
194
+ " --stdin for filters/order_by/parameters"
195
+ ),
196
+ "example": (
197
+ " pm metrics-view query <uid> --measure=orders --dimension=region --json"
198
+ ),
199
+ "summary": "Manage + query metrics view blocks (semantic layer).",
200
+ },
201
+ "tableau": {
202
+ "verbs": {
203
+ "create": "Create.",
204
+ "list": "List. [--name <text>] [--limit <n>].",
205
+ "get": "Show one. <uid>.",
206
+ "run": "Exports/downloads Tableau view as PNG/PDF/embed URL.",
207
+ "results": "Past render result(s).",
208
+ "search": "List views (or --workbooks).",
209
+ "view": "One view's metadata.",
210
+ },
211
+ "body_fields": (
212
+ " --integration <uid> search/view integration\n"
213
+ " --keyword <text> name filter\n"
214
+ " --workbook-id <id> narrow search\n"
215
+ " --workbooks search workbooks\n"
216
+ " --view-id <id> required for view"
217
+ ),
218
+ "example": (
219
+ " pm tableau search --integration=<uid> --keyword=Sales --json"
220
+ ),
221
+ "summary": "Tableau blocks + discovery.",
222
+ },
223
+ "slides": {
224
+ "verbs": {
225
+ "create": "Create.",
226
+ "list": "List. [--name <text>] [--limit <n>].",
227
+ "get": "Show one. <uid>.",
228
+ "run": "Compose presentation.",
229
+ "results": "Past render result(s).",
230
+ "parse-template": "Parse master deck (once after create).",
231
+ "export": "Export as PDF. [--filename]",
232
+ "batch-update": "Slides API batchUpdate. --stdin.",
233
+ },
234
+ "body_fields": (
235
+ " --stdin JSON:\n"
236
+ " integration_id, template_id, source_file_id"
237
+ ),
238
+ "example": (
239
+ " pm slides parse-template <uid> --json\n"
240
+ " pm slides export <uid> --filename=q4.pdf --json"
241
+ ),
242
+ "summary": (
243
+ "Google Slides blocks (google_slides)."
244
+ ),
245
+ },
246
+ "block": {
247
+ "verbs": {
248
+ "get": "Show one. <uid>.",
249
+ "list": "List. [--type=<T>] [--name <text>] [--limit <n>].",
250
+ "create": "--type=<T> --name=<n> --stdin.",
251
+ "update": "<uid> [--name=<n>] [--stdin].",
252
+ "delete": "<uid> [--yes].",
253
+ "run": "Execute (stored type wins; --type rejected).",
254
+ "results": "<uid> [--last|--result-id].",
255
+ },
256
+ "body_fields": (
257
+ " content_json shape on create depends on --type\n"
258
+ " (see `pm help <type>` for per-type fields)."
259
+ ),
260
+ "example": (
261
+ " pm block list --type=sql --json\n"
262
+ " pm block create --type=email --name=daily --stdin < body.json"
263
+ ),
264
+ "summary": "CRUD plumbing across all block types.",
265
+ },
266
+
267
+ # ── CRUD resource groups ──────────────────────────────────────────
268
+ # Pages for the non-typed-block-group verbs. Without these, ``pm
269
+ # help report`` (etc.) returned ``No help for 'report'``, leaving
270
+ # the agent to guess flags after every failure (session N8wE87kOmz
271
+ # took 46 tool calls partly because of this discoverability gap).
272
+
273
+ "report": {
274
+ "verbs": {
275
+ "list": "List reports (newest first). [--name <text>] [--limit <n>].",
276
+ "get": "Show one. <uid>.",
277
+ "yaml": "Dump YAML. <uid>.",
278
+ "update": "<uid> [--name=<n>].",
279
+ "apply": (
280
+ "YAML via stdin: `pm report apply -`. With --uid, blocks "
281
+ "missing from the YAML are DELETED — use --no-prune to "
282
+ "only upsert, --dry-run to preview."
283
+ ),
284
+ "run": "Trigger NEW run. For past runs use `pm run list --report=<uid>`.",
285
+ "delete": "<uid> [--yes].",
286
+ },
287
+ "example": (
288
+ " pm report list --json\n"
289
+ " pm run list --report=<uid> --last-days=7\n"
290
+ " pm report apply - --uid=<uid> # YAML via tool's stdin"
291
+ ),
292
+ "summary": "Reports and their runs.",
293
+ },
294
+ "run": {
295
+ "verbs": {
296
+ "list": "Recent runs. [--report <uid>] [--last-days <n>] [--status <s>] [--limit <n>].",
297
+ "get": "Status, timing, flow_run_id. <run_uid>.",
298
+ "results": "Per-block results for a run. <run_uid>.",
299
+ "logs": "Log lines for a run. <run_uid|flow_run_id>.",
300
+ },
301
+ "body_fields": (
302
+ " ``get`` is metadata only — NO row data (use `pm sql run\n"
303
+ " --stdin`). Debug a failed report: list its runs, then\n"
304
+ " read one run's logs (`--level=ERROR` for failure lines)."
305
+ ),
306
+ "example": (
307
+ " pm run list --report=<report_uid> --last-days=2 --status=failed\n"
308
+ " pm run logs <run_uid> --level=ERROR --json"
309
+ ),
310
+ "summary": "Noteflow run inspection.",
311
+ },
312
+ "database": {
313
+ "verbs": {
314
+ "list": "Workspace databases. [--name <text>] [--limit <n>].",
315
+ "schemas": "<db_ref>.",
316
+ "tables": "<db_ref> <schema>.",
317
+ "describe": "<db_ref> <schema> <table>.",
318
+ "select-star": "SELECT * SQL, not run. <db_ref> <schema> <table>.",
319
+ "exists": "<db_ref> <schema> <table>.",
320
+ "search": "<db_ref> <pattern> [--schema <s>].",
321
+ "functions": "<db_ref>.",
322
+ },
323
+ "body_fields": (
324
+ " <db_ref>: uid or name (quote spaces).\n"
325
+ " select-star prints SQL; pipe it into `pm sql run --stdin`."
326
+ ),
327
+ "example": (
328
+ " pm database list --json\n"
329
+ " pm database tables \"Demo DB\" public --json"
330
+ ),
331
+ "summary": "Database introspection.",
332
+ },
333
+ "folder": {
334
+ "verbs": {
335
+ "list": "Workspace folders. [--name <text>] [--limit <n>].",
336
+ "get": "Show one folder. <uid>.",
337
+ "create": "Create a folder. --name=<n> [--parent=<uid>].",
338
+ "update": "Rename / move. <uid> [--name=<n>] [--parent=<uid>].",
339
+ "delete": "Delete a folder. <uid> [--yes].",
340
+ },
341
+ "example": (
342
+ " pm folder list --json\n"
343
+ " pm folder create --name=Marketing --json"
344
+ ),
345
+ "summary": "Folder CRUD.",
346
+ },
347
+ "user": {
348
+ "verbs": {
349
+ "list": "Workspace users. [--name <text>] [--limit <n>].",
350
+ "get": "Show one user. <uid>.",
351
+ "create": "Create a user. --email=<e> [--name=<n>].",
352
+ "delete": "Delete a user. <uid> [--yes].",
353
+ "set-role": "Assign a role. <user_uid> <role_name>.",
354
+ },
355
+ "example": (
356
+ " pm user list --json\n"
357
+ " pm user set-role <user_uid> editor --json"
358
+ ),
359
+ "summary": "User management.",
360
+ },
361
+ "recipient": {
362
+ "verbs": {
363
+ "list": "Recipients in the workspace CRM. [--name <text>] [--limit <n>].",
364
+ "get": "Show one recipient. <uid>.",
365
+ "details": "Aggregate read (linked destinations etc.). <uid>.",
366
+ "create": "Create a recipient. --name=<n> --email=<e>.",
367
+ "update": "Update recipient. <uid> [--name=<n>] [--email=<e>].",
368
+ "delete": "Delete recipient. <uid> [--yes].",
369
+ },
370
+ "example": (
371
+ " pm recipient list --json\n"
372
+ " pm recipient create --name=\"Alice\" --email=alice@acme.test --json"
373
+ ),
374
+ "summary": "Recipients (delivery-target CRM).",
375
+ },
376
+ "integration": {
377
+ "verbs": {
378
+ "list": "Workspace integrations (Tableau / Slack / Email / SFTP / GCP / S3 / Slides etc.). [--name <text>] [--type <t>] [--limit <n>].",
379
+ },
380
+ "body_fields": (
381
+ " Use ``--type=tableau`` (etc.) to filter to one type.\n"
382
+ " Pass the resulting uid as ``--integration=<uid>`` to\n"
383
+ " search / view / send verbs."
384
+ ),
385
+ "example": (
386
+ " pm integration list --json\n"
387
+ " pm integration list --type=tableau --json"
388
+ ),
389
+ "summary": "Integration discovery.",
390
+ },
391
+ "favorite": {
392
+ "verbs": {
393
+ "list": "Favorited blocks / reports. [--limit <n>].",
394
+ "create": "Mark as favorite. <uid>.",
395
+ "delete": "Unfavorite. <uid>.",
396
+ },
397
+ "example": (
398
+ " pm favorite list --json"
399
+ ),
400
+ "summary": "Favorites management.",
401
+ },
402
+ "organization": {
403
+ "verbs": {
404
+ "current": "Show the calling user's organization.",
405
+ },
406
+ "example": (
407
+ " pm organization current --json"
408
+ ),
409
+ "summary": "Organization discovery.",
410
+ },
411
+ "role": {
412
+ "verbs": {
413
+ "list": "Roles available in the workspace. [--name <text>] [--limit <n>].",
414
+ "assign": "Assign role to user. <user_uid> <role_name>.",
415
+ "revoke": "Revoke role from user. <user_uid> <role_name>.",
416
+ },
417
+ "example": (
418
+ " pm role list --json\n"
419
+ " pm role assign <user_uid> editor --json"
420
+ ),
421
+ "summary": "Role assignments.",
422
+ },
423
+ "saved-block": {
424
+ "verbs": {
425
+ "list": "Saved (template) blocks in the workspace. [--name <text>] [--limit <n>].",
426
+ "update": "Update a saved block. <uid> [--name=<n>] [--stdin].",
427
+ "delete": "Delete a saved block. <uid> [--yes].",
428
+ },
429
+ "example": (
430
+ " pm saved-block list --json"
431
+ ),
432
+ "summary": "Saved-block (template) management.",
433
+ },
434
+ "workspace": {
435
+ "verbs": {
436
+ "list": "Workspaces this user belongs to. [--name <text>] [--limit <n>].",
437
+ "current": "Show the active workspace for this session.",
438
+ },
439
+ "body_fields": (
440
+ " Switching / creating / deleting workspaces is intentionally\n"
441
+ " NOT available to the agent — context comes from the calling\n"
442
+ " user's session."
443
+ ),
444
+ "example": (
445
+ " pm workspace current --json\n"
446
+ " pm workspace list --json"
447
+ ),
448
+ "summary": "Workspace discovery (read-only for agents).",
449
+ },
450
+ }
451
+
452
+
453
+ # ── Account topics (plain pages, no verb table) ──────────────────────
454
+ #
455
+ # Listed on the top page, so `pm help <topic>` must answer for each.
456
+
457
+ _PLAIN_PAGES: Dict[str, str] = {
458
+ "login": (
459
+ "pm login — Log in with a personal access token.\n\n"
460
+ " pm login --token paste the token at the hidden prompt\n"
461
+ " <secrets-cmd> | pm login --token pipe it from a secrets manager\n"
462
+ " PM_API_TOKEN=... use it without storing (CI, scripts)\n\n"
463
+ "Tokens are created in the web app under Access Tokens."
464
+ ),
465
+ "logout": (
466
+ "pm logout — Forget the stored token or session for the profile.\n\n"
467
+ " pm logout\n pm --profile staging logout"
468
+ ),
469
+ "whoami": (
470
+ "pm whoami — Show who you are logged in as, and the workspace.\n\n"
471
+ " pm whoami --json\n\n"
472
+ "A token is bound to the workspace it was created in."
473
+ ),
474
+ "token": (
475
+ "pm token — Personal access tokens.\n\n"
476
+ "Verbs:\n"
477
+ " list Your tokens (metadata only). [--name <text>] [--limit <n>]\n"
478
+ " create Create one; the value is printed once, on stdout.\n"
479
+ " revoke Revoke by numeric ID. <id>\n\n"
480
+ "Example:\n pm token create ci-exports --expires-on 2027-01-01"
481
+ ),
482
+ "config": (
483
+ "pm config — Profiles: which PushMetrics server pm talks to.\n\n"
484
+ "Verbs:\n"
485
+ " list Profiles and active env overrides.\n"
486
+ " set Set one field. <profile> <key> <value>\n"
487
+ " keys: api_url auth0_domain auth0_client_id auth0_audience\n\n"
488
+ "Example:\n pm config set staging api_url https://staging.example.com"
489
+ ),
490
+ }
491
+
492
+
493
+ # ── Top-level page (≤800 chars) ──────────────────────────────────────
494
+
495
+
496
+ # NOTE: the surface map below only lists groups + verbs the agent can
497
+ # actually invoke via the cli_adapter ALLOWED_COMMANDS set. `auth`
498
+ # (login / logout / whoami), `webhook`, and the mutating
499
+ # `workspace` / `organization` verbs are deliberately omitted —
500
+ # the agent runs under a delegation JWT (no Auth0 needed), has no
501
+ # webhook surface allowed, and inherits its workspace + organization
502
+ # context from the calling user's session.
503
+ _TOP_LEVEL_PAGE = """pm — PushMetrics CLI.
504
+
505
+ Common:
506
+ pm sql run --stdin --json Run SQL via assigned scratch (body on stdin)
507
+ pm report list List reports
508
+ pm block list --type=sql Filter blocks by type
509
+ pm metrics-view query <uid> Run a semantic query
510
+ pm tableau search --integration=<uid> List Tableau views
511
+
512
+ Surface:
513
+ Domain actions: sql email slack sftp chart explore metrics-view
514
+ tableau slides
515
+ Resources: report folder favorite user role organization
516
+ saved-block integration recipient run
517
+ Block plumbing: block
518
+ DB: database
519
+ Workspace: workspace
520
+ Account: login logout whoami token config
521
+
522
+ `pm help <topic>` for verb-specific help with examples.
523
+ `pm --help` lists every command."""
524
+
525
+
526
+ # ── Suppression: PM_AGENT_DENIED_ACTIONS ─────────────────────────────
527
+
528
+
529
+ def _denied_actions_from_env() -> "frozenset[str]":
530
+ """Parse ``PM_AGENT_DENIED_ACTIONS`` into a set of canonical_actions.
531
+
532
+ Empty / unset env var → empty set (no suppression). Whitespace
533
+ around commas tolerated. Unknown entries are kept as-is — the
534
+ canonical_action format is ``<noun>.<verb>``, but we don't validate
535
+ here; help suppression is best-effort and pm_tool.py is the
536
+ real enforcement point.
537
+ """
538
+ raw = os.environ.get("PM_AGENT_DENIED_ACTIONS", "")
539
+ return frozenset({x.strip() for x in raw.split(",") if x.strip()})
540
+
541
+
542
+ def _canonical_action_for(group: str, verb: str) -> str:
543
+ """Map (group, verb) to ``<noun>.<verb>`` for deny-list matching.
544
+
545
+ Mirrors prefect/queryme/agent/canonical_actions.py's static map for
546
+ the typed-group case. Block-plumbing keeps ``block.<verb>`` here
547
+ (without uid-based type discrimination) because the help page
548
+ describes the verb generically — runtime dispatch will resolve to
549
+ a typed action via the real resolver.
550
+ """
551
+ noun = _GROUP_TO_NOUN.get(group, group)
552
+ return f"{noun}.{verb}"
553
+
554
+
555
+ def _filter_verbs(
556
+ group: str,
557
+ verbs: Dict[str, str],
558
+ denied: "frozenset[str]",
559
+ ) -> Dict[str, str]:
560
+ if not denied:
561
+ return verbs
562
+ return {
563
+ verb: desc
564
+ for verb, desc in verbs.items()
565
+ if _canonical_action_for(group, verb) not in denied
566
+ }
567
+
568
+
569
+ # ── Page rendering ────────────────────────────────────────────────────
570
+
571
+
572
+ def _render_group_page(group: str, denied: "frozenset[str]") -> str:
573
+ spec = _GROUP_PAGES[group]
574
+ visible_verbs = _filter_verbs(group, spec["verbs"], denied)
575
+
576
+ lines: List[str] = [f"pm {group} — {spec['summary']}", ""]
577
+
578
+ if not visible_verbs:
579
+ lines.append(
580
+ "No verbs available in the current agent mode. "
581
+ "Check PM_AGENT_DENIED_ACTIONS or use a wider mode.",
582
+ )
583
+ return "\n".join(lines)
584
+
585
+ lines.append("Verbs:")
586
+ width = max(len(v) for v in visible_verbs) + 2
587
+ for verb, desc in visible_verbs.items():
588
+ lines.append(f" {verb.ljust(width)}{desc}")
589
+
590
+ if "body_fields" in spec and spec["body_fields"]:
591
+ lines.append("")
592
+ lines.append("Body fields:")
593
+ lines.append(spec["body_fields"])
594
+
595
+ lines.append("")
596
+ lines.append("Example:")
597
+ lines.append(spec["example"])
598
+
599
+ return "\n".join(lines)
600
+
601
+
602
+ def _known_topics() -> List[str]:
603
+ return sorted(set(_GROUP_PAGES) | set(_PLAIN_PAGES))
604
+
605
+
606
+ # ── Click command ────────────────────────────────────────────────────
607
+
608
+
609
+ @click.command(name="help")
610
+ @click.argument("topic", required=False)
611
+ @click.pass_context
612
+ def help_cmd(ctx: click.Context, topic: Optional[str]) -> None:
613
+ """Show compact help for a group, with examples.
614
+
615
+ With no topic, prints the surface map; with a topic (e.g. pm help sql),
616
+ prints that group's page. This is the view the AI agent reads.
617
+
618
+ Suppresses verbs denied via PM_AGENT_DENIED_ACTIONS so agent-mode
619
+ help only surfaces invokable actions.
620
+ """
621
+ denied = _denied_actions_from_env()
622
+ if topic is None:
623
+ click.echo(_TOP_LEVEL_PAGE)
624
+ return
625
+ if topic in _PLAIN_PAGES:
626
+ click.echo(_PLAIN_PAGES[topic])
627
+ return
628
+ if topic not in _GROUP_PAGES:
629
+ click.echo(
630
+ f"No help for {topic!r}. Known topics: {', '.join(_known_topics())}",
631
+ err=True,
632
+ )
633
+ ctx.exit(1)
634
+ click.echo(_render_group_page(topic, denied))
635
+
636
+
637
+ # Exposed for tests + introspection.
638
+ __all__ = [
639
+ "help_cmd",
640
+ "_GROUP_PAGES",
641
+ "_GROUP_TO_NOUN",
642
+ "_TOP_LEVEL_PAGE",
643
+ ]