postgresai 0.17.0-dev.1 → 0.17.0-dev.11

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.
package/README.md CHANGED
@@ -76,6 +76,8 @@ Password input options (in priority order):
76
76
  - `PGAI_MON_PASSWORD` environment variable
77
77
  - if not provided: a strong password is generated automatically
78
78
 
79
+ Monitoring passwords must use printable ASCII characters; non-ASCII passwords are rejected until SASLprep normalization is supported. Role creation and password resets send a SCRAM-SHA-256 verifier instead of the cleartext password, including when the server uses `password_encryption=md5`. Printed SQL redacts the verifier as well. Server statement logging can still record the verifier; treat it as sensitive.
80
+
79
81
  By default, the generated password is printed **only in interactive (TTY) mode**. In non-interactive mode, you must either provide the password explicitly, or opt-in to printing it:
80
82
  - `--print-password` (dangerous in CI logs)
81
83
 
@@ -125,6 +127,52 @@ Notes:
125
127
  - All standard options work with Supabase mode (`--verify`, `--print-sql`, `--skip-optional-permissions`, etc.)
126
128
  - When using `--verify`, the tool checks if all required setup is in place
127
129
 
130
+ ### ClickHouse Managed Postgres
131
+
132
+ Hosts ending in `.pg.clickhouse.cloud` are auto-detected; `--provider clickhouse` forces it.
133
+ The plan is the same superuser plan as self-managed Postgres (`--print-sql` shows it).
134
+
135
+ ```bash
136
+ npx postgresai prepare-db 'postgres://postgres:...@xxx.pg.clickhouse.cloud:5432/postgres?channel_binding=require'
137
+ ```
138
+
139
+ Before any grant runs, one `-- scope:` line lists exactly what the run grants the
140
+ monitoring role, derived from the steps about to run (so it shrinks under `--skip-optional-permissions`
141
+ and is not printed on `--reset-password`, which grants nothing). With `--json` it goes to stderr.
142
+
143
+ For `prepare-db`, `channel_binding=require` in a URI or conninfo string enables SCRAM-SHA-256-PLUS when the server
144
+ offers it, disables the plaintext retry that `sslmode=prefer` would otherwise do, and is rejected
145
+ together with `sslmode=disable`. The mechanism actually negotiated is not enforced by the driver.
146
+
147
+ The collector cannot do channel binding. `mon targets add`, `mon local-install --db-url`, and
148
+ `pgai connect` remove `channel_binding` from the saved/box URL. With `require`, they refuse
149
+ `sslmode=disable` or `allow`; keep `verify-full` with a warning; and upgrade `require`, `prefer`,
150
+ `verify-ca` or unset to `verify-full` after testing certificate and hostname verification with
151
+ Node's default trust store (or the CA file in `sslrootcert`). Authentication or other errors after
152
+ a verified TLS handshake do not block the upgrade. If verification fails, provide
153
+ `sslrootcert=<CA file>` with `sslmode=verify-full`, or explicitly accept the downgrade by removing
154
+ `channel_binding=require`. Successful upgrades print the collector warning and
155
+ `upgraded to sslmode=verify-full`. `channel_binding=prefer` or `disable` is removed with a note.
156
+
157
+ ### ClickHouse Cloud host metrics
158
+
159
+ For a ClickHouse Managed Postgres target, `mon local-install --db-url` and `mon targets add` also set up
160
+ host metrics (CPU, memory, disk, I/O) from the ClickHouse Cloud Prometheus endpoint when these are set:
161
+
162
+ ```bash
163
+ export CLICKHOUSE_ORG_ID='<org-id>' CLICKHOUSE_KEY_ID='<key-id>' CLICKHOUSE_KEY_SECRET='<key-secret>'
164
+ npx postgresai mon local-install --db-url 'postgres://postgres_ai_mon:...@xxx.pg.clickhouse.cloud:5432/postgres?sslmode=require'
165
+ ```
166
+
167
+ Use an organization API key with the **Basic Service API Reader** role: it is the least privilege that
168
+ works (it can list Postgres services and read `/prometheus`, and cannot change anything or create keys).
169
+ The CLI does not need an Admin key.
170
+
171
+ The key secret is stored in plaintext in `host-metrics/clickhouse-<name>.secret` in the monitoring
172
+ project directory (file 0600, directory 0700), because VictoriaMetrics reads it from a file for basic
173
+ auth. It is never printed or logged. `mon targets remove <name>` deletes it. To rotate the key, re-run
174
+ `mon targets add` with the new key.
175
+
128
176
  ### Verify and password reset
129
177
 
130
178
  Verify that everything is configured as expected (no changes):
@@ -141,6 +189,144 @@ npx postgresai prepare-db postgresql://admin@host:5432/dbname --reset-password -
141
189
 
142
190
  ## Quick start
143
191
 
192
+ ### One command: `pgai connect`
193
+
194
+ ```bash
195
+ pgai connect 'postgresql://postgres:<password>@<host>:5432/postgres'
196
+ ```
197
+
198
+ It signs you in if needed, creates the `postgres_ai_mon` role (with an admin URL; otherwise it
199
+ prints the SQL), provisions monitoring in PostgresAI Cloud, runs the express checkup (the checks
200
+ of `pgai checkup`, as `postgres_ai_mon`) while the box starts and prints its findings (in JSON:
201
+ `checkup`; skipped where PostgresAI keeps the role's password: no `checkup` key, no first report),
202
+ waits, and prints the dashboard URL. Safe to re-run; a `--clickhouse-key` given on a
203
+ re-run is checked, and a rejected one is `action_required`. For ClickHouse Managed Postgres add `--clickhouse-key <key-id>:<key-secret>`
204
+ (a Basic Service API Reader key) for CPU, memory and disk. `--self-hosted` runs the stack on this
205
+ machine instead. When stdout is not a terminal the result is JSON with `status`, `dashboard_url`
206
+ and `next`. Then: `pgai databases`, `pgai status <name>`, `pgai disconnect <name>`.
207
+
208
+ With JSON output (`--json`, or stdout not a terminal) stderr is the progress stream: one JSON
209
+ object a line, each with `event`. Besides the steps, any other text is
210
+ `{"event":"log","level":"error"|"warn"|"info"|"debug","message":...}`: an error (also a missing
211
+ argument or a bad option; an unknown option is named without its value), a warning (`warn`: a line
212
+ with `Warning:`, also after a `[F001] ` prefix), the `--debug` request log (`debug`), and with
213
+ `--self-hosted` each line of `mon local-install`
214
+ (`"source":"mon local-install"`; `info` from its stdout, `error` from its stderr; the Grafana and
215
+ VictoriaMetrics logins it prints at its end are masked: `pgai mon show-grafana-credentials` shows them).
216
+
217
+ `pgai init` is the same for a person at a terminal: it signs in, asks for the database URL (and,
218
+ for ClickHouse, the API key; neither is shown as typed), then runs `pgai connect`. Without a
219
+ terminal, or with `--json`, it only points to `pgai connect`.
220
+
221
+ | Option | |
222
+ |---|---|
223
+ | `--provider <provider>` | `clickhouse`, `rds`, `supabase` or `self-managed`; default: detected from the host |
224
+ | `--clickhouse-key <key-id>:<key-secret>` | ClickHouse Cloud API key, for CPU, memory and disk |
225
+ | `--self-hosted` | run the stack on this machine (`mon local-install`) instead of PostgresAI Cloud |
226
+ | `--reset-password` | `postgres_ai_mon` exists and its password is lost: set a new one (see below) |
227
+ | `--wait <minutes>` | how long to wait for the monitoring box; `0` does not wait (default 20) |
228
+ | `-y, --yes` | never prompt; accept a billed box's price (`connect` does not start the browser sign-in; `disconnect` does not ask) |
229
+ | `--coupon <code>` | promotion code for the organization's monitoring subscription |
230
+ | `--json` | JSON output, also on a terminal |
231
+
232
+ Without `--yes`, a non-interactive run for a billed box returns `action_required` with the price
233
+ and asks you to re-run with `--yes` to accept it.
234
+
235
+ | Environment | |
236
+ |---|---|
237
+ | `PGAI_API_KEY` | the API key, instead of signing in (agents, CI) |
238
+ | `PGAI_MON_PASSWORD` | the password of `postgres_ai_mon`. For a new role it is set; for an existing role it is checked by logging in, and never changed. Without it a new role gets a generated password. Not needed for another database on a server PostgresAI already monitors for the organization, with `sslmode=require` or `verify-*` in the URL (see below) |
239
+ | `PGPASSWORD` | the password for a URL without one. With a `postgres_ai_mon` URL it is the password the monitoring box gets, once the server has checked it |
240
+ | `PGSSLMODE` | the `sslmode` for a URL without one (`sslmode` in the URL wins, as in libpq) |
241
+ | `CLICKHOUSE_KEY_ID` + `CLICKHOUSE_KEY_SECRET` | instead of `--clickhouse-key`; read for ClickHouse hosts only |
242
+
243
+ `postgres_ai_mon` is one role for the whole server, so `connect` never changes the password of an
244
+ existing one. If the role exists and you do not have its password (a reconnect after
245
+ `pgai disconnect`, say), `pgai connect <admin-url> --reset-password` sets a new one
246
+ (`PGAI_MON_PASSWORD`, else generated). It is refused while another database on the same server (the same host name, in this
247
+ organization) is monitored with the role, while a box is named `Monitoring <hash>` (the platform
248
+ could not read its URL, so its server is not known), with `--self-hosted`, and for a database
249
+ already connected. After the price is accepted, the run takes the platform's lock on the server (its host
250
+ name and port, in this organization) until the box is requested. A second `--reset-password` for
251
+ the same server exits 3 meanwhile. A run that stopped, or whose box request got no answer, frees the
252
+ server 15 minutes after it took the lock. A platform without the lock answers "set
253
+ `PGAI_MON_PASSWORD`" instead. Anything else that logs in as `postgres_ai_mon` (another organization,
254
+ another host name for the same server, a connect without `--reset-password` running at the same
255
+ time) needs the new password.
256
+
257
+ Another database on a server PostgresAI already monitors for the organization needs neither: with
258
+ `sslmode=require` or `verify-*` in the URL, the role keeps its password, the box's URL carries
259
+ none, and PostgresAI fills in the one it kept from the first `connect` (only over TLS, each query
260
+ parameter once, and only while a box on that server is not deleted, being deleted, or failed to
261
+ delete). The express checkup is skipped then: it runs only as `postgres_ai_mon`, never as the
262
+ admin. The full checkup follows on the box. On a server without TLS the kept password cannot be
263
+ used: set `PGAI_MON_PASSWORD`, or turn TLS on; if nobody has the password, disconnect the server's
264
+ other databases, then `--reset-password` with `PGAI_MON_PASSWORD` set to a new one, and connect
265
+ them again with it (`connect` names them).
266
+
267
+ When preparing an existing role with a supplied password (an admin URL with `PGAI_MON_PASSWORD`,
268
+ or the role's own URL), `connect` also tries a random password as `postgres_ai_mon` to learn
269
+ whether the server checks passwords for this host. Successful PostgresAI Cloud preparation makes
270
+ two attempts: one in the check before the price, and one in preparation. Stopping after the check
271
+ (for example, without accepting the price) makes one. Successful `--self-hosted` preparation
272
+ makes one. Creating or resetting the role with an admin URL, using the platform's stored password,
273
+ or returning an existing box's status makes no random-password attempts. A rejected supplied
274
+ password stops before the random-password probe; that rejection is itself a failed login.
275
+ On a server that checks passwords, each random-password attempt adds one
276
+ `password authentication failed for user "postgres_ai_mon"` line to the server log and counts
277
+ toward failed-login policies (credcheck, fail2ban). On a server that does not (`trust`), `next`
278
+ says that the password was not checked; a password from `PGPASSWORD` is then not used: put it
279
+ in the URL.
280
+
281
+ A role that is not a superuser creates `postgres_ai_mon` only if it can run the whole
282
+ preparation: `CREATEROLE`, `CREATE` on the database, `pg_stat_statements` already installed, and
283
+ on PostgreSQL 16+ `ADMIN OPTION` on `pg_monitor` and `pg_read_all_stats`. Otherwise the SQL is
284
+ printed and nothing is created.
285
+
286
+ The host and the port are the ones in the URL: `host` or `port` in the query string is refused.
287
+ Of the query string, the monitoring box gets `sslmode` and `application_name`; `channel_binding`
288
+ is removed using the collector rule above, before a price is quoted or anything is changed.
289
+ Certificate files (`sslrootcert`, `sslcert`, `sslkey`) are used for the connections from this
290
+ machine only; with `sslmode=verify-ca` or `verify-full` and a private CA in `sslrootcert`, `next`
291
+ says that the box has no copy of that CA.
292
+
293
+ A run that fails does not leave such a role behind. The ClickHouse key and the service state are
294
+ checked before the database is touched. If the platform refuses the launch, or a later step of the
295
+ preparation fails, a role that this run created with a generated password is dropped again, so the
296
+ same command can be run again. (After a platform error that is not a refusal, such as a 5xx or a
297
+ timeout, the role stays: a monitoring box may be starting with it.)
298
+
299
+ An admin URL without a database connects to `PGDATABASE`, or else to the database named like the
300
+ user; the name of the connected database (`<host>[:<port>]/<database>`) uses that database.
301
+
302
+ `pgai connect`, `pgai status` and `pgai databases` use the same words:
303
+
304
+ | `status` | Meaning | Exit code |
305
+ |---|---|---|
306
+ | `connected` | monitoring works; `dashboard_url` is set | 0 |
307
+ | `provisioning` | the monitoring box is being set up; check with `pgai status <name>` | 0 |
308
+ | `disconnecting` | a disconnect is in progress (`pgai status`) | 0 |
309
+ | `disconnected` | the result of `pgai disconnect` | 0 |
310
+ | `action_required` | do what `next` says, then re-run | 3 |
311
+ | `failed` | `next` has the reason | 1 |
312
+
313
+ `pgai init` exits with 130 when cancelled at a prompt (Ctrl-C, Ctrl-D).
314
+
315
+ `--self-hosted` starts `mon local-install` with the monitoring URL, the API key and the ClickHouse
316
+ key in its environment (`PGAI_DB_URL`, `PGAI_API_KEY`, `CLICKHOUSE_*`), not in its arguments.
317
+ `mon local-install` reads `PGAI_DB_URL` like `--db-url`, and `PGAI_API_KEY` only together with
318
+ `PGAI_DB_URL`: an exported `PGAI_API_KEY` alone does not change a plain or `--demo` install.
319
+ `mon targets add [name]` reads `PGAI_DB_URL` when argv has no `postgres://` / `postgresql://` URL
320
+ (a URL in argv wins); with it set, a lone argument other than a plain name (ASCII letters,
321
+ digits, `.`, `_`, `=`, `-`, with no `password=` / `pwd=`) is refused. Its default name is
322
+ `<host>-<database>`. Percent-encode the user name and the password one at a time (all but ASCII
323
+ letters, digits, `-`, `.`, `_` and `~`), and an `@` in the database name or the query. A user name
324
+ or password that pgx would misread or refuse is refused; an `@` after the host is refused, since it
325
+ cannot be told apart from one in a password; so is a control character anywhere in the URL.
326
+ Under sudo, pass `PGAI_DB_URL` on stdin, not with `--preserve-env`: sudo logs the variables it keeps.
327
+ Where sudoers enables I/O logging (`log_input`), sudo records stdin too: read the URL from a file
328
+ of mode 0600 inside the root shell instead.
329
+
144
330
  ### Authentication
145
331
 
146
332
  Authenticate via browser to obtain API key:
@@ -250,12 +436,17 @@ postgresai mon health [--wait <sec>] # Check monitoring services health
250
436
 
251
437
  When `--instance-id <uuid>` (or `PGAI_INSTANCE_ID`) is set, `local-install` forwards the id to the platform, which **adopts** the already-provisioned monitoring instance instead of self-registering a duplicate under an auto-created `postgres-ai-monitoring` project. The CLI then persists the adopted instance's real project to `.pgwatch-config`, so checkup reports upload alongside the rest of that instance's health data. Adoption is awaited (with one automatic retry); if it fails, the CLI warns and reports fall back to the default project until you re-run `local-install`. Without the flag, the legacy self-registration path is byte-for-byte unchanged.
252
438
 
253
- `local-install` writes `.env` in the monitoring directory. It preserves existing `REPLICATOR_PASSWORD` and `VM_AUTH_*` values or generates new random ones when missing; `VM_AUTH_USERNAME` defaults to `vmauth` when absent. The replication password is used by the demo PostgreSQL standby replication user, and the VM auth credentials are required before Docker Compose can provision Grafana datasources. If you run `docker compose` directly or maintain `.env` yourself, set both VM auth values before upgrading. For rotation, run `VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh` from the monitoring directory so `.env`, `sink-prometheus`, and `grafana` update together.
439
+ `local-install` writes `.env` in the monitoring directory. It preserves existing `REPLICATOR_PASSWORD`, `VM_AUTH_*` and the VictoriaMetrics admin-endpoint keys (`VM_DELETE_AUTH_KEY`, `VM_SNAPSHOT_AUTH_KEY`, `VM_FORCE_MERGE_AUTH_KEY`, `VM_PPROF_AUTH_KEY`, added in 0.17) or generates new random ones when missing. The admin keys live on the `sink-prometheus` command line, so `mon update` and `mon update-config` also recreate that container to apply them; `mon restart` (`docker compose restart`) does not; `VM_AUTH_USERNAME` defaults to `vmauth` when absent. The replication password is used by the demo PostgreSQL standby replication user, and the VM auth credentials are required before Docker Compose can provision Grafana datasources. If you run `docker compose` directly or maintain `.env` yourself, set both VM auth values before upgrading. For rotation, run `VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh` from the monitoring directory so `.env`, `sink-prometheus`, and `grafana` update together.
254
440
 
255
441
  #### Monitoring target databases (`mon targets` subgroup)
256
442
  ```bash
257
443
  postgresai mon targets list # List databases to monitor
258
444
  postgresai mon targets add <conn-string> <name> # Add database to monitor
445
+ # Same, URL kept out of argv and out of the sudo log (read from stdin, not exported),
446
+ # unless sudoers enables I/O logging (log_input), which records stdin:
447
+ printf '%s\n' "$URL" | sudo sh -c 'IFS= read -r PGAI_DB_URL; export PGAI_DB_URL; exec postgresai mon targets add <name>'
448
+ # With log_input, read it from a file of mode 0600 inside the root shell:
449
+ sudo sh -c 'IFS= read -r PGAI_DB_URL < /path/to/db-url; export PGAI_DB_URL; exec postgresai mon targets add <name>'
259
450
  postgresai mon targets remove <name> # Remove monitoring target
260
451
  postgresai mon targets test <name> # Test target connectivity
261
452
  ```
@@ -271,6 +462,35 @@ postgresai mon check # System readiness check
271
462
  postgresai mon shell <service> # Open shell to monitoring service
272
463
  ```
273
464
 
465
+ ### PromQL queries (`promql`)
466
+
467
+ Ask a monitoring instance a PromQL question through the platform. The platform
468
+ never connects to the instance: it queues the query as a job, the instance picks
469
+ it up on its next poll, runs it against its own metric store, and posts the
470
+ answer back.
471
+
472
+ ```bash
473
+ # instant query
474
+ pgai promql 'up' --instance <instance-uuid>
475
+
476
+ # range query
477
+ pgai promql 'sum(rate(pgwatch_db_stats_xact_commit[5m]))' \
478
+ --instance <instance-uuid> \
479
+ --range --start 2026-09-21T00:00:00Z --end 2026-09-21T01:00:00Z --step 60
480
+
481
+ # on the box itself, --instance is read from .pgwatch-config
482
+ pgai promql 'up'
483
+ ```
484
+
485
+ `--at <time>` evaluates an instant query at a given moment (ignored with
486
+ `--range`). `--json` prints every point instead of the first and last per series.
487
+ The wait is sized from the instance's poll pacing; `--timeout` overrides it. A
488
+ result too large for the byte budget is trimmed and flagged `truncated`.
489
+
490
+ Requires platform-all !809
491
+ (https://gitlab.com/postgres-ai/platform-all/-/merge_requests/809); until it is
492
+ deployed the command returns a PGRST202 404.
493
+
274
494
  ### MCP server (`mcp` group)
275
495
 
276
496
  ```bash
@@ -299,14 +519,15 @@ required** — the server will not assume an organization. Call `orgs_list` (or
299
519
  run `pgai orgs`) to discover the available ids.
300
520
 
301
521
  Tools exposed:
302
- - `list_issues`: returns the same JSON as `postgresai issues list` (args: `{ org_id, status?, hidden_only?, limit?, offset?, debug? }`).
522
+ - `list_issues`: returns the same JSON as `postgresai issues list` (args: `{ org_id, status?, hidden_only?, limit?, offset?, debug? }`). Unlike the CLI, omitting `status` returns open and closed issues; pass `status: "open"` to match the CLI default.
303
523
  - `view_issue`: view a single issue with its comments (args: `{ issue_id, org_id, debug? }`).
304
- - `create_issue`: create a new issue (args: `{ title, description?, org_id, attachments?, debug? }`).
305
- - `update_issue`: update title/description/status/labels (args: `{ issue_id, org_id, title?, description?, status?, labels?, attachments?, debug? }`).
524
+ - `create_issue`: create a new issue (args: `{ title, description?, org_id, project_id?, labels?, attachments?, is_hidden?, debug? }`).
525
+ - `update_issue`: update title/description/status/labels/is_hidden (args: `{ issue_id, org_id, title?, description?, status?, labels?, attachments?, is_hidden?, debug? }`).
306
526
  - `post_issue_comment`: post a comment (args: `{ issue_id, org_id, content?, parent_comment_id?, attachments?, debug? }`).
307
527
  - `update_issue_comment`: update an existing comment (args: `{ comment_id, org_id, content?, attachments?, debug? }`).
308
528
  - `upload_file`: upload a local file and return the storage URL plus a ready-to-paste markdown link (args: `{ path, org_id, debug? }`).
309
529
  - `download_file`: download a file from storage (args: `{ url, org_id, output_path?, debug? }`).
530
+ - `connect_database`: `pgai connect` as a tool (args: `{ database_url, org_id, provider?, clickhouse_key?, yes?, coupon?, debug? }`). Returns the same JSON (`status`: `connected`, `provisioning`, `disconnecting`, `action_required` or `failed`; `dashboard_url`; `next`). For a billed box, the tool first returns the price with `action_required`; an explicit call with `yes: true` accepts it. `coupon` supplies a promotion code. It does not wait for the monitoring box: call it again to see the status. `database_url` must carry its password, and only the query parameters `sslmode`, `channel_binding`, `application_name` (and `password`); `PGAI_MON_PASSWORD`, `PGPASSWORD` and `CLICKHOUSE_KEY_ID` + `CLICKHOUSE_KEY_SECRET` of the server process are not used, a TLS failure is not retried in plaintext, and `--self-hosted` is CLI-only. For another database on a server PostgresAI already monitors for the organization, an admin `database_url` with `sslmode=require` (or `verify-*`) is enough: PostgresAI fills in the password of `postgres_ai_mon` it keeps.
310
531
 
311
532
  #### `attachments` parameter (issue/comment tools)
312
533
 
@@ -350,11 +571,14 @@ sensitive.
350
571
  ### Issues management (`issues` group)
351
572
 
352
573
  ```bash
353
- postgresai issues list # List issues (shows: id, title, status, created_at; is_hidden only when set)
574
+ postgresai issues list # List OPEN issues (shows: id, title, status, created_at; is_hidden only when set)
575
+ postgresai issues list --status closed # Only closed issues (--status all for both)
354
576
  postgresai issues list --hidden-only # Only hidden issues (PostgresAI staff)
355
577
  postgresai issues view <issueId> # View issue details and comments
356
- postgresai issues create --org-id <id> --title <t> # Create a new issue
578
+ postgresai issues create --org-id <id> <title> # Create a new issue
579
+ postgresai issues create --org-id <id> <title> --hidden # Create a hidden issue (PostgresAI staff)
357
580
  postgresai issues update <issueId> [--title ... --status ...]# Update an existing issue
581
+ postgresai issues update <issueId> --hidden|--no-hidden # Hide / unhide an issue (PostgresAI staff)
358
582
  postgresai issues post-comment <issueId> <content> # Post a comment to an issue
359
583
  postgresai issues update-comment <commentId> <content> # Update an existing comment
360
584
  postgresai issues files upload <path> # Upload a file, print URL + markdown
@@ -370,7 +594,11 @@ postgresai issues files download <url> [-o <path>] # Download a file
370
594
  Hidden issues are staff-internal. `issues list` and `issues view` mark them
371
595
  with `is_hidden: true`; the key is omitted entirely otherwise, so ordinary
372
596
  issues look exactly as they always have. `--hidden-only` lists just the hidden
373
- ones, filtered server-side.
597
+ ones, filtered server-side. `issues create --hidden` creates one, and
598
+ `issues update --hidden` / `--no-hidden` hides or unhides an existing issue
599
+ (MCP: `is_hidden` on `create_issue` / `update_issue`). The CLI does no staff
600
+ check of its own: the platform refuses a visibility change for a non-staff
601
+ credential with a plain error, and the CLI prints it and exits non-zero.
374
602
 
375
603
  Staff access is granted per credential, not per person, and a credential that
376
604
  does not qualify simply sees nothing — `--hidden-only` returns an empty list
@@ -556,7 +784,7 @@ PGPASSWORD=... postgresai checkup \
556
784
  `postgresai checkup <ID> <conn>`); stdout is then a one-key object.
557
785
  - **stderr** carries only human-readable diagnostics (progress, warnings,
558
786
  errors). It never contains report JSON — machine consumers should read stdout
559
- only. Do not parse stderr as JSON.
787
+ only. Do not parse stderr of `checkup` as JSON (that of `pgai connect` is JSON lines, see above).
560
788
  - **Exit codes**: `0` on success; non-zero when the run fails (connection
561
789
  failure, insufficient permissions, an unknown/unavailable check ID, or a
562
790
  failing check). On a non-zero exit, no JSON report object is written to