@carllee1983/dbcli 1.31.0 → 1.37.0

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 (36) hide show
  1. package/.agents/plugins/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/.codex-plugin/plugin.json +41 -0
  4. package/.cursor/rules/dbcli.mdc +541 -0
  5. package/.cursor/skills/dbcli/SKILL.md +106 -0
  6. package/.cursor/skills/dbcli/reference.md +2232 -0
  7. package/.cursor-plugin/plugin.json +36 -0
  8. package/.github/skills/dbcli/SKILL.md +541 -0
  9. package/.github/skills/dbcli/reference.md +2232 -0
  10. package/CHANGELOG.md +81 -0
  11. package/README.md +38 -1
  12. package/README.zh-TW.md +37 -1
  13. package/assets/SKILL.md +86 -2
  14. package/assets/SKILL.zh-TW.md +71 -3
  15. package/assets/reference.md +286 -1
  16. package/assets/tasks/audit-permissions.md +35 -0
  17. package/assets/tasks/connection-health.md +37 -0
  18. package/assets/tasks/migration-review.md +41 -0
  19. package/assets/tasks/pr-database-review.md +38 -0
  20. package/assets/tasks/safe-backfill-verify.md +63 -0
  21. package/assets/tasks/safe-backfill.md +42 -0
  22. package/assets/tasks/schema-drift-review.md +37 -0
  23. package/assets/tasks/slow-endpoint-investigation.md +40 -0
  24. package/dist/cli.mjs +55833 -63508
  25. package/dist/core.mjs +1 -1
  26. package/gemini-extension.json +6 -0
  27. package/package.json +18 -5
  28. package/plugins/dbcli-agent/.codex-plugin/plugin.json +40 -0
  29. package/plugins/dbcli-agent/INSTALL.md +241 -0
  30. package/plugins/dbcli-agent/README.md +89 -0
  31. package/plugins/dbcli-agent/scripts/install-dbcli.sh +15 -0
  32. package/plugins/dbcli-agent/scripts/install-skills.sh +83 -0
  33. package/plugins/dbcli-agent/skills/dbcli/SKILL.md +541 -0
  34. package/plugins/dbcli-agent/skills/dbcli/reference.md +2232 -0
  35. package/skills/dbcli/SKILL.md +541 -0
  36. package/skills/dbcli/reference.md +2232 -0
@@ -0,0 +1,2232 @@
1
+ # dbcli — full command reference
2
+
3
+ Companion to [SKILL.md](SKILL.md). Exhaustive flags, copy-paste examples, `shell`, `completion`, `upgrade`, `migrate` DDL, and extended MongoDB examples.
4
+
5
+ For cross-engine support status, see `docs/feature-matrix.md` in the repository.
6
+
7
+ ## Commands
8
+
9
+ ### init
10
+
11
+ Initialize `.dbcli` configuration file. Typically run manually by the developer — avoid running on behalf of the user unless explicitly requested.
12
+
13
+ ```bash
14
+ dbcli init # Single connection (v1 format)
15
+ dbcli init --system mysql --host localhost --port 3306 --user root --name mydb
16
+ dbcli init --use-env-refs # Store env var references
17
+ dbcli init --no-interactive --force # Non-interactive mode
18
+
19
+ # MongoDB
20
+ dbcli init --system mongodb --uri "mongodb://user:pass@host:27017/mydb?authSource=admin"
21
+ dbcli init --system mongodb --host localhost --port 27017 --user admin --password secret --name mydb
22
+ dbcli init --system mongodb --host localhost --port 27017 --name mydb # No auth
23
+
24
+ # Redis (database = logical DB index)
25
+ dbcli init --system redis --host localhost --port 6379
26
+ dbcli init --system redis --host localhost --port 6379 --password secret --name 0
27
+
28
+ # Elasticsearch
29
+ dbcli init --system elasticsearch --host localhost --port 9200 --user elastic --password changeme
30
+ dbcli init --system elasticsearch --cloud-id "myCluster:dXMtZWFzdC0xLmF3..." --api-key "<base64>"
31
+
32
+ # Multi-connection (v2 format)
33
+ dbcli init --conn-name staging --env-file .env.staging # Named connection with custom env file
34
+ dbcli init --conn-name prod --env-file .env.production --use-env-refs --skip-test
35
+ dbcli init --remove staging # Remove a named connection
36
+ dbcli init --rename staging:production # Rename a connection
37
+ ```
38
+
39
+ **Key options:** `--system`, `--permission`, `--use-env-refs`, `--skip-test`, `--no-interactive`, `--force`, `--conn-name <name>`, `--env-file <path>`, `--remove <name>`, `--rename <old:new>`
40
+
41
+ **MongoDB-specific options:** `--uri <uri>` (full connection URI), `--auth-source <db>` (auth database, default: `admin` when user/password set)
42
+
43
+ **Elasticsearch-specific options:** `--cloud-id <id>` (Elastic Cloud), `--api-key <key>` (ApiKey auth). Other ES fields (`nodes[]`, `protocol`, `caPath`, `rejectUnauthorized`) can be edited directly in `.dbcli`.
44
+
45
+ **Redis note:** the `database` (or `--name`) field is the logical DB index (`"0"` … `"15"`), not a database name.
46
+
47
+ **Multi-connection:** Using `--conn-name` or `--env-file` creates a v2 config with named connections. Each connection can have its own env file and permission level. Existing v1 configs are automatically imported as the `default` connection when upgrading.
48
+
49
+ > **AI agent note on `--use-env-refs`:** If an existing `.dbcli` config contains `{"$env": "DB_HOST"}` style references, the connection values are read from environment variables at runtime. Do NOT re-run `init` to replace these references with actual values — the env-ref format is intentional for CI/CD and multi-environment setups.
50
+
51
+ ### use
52
+
53
+ Switch or display the default database connection (v2 multi-connection config).
54
+
55
+ ```bash
56
+ dbcli use # Show current default connection
57
+ dbcli use staging # Switch default to 'staging'
58
+ dbcli use --list # List all connections (* marks default)
59
+ ```
60
+
61
+ Any command can also use `--use <name>` to temporarily select a connection without changing the default:
62
+
63
+ ```bash
64
+ dbcli query --use staging "SELECT * FROM users LIMIT 10"
65
+ dbcli list --use prod
66
+ ```
67
+
68
+ **Requires v2 config** (created with `dbcli init --conn-name`).
69
+
70
+ ### list
71
+
72
+ List all tables (SQL), collections (MongoDB), keys (Redis), or indices (Elasticsearch).
73
+
74
+ ```bash
75
+ dbcli list
76
+ dbcli list --format json
77
+ dbcli list --include-system # Elasticsearch: include `.system` indices
78
+ ```
79
+
80
+ **Permission:** query-only+
81
+
82
+ > **MongoDB:** Lists collections with estimated document count.
83
+ > **Redis:** Returns up to 100 000 keys via `SCAN MATCH * COUNT 1000`. The header reads `Keys in db <n> (redis):` where `<n>` is the logical DB index.
84
+ > **Elasticsearch:** Returns indices with `documentCount` from `/_stats/docs`; aliases are tagged separately. System indices (names starting with `.`) are hidden unless `--include-system` is passed.
85
+
86
+ ### schema
87
+
88
+ Display table schema or scan entire database.
89
+
90
+ ```bash
91
+ dbcli schema # Scan all tables, save to .dbcli/schemas/
92
+ dbcli schema users # Show single table schema
93
+ dbcli schema users --format json
94
+ dbcli schema --refresh # Detect and apply schema changes
95
+ dbcli schema --reset # Clear all schema data and re-fetch
96
+ dbcli schema --reset --force # Skip confirmation
97
+
98
+ # Per-connection schema isolation (v2 multi-connection config)
99
+ dbcli schema --use staging # Scan staging DB; saves to .dbcli/schemas/staging/
100
+ dbcli schema --use prod # Scan prod DB; saves to .dbcli/schemas/prod/
101
+ ```
102
+
103
+ **Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`, `--use <connection>`, `--sample-size <n>` (mongo only), `--sample-method <random|natural>` (mongo only)
104
+ **Permission:** query-only+
105
+
106
+ **Schema storage (v1.4+):** Schema is persisted as layered files under `.dbcli/schemas/`. With v2 multi-connection config each connection gets its own subdirectory (`.dbcli/schemas/<connection>/`). Run `dbcli schema --use <connection>` once per connection before querying it — otherwise `schema <table>` may return data from the wrong connection's cache.
107
+
108
+ > **Redis:** `schema <key>` is required (no full scan). The output exposes `type`, `ttl`, `size`, and a small `sample` (e.g. first 5 hash keys). `--reset` / `--refresh` are rejected — Redis caches no schema.
109
+ > **Elasticsearch:** `schema [index]` flattens the `_mapping` properties (nested `a.b.c`) and emits each `.fields` multi-field as a separate column (e.g. `text` + `text.keyword`). Full scan iterates all non-system indices and stores per-connection caches alongside SQL engines.
110
+ > **MongoDB:** schema is sampled via `$sample` (default 100, max 1000). `--sample-method natural` switches to `find().limit()`; `random` (default) falls back to natural order on driver error. Output columns surface nested dot-paths with `presence` (0..1) and `redacted: true` flags for blacklist-matched paths. The persisted cache records `sampleMethod` and `sampleSize`; `dbcli doctor` reports them via a `sampled: method=…, size=…` line.
111
+
112
+ ### query
113
+
114
+ Execute SQL query (MySQL/PostgreSQL/MariaDB) or JSON filter/pipeline (MongoDB).
115
+
116
+ ```bash
117
+ # SQL databases
118
+ dbcli query "SELECT * FROM users LIMIT 10"
119
+ dbcli query "SELECT id, email FROM users" --format json
120
+ dbcli query "SELECT * FROM logs" --no-limit
121
+
122
+ # MongoDB: JSON filter (find)
123
+ dbcli query '{"status": "active"}' --collection users
124
+ dbcli query '{"age": {"$gt": 18}}' --collection users --format json
125
+
126
+ # MongoDB: aggregation pipeline
127
+ dbcli query '[{"$match": {"status": "active"}}, {"$group": {"_id": "$role", "count": {"$sum": 1}}}]' --collection users
128
+
129
+ # Redis: any whitelisted command (permission-gated by command)
130
+ dbcli query "GET session:abc"
131
+ dbcli query "HGETALL user:42" --format json
132
+ dbcli query "SCAN 0 MATCH user:* COUNT 100"
133
+ dbcli query "SET feature:flag enabled" # requires read-write+
134
+ dbcli query "DEL stale:key" # requires data-admin+
135
+
136
+ # Elasticsearch: DSL body or Lucene q-string
137
+ dbcli query '{"query":{"match":{"status":"active"}}}' --collection orders
138
+ dbcli query 'status:active AND amount:>100' --index orders --limit 50
139
+
140
+ # Interactive HTML dashboard (see "Interactive HTML dashboard" below)
141
+ dbcli query "SELECT day, dau FROM dau_daily" --ui # open in browser
142
+ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdout
143
+ ```
144
+
145
+ **Options:** `--format <table|json|csv|html>`, `--ui` (open the dashboard in the system browser; implies `--format html`), `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`), `--recovery`
146
+ **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
147
+
148
+ > **MongoDB notes:**
149
+ > - SQL syntax is rejected — use JSON object (filter) or JSON array (pipeline)
150
+ > - `--collection <name>` is required
151
+ > - Auto-limit does not apply; use `$limit` in your pipeline if needed
152
+
153
+ > **Redis notes:**
154
+ > - The first token must be an allow-listed command (`GET`/`SET`/`HGET`/`HSET`/`DEL`/...). Unknown commands are refused.
155
+ > - Permission tier is derived from the command (read → `query-only`, write → `read-write`, delete → `data-admin`, `KEYS`/`FLUSHDB`/`CONFIG`/... → `admin`).
156
+ > - Output is always shaped into rows: scalar replies become `{value: ...}`; arrays become indexed rows; `HGETALL` is folded into a single object.
157
+
158
+ > **Elasticsearch notes:**
159
+ > - `--collection` (or `--index`) is required.
160
+ > - A body that begins with `{` is sent as DSL via `POST /<index>/_search`; otherwise the value is URL-encoded into `?q=...` (Lucene query string) via `GET`.
161
+ > - Hits are flattened: each result row contains `_id` plus dotted-path fields from `_source`. Pass `--format json` to keep nested structures readable.
162
+ > - Query-only mode caps at 1000 hits; `--no-limit` is internally capped at 10 000 (use saved searches / `search_after` for deeper pagination).
163
+
164
+ ### explain
165
+
166
+ **(v1.23)** Read-only query-plan inspection across MySQL/MariaDB and PostgreSQL,
167
+ wrapping `EXPLAIN` / `EXPLAIN ANALYZE` / MariaDB `ANALYZE SELECT` behind one
168
+ interface. Output is a unified `ExplainRow` schema plus severity-coded
169
+ annotations. SQL `SELECT` only.
170
+
171
+ ```bash
172
+ dbcli explain "SELECT * FROM betting_logs WHERE settled_at >= '2026-03-01'"
173
+ dbcli explain @analytics/live-summary # saved query
174
+ dbcli explain @file.sql # @file reference
175
+ dbcli explain --analyze "SELECT ..." # MariaDB ANALYZE SELECT / PG EXPLAIN ANALYZE
176
+ dbcli explain --format json "..." # markdown (default) | json | table
177
+ dbcli explain --bulk @queries.sql # batch from file
178
+ dbcli explain --bulk @analytics/* # glob over saved queries
179
+ ```
180
+
181
+ **Options:** `--analyze` (run the query for real — EXPLAIN ANALYZE / ANALYZE SELECT), `--format <markdown|json|table>` (default `markdown`), `--bulk <input>` (comma-separated `@file` / `@glob` / `@saved-query`).
182
+ **Permission:** query-only+ (no upgrade required).
183
+
184
+ **Annotations:**
185
+
186
+ | Rule | Severity | Triggered when |
187
+ |---|---|---|
188
+ | `full-scan` | red | MySQL `type=ALL` or `key=NULL`; PG `Seq Scan` |
189
+ | `temp-table` | yellow | MySQL `Using temporary` |
190
+ | `filesort` | yellow | MySQL `Using filesort`; PG `Sort Method: external merge` |
191
+ | `cost-estimate-skew` | gray | `--analyze` actual rows / planner rows > 10× |
192
+ | `nested-loop-large` | yellow | PG `Nested Loop` with planner rows > 10,000 |
193
+
194
+ > Notes:
195
+ > - `--analyze` executes the statement — do not use against destructive SQL.
196
+ > - Auto-`LIMIT` is **not** applied to EXPLAIN statements (since v1.23 P1).
197
+
198
+ ### plan
199
+
200
+ Static SQL risk analyzer. Classifies a statement into the same permission tiers
201
+ used by `query` (`query-only` / `read-write` / `data-admin` / `admin`) and lists
202
+ the underlying signals (DML / DDL / multi-statement / unsafe constructs) without
203
+ ever connecting to the database.
204
+
205
+ ```bash
206
+ dbcli plan "SELECT * FROM users"
207
+ dbcli plan "UPDATE users SET name='x'" # human-readable text classification
208
+ dbcli plan "DROP TABLE users" --format json # machine-readable risk report
209
+ ```
210
+
211
+ **Options:** `--format <text|json>` (default `text`).
212
+ **Permission:** n/a (offline analyzer; no connection opened).
213
+
214
+ Use cases:
215
+ - Agents that want to decide whether to call `query` vs `insert` / `update` /
216
+ `delete` before sending SQL.
217
+ - Pre-flight safety check before binding parameters into a saved snippet.
218
+ - Lint hook for code review pipelines that store SQL in source.
219
+
220
+ `plan` does not enforce blacklist or auto-`LIMIT`; those still apply when the
221
+ SQL is actually executed via `query` / `q`.
222
+
223
+ ### q
224
+
225
+ Run a saved query snippet by `@name`. Snippets are parameterised SELECT/WITH statements resolved from three layers, with **local > shared > builtin** precedence (a local file always shadows shared and builtin variants of the same key):
226
+
227
+ - `builtin` — bundled with dbcli (e.g. `@diag/*`); read-only at runtime.
228
+ - `.dbcli-shared/queries/` — committed, team-shared.
229
+ - `.dbcli/queries/` — gitignored, personal override.
230
+
231
+ Engine variants (`name.postgres.sql` / `name.mysql.sql`) at the same layer are merged; the variant matching the active connection's engine is selected at execution time.
232
+
233
+ ```bash
234
+ dbcli q @dau # run with declared defaults
235
+ dbcli q @dau --param days=30 --format json # override a param
236
+ dbcli q @analytics/revenue --param-file params.json
237
+ dbcli q @dau --dry-run # show final SQL + bind values
238
+ dbcli q @dau --no-limit # disable size guard wrap
239
+ dbcli q @analytics/revenue --param days=30 --ui # open dashboard
240
+ dbcli q @analytics/revenue --param days=30 --format html > report.html
241
+ ```
242
+
243
+ **Options:**
244
+ - `--format <table|json|csv|html>` — output format (default: `table`)
245
+ - `--ui` — open the rendered HTML dashboard in the system browser (implies `--format html`; writes to a temp file then invokes `open` / `xdg-open` / `start`)
246
+ - `--param <key=value>` — pass a parameter (repeatable)
247
+ - `--param-file <path>` — JSON object whose keys are param names
248
+ - `--no-limit` — skip the `SELECT * FROM (…) AS _dbcli_guard LIMIT 1000` wrap
249
+ - `--dry-run` — print the bound SQL + values without executing
250
+ - `--use <name>` — pick a v2 named connection
251
+ - `--recovery` — emit a `RecoveryEnvelope` on failure (see `recover`)
252
+
253
+ **Permission:** query-only+
254
+
255
+ #### Snippet file format
256
+
257
+ Each `.sql` file is plain SQL with optional YAML frontmatter inside a leading `-- ---` block. Lines outside frontmatter form the SQL body.
258
+
259
+ ```sql
260
+ -- ---
261
+ -- name: DAU
262
+ -- description: Daily Active Users
263
+ -- engine: postgres # or [postgres, mysql]
264
+ -- params:
265
+ -- days:
266
+ -- type: int # int | string | float | bool | date | datetime
267
+ -- default: 7
268
+ -- required: false
269
+ -- description: lookback window in days
270
+ -- enum: [7, 30, 90]
271
+ -- tags: [analytics]
272
+ -- intent: perf.slow-query # optional; consumed by `queries suggest`
273
+ -- visual: # optional; consumed by `--ui` / `--format html`
274
+ -- title: Daily Active Users
275
+ -- kpis:
276
+ -- - { label: DAU, value_column: dau, format: number }
277
+ -- charts:
278
+ -- - { type: line, title: DAU trend, x: day, y: [dau] }
279
+ -- ---
280
+ SELECT COUNT(DISTINCT user_id) AS dau
281
+ FROM events
282
+ WHERE created_at > NOW() - (:days || ' days')::interval;
283
+ ```
284
+
285
+ Param placeholders use `:name`. They are rewritten to `$1, $2, …` (Postgres) or `?, ?, …` (MySQL) at execution time and passed as bind values — string interpolation is never used.
286
+
287
+ The `visual:` block is documented in detail under [Interactive HTML dashboard](#interactive-html-dashboard) below. Unknown / malformed fields are silently dropped at parse time; the snippet still runs and the dashboard falls back to a sortable table.
288
+
289
+ #### Param type coercion
290
+
291
+ | Declared `type` | Accepts |
292
+ |-----------------|---------|
293
+ | `int` | integer literal |
294
+ | `float` | decimal literal |
295
+ | `bool` | `true` / `false` / `1` / `0` / `yes` / `no` |
296
+ | `string` | any value |
297
+ | `date` | `YYYY-MM-DD` |
298
+ | `datetime` | ISO 8601 |
299
+
300
+ `enum` (optional) restricts the accepted values; mismatch is a hard error. CLI `--param` overrides `--param-file`, which overrides the snippet's `default`.
301
+
302
+ #### Safety invariants
303
+
304
+ - Only `SELECT` / `WITH` (CTE) bodies are accepted; `INSERT/UPDATE/DELETE/DDL` are rejected by the parser.
305
+ - Multi-statement bodies (`SELECT 1; DROP TABLE x`) are rejected.
306
+ - Template syntax inside SQL (`${…}`, `{{…}}`) is rejected — use `:name` parameters.
307
+ - Files exceeding 64 KiB are rejected.
308
+ - `--no-limit` is honoured only at the outermost level; nested subqueries are still wrapped by the size guard.
309
+
310
+ ##### Elasticsearch snippets
311
+
312
+ Body is a JSON DSL `_search` request body. Frontmatter requires an `index:` field (may contain `:param`).
313
+
314
+ Example:
315
+
316
+ -- ---
317
+ -- name: events-by-day
318
+ -- engine: elasticsearch
319
+ -- index: 'events-:date'
320
+ -- params:
321
+ -- date: { type: date, required: true }
322
+ -- user_id: { type: int, required: true }
323
+ -- ---
324
+ {
325
+ "query": {
326
+ "bool": {
327
+ "filter": [
328
+ { "term": { "user_id": :user_id } }
329
+ ]
330
+ }
331
+ },
332
+ "size": 100
333
+ }
334
+
335
+ Substitution rules (type-aware JSON injection):
336
+
337
+ - `int` / `float` / `bool` outside string literals → bare value (`42`, `1.5`, `true`)
338
+ - `string` / `date` / `datetime` outside string literals → JSON-quoted (`"Alice"`, `"2026-05-08"`)
339
+ - Any param inside a JSON string literal → escaped inner form (`"prefix-:name"` works)
340
+
341
+ `script` and `script_fields` are rejected anywhere in the body.
342
+
343
+ Size guard: if `size` is missing, `1000` is injected (or `0` when `aggs` is present); explicit `size > 1000` is overridden with a warning unless `--no-limit`.
344
+
345
+ ##### Redis snippets
346
+
347
+ Body is a single Redis command on one line. Only read-only commands are allowed:
348
+ `GET MGET HGET HGETALL HMGET HKEYS HVALS HLEN HEXISTS LRANGE LLEN LINDEX SMEMBERS SISMEMBER SCARD ZRANGE ZRANGEBYSCORE ZRANGEBYLEX ZSCORE ZCARD ZCOUNT ZRANK TYPE EXISTS TTL PTTL STRLEN OBJECT SCAN HSCAN SSCAN ZSCAN`.
349
+
350
+ `KEYS`, `EVAL`, `FLUSHDB`, `FLUSHALL`, `CONFIG`, `DEBUG`, `SHUTDOWN`, `SCRIPT` and any write command are rejected.
351
+
352
+ Example:
353
+
354
+ -- ---
355
+ -- name: cache-user
356
+ -- engine: redis
357
+ -- params:
358
+ -- id: { type: int, required: true }
359
+ -- ---
360
+ HGETALL user::id
361
+
362
+ Substitution rules: pure raw text — `:name` becomes the value's `String()` form. **Quoting is the snippet author's responsibility**: wrap `:name` in double quotes if the value may contain whitespace. The parser warns when a `string`-typed `:name` is adjacent to non-whitespace and unquoted.
363
+
364
+ Size guard: `LRANGE` / `ZRANGE` stop overridden when `< 0` or `> 1000`; `SCAN` / `HSCAN` / `SSCAN` / `ZSCAN` get `COUNT 1000` injected if absent. `--no-limit` disables.
365
+
366
+ ##### MongoDB snippets
367
+
368
+ File extension: `.mongodb.sql`. Frontmatter must declare `engine: mongodb` and
369
+ `operation: find` or `operation: aggregate`. `target: <collection>` provides a default
370
+ collection that `dbcli q --collection <name>` can override. The body is JSON: an object
371
+ for `find` and an array for `aggregate`. Each `{{param}}` placeholder is JSON-encoded
372
+ at substitution time — strings are quoted and escaped, so an attacker-supplied string
373
+ cannot escape into operator position.
374
+
375
+ Find example (`active-users.mongodb.sql`):
376
+
377
+ -- ---
378
+ -- name: active-users
379
+ -- engine: mongodb
380
+ -- operation: find
381
+ -- target: users
382
+ -- description: Active users matching the given status
383
+ -- params:
384
+ -- status:
385
+ -- type: string
386
+ -- required: true
387
+ -- ---
388
+ {
389
+ "status": {{status}}
390
+ }
391
+
392
+ Aggregate example (`top-orders-by-city.mongodb.sql`):
393
+
394
+ -- ---
395
+ -- name: top-orders-by-city
396
+ -- engine: mongodb
397
+ -- operation: aggregate
398
+ -- target: orders
399
+ -- description: Top order counts per city for a given status
400
+ -- params:
401
+ -- status:
402
+ -- type: string
403
+ -- required: true
404
+ -- limit:
405
+ -- type: int
406
+ -- default: 10
407
+ -- ---
408
+ [
409
+ { "$match": { "status": {{status}} } },
410
+ { "$group": { "_id": "$city", "n": { "$sum": 1 } } },
411
+ { "$sort": { "n": -1 } },
412
+ { "$limit": {{limit}} }
413
+ ]
414
+
415
+ Run with `dbcli q @active-users -p status=active` or `dbcli q @top-orders-by-city -p status=open -p limit=5`. The `q` command applies the same nested-blacklist redaction to results that `query` and `export` do.
416
+
417
+ ### queries
418
+
419
+ Manage saved snippets — discover, inspect, scaffold, and edit local copies. Mutating
420
+ subcommands (`delete`, `rename`, `copy`, `import`) only operate on the local layer
421
+ (`.dbcli/queries/`); builtin and shared snippets are never modified in place.
422
+
423
+ ```bash
424
+ # Discovery / inspection
425
+ dbcli queries list # all snippets (builtin + shared + local)
426
+ dbcli queries list --tag analytics --engine postgres --format json
427
+ dbcli queries list --source local # only personal overrides
428
+ dbcli queries show @dau # frontmatter + SQL
429
+ dbcli queries show @dau --format json # MCP-shaped contract
430
+ dbcli queries search slow query # fuzzy-ranked keyword search across snippets
431
+ dbcli queries search cache --engine postgres --source builtin --limit 5
432
+ dbcli queries suggest perf # browse snippets by intent prefix (v1.11+)
433
+ dbcli queries suggest perf.cache-hit --format json
434
+
435
+ # Authoring
436
+ dbcli queries new @new/sample # scaffold under .dbcli-shared/queries/
437
+ dbcli queries new @scratch --local # personal copy under .dbcli/queries/
438
+ dbcli queries edit @dau # opens local first, falls back to shared
439
+ dbcli queries edit @dau --shared # always edit the shared file
440
+ dbcli queries check # parse all snippets; exit 1 on errors
441
+ dbcli queries check --strict # promote warnings (e.g. missing engine) to errors
442
+
443
+ # Local-layer file management
444
+ dbcli queries delete @scratch # remove local file(s); prompts unless --force
445
+ dbcli queries delete @scratch --force
446
+ dbcli queries rename @scratch @analytics/dau # rename within local layer; preserves engine suffix
447
+ dbcli queries copy @diag/connections @my/connections # fork builtin/shared into local for editing
448
+ dbcli queries import ./hotfix.sql # import an external .sql into .dbcli/queries/
449
+ dbcli queries import ./hotfix.sql --as @diag/custom # override the snippet key
450
+ dbcli queries export @dau --output dau.sql # write snippet body to a file (stdout if omitted)
451
+ dbcli queries export @diag/connections --engine postgres # pick a variant when multiple engines exist
452
+ ```
453
+
454
+ **`list` options:** `--format <table|json|csv>`, `--tag <tag>`, `--engine <postgres|mysql|redis|elasticsearch|all>`, `--source <local|shared|builtin|all>`
455
+ **`show` options:** `--format <table|json|csv>`
456
+ **`search` options:** `--format <table|json>`, `--engine <postgres|mysql|redis|elasticsearch|all>`, `--source <local|shared|builtin|all>`, `--limit <n>` (default 10), `--include-internal` (show fuzzy ranking score). Keyword(s) are fuzzy-matched against name, description, tags, intent.
457
+ **`suggest` options:** `--format <table|json>`, `--engine <postgres|mysql|redis|elasticsearch|all>`, `--source <local|shared|builtin|all>`. Intent prefix-matched against the snippet's `intent` frontmatter field. Common intents: `perf.slow-query`, `perf.cache-hit`, `capacity.size`, `safety.connections`, `monitor.cluster-health`.
458
+ **`new` options:** `--local`, `--edit`
459
+ **`edit` options:** `--shared`
460
+ **`check` options:** `--strict`, `--format <table|json|csv>`
461
+ **`delete` options:** `--force` (skip the confirmation prompt). Refuses to run if `@name` has no local copy.
462
+ **`rename` options:** `--force`. Both names must start with `@`. Engine suffix (`.postgres.sql` / `.mysql.sql`) is preserved; frontmatter `name:` is rewritten to the new key.
463
+ **`copy` options:** *(none)*. Copies every variant (all engines) of the source into the local layer; fails if the destination already has a local copy.
464
+ **`import` options:** `--force` (overwrite existing local file), `--as <name>` (override snippet key; defaults to filename without `.postgres` / `.mysql` suffix). Source must be `.sql` and parse cleanly (frontmatter validated, non-SELECT bodies rejected).
465
+ **`export` options:** `--output <path>` (write to file; otherwise stdout), `--engine <postgres|mysql>` (required when the snippet has multiple engine variants).
466
+
467
+ `--format json` on `list` and `show` emits a stable, machine-readable shape — designed to back a future MCP server without further refactor.
468
+
469
+ ### insert
470
+
471
+ Insert data into a table.
472
+
473
+ ```bash
474
+ dbcli insert users --data '{"name":"Alice","email":"alice@example.com"}'
475
+ dbcli insert users --data '{"name":"Alice"}' --dry-run
476
+ dbcli insert users --data '{"name":"Alice"}' --force
477
+ ```
478
+
479
+ **Options:** `--data <json>`, `--dry-run`, `--force`
480
+ **Permission:** read-write+
481
+
482
+ ### update
483
+
484
+ Update existing data.
485
+
486
+ ```bash
487
+ dbcli update users --where "id=1" --set '{"name":"Bob"}'
488
+ dbcli update users --where "id=1" --set '{"name":"Bob"}' --dry-run
489
+ ```
490
+
491
+ **Options:** `--where <condition>` (required), `--set <json>` (required), `--dry-run`, `--force`
492
+ **Permission:** read-write+
493
+
494
+ ### delete
495
+
496
+ Delete data from a table.
497
+
498
+ ```bash
499
+ dbcli delete users --where "id=1"
500
+ dbcli delete users --where "id=1" --dry-run
501
+ dbcli delete users --where "id=1" --force
502
+ ```
503
+
504
+ **Options:** `--where <condition>` (required), `--dry-run`, `--force`
505
+ **Permission:** data-admin+
506
+
507
+ ### export
508
+
509
+ Export query results to file or stdout.
510
+
511
+ ```bash
512
+ dbcli export "SELECT * FROM users" --format csv --output users.csv
513
+ dbcli export "SELECT * FROM users" --format csv --output users.csv --force # Skip overwrite confirmation
514
+ dbcli export "SELECT * FROM users" --format json | jq '.[]'
515
+ dbcli export "SELECT * FROM users" --format jsonl --output users.ndjson
516
+ dbcli export "SELECT * FROM orders" --format html --output orders.html # standalone dashboard
517
+
518
+ # Elasticsearch (v1.22)
519
+ dbcli export '{"query":{"match":{"status":"active"}}}' --index orders --format jsonl --output orders.ndjson
520
+ dbcli export orders --format csv --output orders.csv # index name as query → match_all + scroll
521
+ dbcli export orders --no-limit --format jsonl # scroll the whole index in batches
522
+ ```
523
+
524
+ **Options:** `--format <json|jsonl|csv|html>` (required), `--output <path>`, `--force`, `--recovery`, `--index <name>` (Elasticsearch), `--no-limit` (Elasticsearch full-index scroll)
525
+ **Permission:** query-only+ — SQL, MongoDB, and **(v1.22)** Elasticsearch.
526
+
527
+ The `html` format emits the same self-contained dashboard as `query --ui` (see [Interactive HTML dashboard](#interactive-html-dashboard)). Because `export` runs raw SQL (no snippet metadata), the HTML report is always rendered as a sortable / filterable table — no KPIs or charts. Use `dbcli q @<name> --format html` (or `--ui`) for the charted view.
528
+
529
+ > **Elasticsearch export (v1.22):** pass a search DSL with `--index <index>` to export the hits, or pass an index name as the query to scroll the whole index via `match_all`. Default cap is 1000 rows; `--no-limit` streams the full index via scroll in batches. Index-level blacklist is checked before export and an audit record is written.
530
+
531
+ ### blacklist
532
+
533
+ Manage sensitive data blacklist to prevent AI access to restricted tables/columns.
534
+
535
+ ```bash
536
+ dbcli blacklist list # Show current blacklist
537
+ dbcli blacklist table add payments # Block entire table
538
+ dbcli blacklist table remove payments # Unblock table
539
+ dbcli blacklist column add users.password # Block specific column
540
+ dbcli blacklist column remove users.password
541
+ ```
542
+
543
+ **Subcommands:** `list`, `table add <name>`, `table remove <name>`, `column add <table.column>`, `column remove <table.column>`
544
+
545
+ ### check
546
+
547
+ Run data health checks on tables.
548
+
549
+ ```bash
550
+ dbcli check users # Check single table
551
+ dbcli check users --format json # JSON output (default)
552
+ dbcli check --all # Check all tables (huge tables auto-skipped)
553
+ dbcli check --all --include-large # Include huge tables
554
+ dbcli check orders --checks nulls,orphans # Specific checks only
555
+ dbcli check orders --sample 10000 # Sample size for large tables
556
+ ```
557
+
558
+ **Checks:** `nulls`, `duplicates`, `orphans`, `emptyStrings`, `rowCount`, `size`
559
+ **Options:** `--all`, `--include-large`, `--checks <types>`, `--sample <number>`, `--format <json|table>`
560
+ **Permission:** query-only+
561
+
562
+ ### diff
563
+
564
+ Compare schema snapshots to detect changes.
565
+
566
+ ```bash
567
+ dbcli diff --snapshot before.json # Save current schema snapshot
568
+ dbcli diff --against before.json # Compare current vs snapshot
569
+ dbcli diff --against before.json --format json
570
+ ```
571
+
572
+ **Options:** `--snapshot <path>`, `--against <path>`, `--format <json|table>`
573
+ **Permission:** query-only+
574
+
575
+ ### snapshot
576
+
577
+ Capture a **result fingerprint** of a query (not schema): `rowCount` plus per-column
578
+ aggregates (null/distinct counts, min/max/sum, an order-independent checksum) and a
579
+ top-level `resultChecksum`. Blacklisted columns are masked at the source by QueryExecutor,
580
+ so the fingerprint is safe to store and share. Use it as a baseline for `assert --against`.
581
+
582
+ ```bash
583
+ dbcli snapshot "SELECT * FROM orders WHERE created_at >= '2026-05-01'" # → .dbcli/snapshots/snap-<timestamp>.json
584
+ dbcli snapshot @analytics/daily-revenue --out base.json # saved query → explicit path
585
+ dbcli snapshot "SELECT status, count(*) FROM orders GROUP BY status" --stdout
586
+ dbcli snapshot "SELECT * FROM orders" --rows --out full.json # also store masked rows
587
+ ```
588
+
589
+ **Options:** `--out <path>` (default `.dbcli/snapshots/snap-<timestamp>.json`), `--rows`, `--stdout`, `--format <json|table>`, `--no-limit`
590
+ **Engines:** SQL only (PostgreSQL / MySQL / MariaDB)
591
+ **Permission:** query-only+
592
+
593
+ ### assert
594
+
595
+ Assert an **invariant** on a query result. Exits `1` on failure (so it composes in
596
+ scripts / CI) unless `--no-fail` is given. Three modes (combinable):
597
+
598
+ - `--expect <condition>` — inline check against the result:
599
+ - `rows > 0` / `rows == 1` … (row count vs operators `> >= < <= == !=`)
600
+ - `value == 5000` / `value == "done"` (single-cell result; project to one column)
601
+ - `col:email not null` · `col:id unique` · `col:amount between 0 and 100` · `col:age >= 18`
602
+ - `--vs <query> --compare rows|value` — reconcile against a second query (cross-check totals/counts).
603
+ - `--against <snapshot> --tolerance <pct>` — compare the current result fingerprint to a saved snapshot. `tolerance 0` requires an exact (order-independent) checksum match; `tolerance 0.01` allows ±1% drift on rowCount and each numeric column sum.
604
+
605
+ ```bash
606
+ dbcli assert "SELECT count(*) FROM orders" --expect "value > 0"
607
+ dbcli assert "SELECT * FROM orders WHERE total < 0" --expect "rows == 0" # no negative totals
608
+ dbcli assert "SELECT email FROM users" --expect "col:email not null"
609
+ dbcli assert "SELECT sum(amount) FROM ledger_a" --vs "SELECT sum(amount) FROM ledger_b" --compare value
610
+ dbcli assert "SELECT * FROM orders" --against base.json --tolerance 0.01
611
+ dbcli assert "SELECT count(*) FROM orders" --expect "value > 100" --no-fail # report only, exit 0
612
+ ```
613
+
614
+ **Options:** `--expect <condition>`, `--vs <query>`, `--compare <rows|value>` (default `value`), `--against <path>`, `--tolerance <pct>` (default `0`), `--no-fail`, `--format <json|table>`
615
+ **Output:** `AssertVerdict` = `{ pass, checks: [{ name, expected, actual, pass }] }`
616
+ **Engines:** SQL only (PostgreSQL / MySQL / MariaDB)
617
+ **Permission:** query-only+
618
+
619
+ #### Verification artifact (--write-verification-artifact)
620
+
621
+ Opt-in flag trio that persists a **VerificationArtifact JSON** (schema v1) under `<cwd>/.dbcli/verification/` after the assertion runs. The artifact is always written to `<cwd>/.dbcli/verification/` (relative to the current working directory), regardless of where the `--config` file is located.
622
+
623
+ | Flag | Required | Description |
624
+ | :--- | :--- | :--- |
625
+ | `--write-verification-artifact` | opt-in | Trigger artifact write. No-op when no verdict has been produced. |
626
+ | `--verification-subject <kind:name>` | yes (when flag is set) | Subject identifier. Format: `<kind>:<name>`. Allowed kinds: `recovery`, `task-pack`, `assertion`, `migration`, `backfill`, `manual`. |
627
+ | `--verification-summary <text>` | no | Free-text summary line stored in the artifact. Default when pass: "Assertion verified the expected state." Default when fail: "Assertion did not verify the expected state." |
628
+
629
+ **Output contract:**
630
+
631
+ - `--format json` — `AssertVerdict` gains `verificationArtifactPath: string` pointing to the written file.
632
+ - `--format table` — an extra `Verification artifact: <path>` line is printed after the verdict table.
633
+ - A `--no-fail` assertion that fails still records status `not_verified` and stores `exitCode: 1` in evidence.
634
+
635
+ **Planned vs Result evidence.** `dbcli skill tasks plan safe-backfill-verify --format json` returns a plan containing a `verification` block with `status: "planned"`. That block is the **planned** evidence definition — it describes which check will run. Running `assert --write-verification-artifact` on the actual data produces **result** evidence (`status: "verified"` or `status: "not_verified"`). The two records are distinct; `"planned"` does **not** indicate that verification has run or passed.
636
+
637
+ > **Casting note:** Postgres returns `count(*)` and `sum()` as bigint (a string in the result set). `value ==` uses strict equality, so `"0" == 0` is false. Cast to `::int` (`count(*)::int`) to ensure numeric comparison works correctly.
638
+
639
+ ```bash
640
+ dbcli assert "SELECT count(*)::int FROM orders WHERE status IS NULL" \
641
+ --expect "value == 0" \
642
+ --write-verification-artifact \
643
+ --verification-subject backfill:safe-backfill-verify
644
+
645
+ dbcli assert "SELECT count(*)::int FROM orders WHERE status IS NULL" \
646
+ --expect "value == 0" \
647
+ --write-verification-artifact \
648
+ --verification-subject backfill:safe-backfill-verify \
649
+ --verification-summary "Post-backfill null-status count is zero."
650
+
651
+ # --no-fail: exits 0 but still records not_verified on failure
652
+ dbcli assert "SELECT count(*)::int FROM orders WHERE status IS NULL" \
653
+ --expect "value == 0" \
654
+ --no-fail \
655
+ --write-verification-artifact \
656
+ --verification-subject backfill:safe-backfill-verify
657
+ ```
658
+
659
+ ### proxy
660
+
661
+ Local-development **observability proxy** for MySQL/MariaDB/PostgreSQL. Inserts dbcli
662
+ between an existing application and its real database: it listens on a configurable
663
+ port, relays TCP frames to the real server, and appends one JSONL event per query to
664
+ `.dbcli/proxy/events.jsonl`. Observe-only — no rewrite, blocking, or query modification.
665
+ Not intended as a production gateway.
666
+
667
+ **Subcommands:** `mysql` · `mariadb` · `postgresql`
668
+
669
+ ```bash
670
+ dbcli proxy mysql --listen 127.0.0.1:3307 --target 127.0.0.1:3306
671
+ dbcli proxy postgresql --listen 127.0.0.1:5434 --target 127.0.0.1:5432
672
+ dbcli proxy mysql --slow-ms 500 --redact literals # redact SQL literals in events
673
+ dbcli proxy mariadb --events ./logs/proxy.jsonl # custom event file
674
+ dbcli proxy postgresql --use prod # infer target from named connection
675
+
676
+ dbcli proxy analyze # analyze .dbcli/proxy/events.jsonl (JSON)
677
+ dbcli proxy analyze --format text --top 10 # human-readable top-10 view
678
+ dbcli proxy analyze --slow-ms 200 --n-plus-one 5 # custom thresholds
679
+ ```
680
+
681
+ **Options:**
682
+ - `--listen <addr:port>` — Address dbcli will listen on (e.g. `127.0.0.1:3307`)
683
+ - `--target <addr:port>` — Address of the real database server to relay to. If omitted, inferred from the active (or `--use`) connection config.
684
+ - `--events <path>` — JSONL event log path (default: `.dbcli/proxy/events.jsonl`)
685
+ - `--slow-ms <ms>` — Threshold in milliseconds above which events are flagged `slow: true` (default: `1000`)
686
+ - `--redact <none|literals>` — Whether to strip SQL literal values from event records (default: `none`; `literals` removes quoted strings and numbers)
687
+ - `--format <text|json>` — Startup / status output format (default: `text`)
688
+ - `--use <name>` — Target a named v2 connection for `--target` inference
689
+
690
+ **Event schema (JSONL):** each line is one event. `type` is one of `proxy_started`, `session_started`, `query_observed`, `query_completed`, `query_errored`, `session_ended`, `parse_error`. A representative `query_completed` line:
691
+ ```json
692
+ { "version": 1, "type": "query_completed", "timestamp": "<ISO-8601>", "engine": "mysql", "sessionId": "pxy_1", "queryId": "qry_pxy_1_1", "client": "127.0.0.1:54321", "target": "127.0.0.1:3306", "sql": "SELECT * FROM users WHERE id = 1", "statement": "SELECT", "tables": ["users"], "durationMs": 42, "requestBytes": 128, "responseBytes": 512, "rowCount": null, "slow": false, "error": null, "tags": [] }
693
+ ```
694
+ `slow` is `true` when `durationMs >= --slow-ms` (also printed as a terminal warning). `rowCount` is best-effort (PostgreSQL command tags; `null` for MySQL). TLS is relayed but not decrypted in v1. Prepared/extended wire protocols are best-effort tagged.
695
+
696
+ **Log rotation:** all writes are serialized through one in-process chain (concurrent sessions never interleave partial lines). The event log auto-rotates to keep one rolling segment — when the next line would reach ~50 MiB or 200,000 entries, the current file is renamed to `<events>.1` (overwriting any prior segment) and a fresh file starts. Worst-case on-disk footprint is ~2× the byte cap.
697
+
698
+ **`proxy analyze`** — offline aggregation of the event log (no DB). Flags: `--events <path>` (default `.dbcli/proxy/events.jsonl`), `--format json|text` (default `json`), `--top <n>` (default 20; text rows + suggestedCommands depth), `--slow-ms <ms>` (default 1000; recomputes slowCount), `--n-plus-one <n>` (default 10), `--no-include-rotated`. JSON report blocks: `summary`, `byFingerprint` (sorted by total time; SELECT entries in the top-N carry `suggestedCommands` for `explain` / `guide missing-index-for`), `slowest`, `errors`, `hotTables`, `repetition` (N+1 suspects). Reads the current log plus the rotated `.1` segment by default.
699
+
700
+ **Engines:** MySQL / MariaDB / PostgreSQL
701
+ **Permission:** n/a (acts as a TCP relay; does not use dbcli's SQL permission model)
702
+
703
+ ### status
704
+
705
+ Show current configuration status (safe for AI agents, no credentials exposed).
706
+
707
+ ```bash
708
+ dbcli status # JSON output (default)
709
+ dbcli status --format text # Human-readable text output
710
+ ```
711
+
712
+ **Output:** `permission`, `system`, `blacklist` summary, `version`
713
+ **Permission:** query-only+
714
+
715
+ ### inspect
716
+
717
+ Read-only snapshot for AI agents. Never emits credentials or blacklisted values.
718
+
719
+ | Flag | Purpose |
720
+ |------|---------|
721
+ | `--format <json\|markdown>` | Output format (default `json`) |
722
+ | `--brief` | Drop sample arrays and trim suggested commands to ≤3 |
723
+ | `--for-agent` | Shortcut for `--format json --brief` |
724
+ | `--no-connect` | Skip the cheap version/object probe (no DB traffic) |
725
+ | `--probe-timeout <ms>` | Hard timeout for the version/object probe (default 1500) |
726
+ | `--require-schema-cache` | Throw `SCHEMA_CACHE_MISSING` (recovery code) when the active SQL connection has no usable schema cache |
727
+ | `--recovery` | On failure, emit a structured `RecoveryEnvelope` to stdout |
728
+
729
+ Example:
730
+
731
+ ```bash
732
+ dbcli inspect --for-agent
733
+ ```
734
+
735
+ Output schema is locked at `schemaVersion: 1`. Sections: `connection`, `permission`, `blacklist`, `objects`, `schemaCache`, `snippets`, `suggestedCommands`, `hints` **(v1.23)**, `warnings`.
736
+
737
+ **`suggestedCommands` (context-aware, v1.23)** — a three-tier weighted list:
738
+ 1. *Bootstrap* — always-safe orientation commands (`blacklist list`, `schema <table>`, ...).
739
+ 2. *Context-aware* — driven by recent activity. When a hot table is detected in the audit log **and** task packs are available, suggests `dbcli skill tasks plan analyze-table-perf --param table=<table>` plus `dbcli queries suggest <intent>` from your snippet intents.
740
+ 3. *Discovery* — broader exploration commands.
741
+
742
+ **`hints` (v1.23)** — a parallel array of human-readable, non-executable notes: the most-queried table from recent audit, the number of available task packs, and the schema-cache size with its last-refresh timestamp. In markdown output they render as a `## Hints` section. Audit reads here are read-only and never throw. Both `suggestedCommands` and `hints` are trimmed under `--for-agent` / `--brief` (≤ 3 hints, single safest command).
743
+
744
+ **Permission:** query-only+
745
+
746
+ ### report
747
+
748
+ Diagnostic report built on top of `inspect`. Reuses inspect context (connection,
749
+ permission, blacklist, snippet inventory) and additionally runs curated built-in
750
+ `@diag/*` snippets grouped into sections.
751
+
752
+ Flags:
753
+ - `--format json|markdown` (default: json)
754
+ - `--section health,capacity,perf` (default: all three)
755
+ - `--brief` — drop evidence rows; keep counts and statuses
756
+ - `--for-agent` — shortcut for `--format json --brief`
757
+ - `--no-connect` — context-only snapshot (skip diagnostics + inspect probe)
758
+ - `--per-snippet-timeout <ms>` (default 3000)
759
+ - `--max-rows-per-evidence <n>` (default 50)
760
+ - `--probe-timeout <ms>` (default 1500, inherited from inspect)
761
+
762
+ Examples:
763
+
764
+ dbcli report --format json
765
+ dbcli report --format markdown --section health,capacity
766
+ dbcli report --for-agent
767
+ dbcli report --no-connect
768
+
769
+ Boundaries:
770
+ - Read-only. Skips snippets whose required params have no default value.
771
+ - Never connects in `--no-connect` mode.
772
+ - MongoDB and no-config workspaces emit a context-only snapshot with a warning.
773
+
774
+ **Permission:** query-only+
775
+
776
+ ### guide
777
+
778
+ Deterministic next-command planner for a fixed set of database goals. Reuses
779
+ `inspect` context (cache-first) and the workspace's saved-query inventory to
780
+ emit an ordered, read-only plan that an AI agent can follow step-by-step.
781
+
782
+ Goals (fixed list):
783
+ - `slow-query` — diagnose slow queries (long-running, locks, cache, indexes).
784
+ - `capacity` — audit storage and memory.
785
+ - `health` — connections, locks, cluster status.
786
+ - `index-usage` — index effectiveness audit.
787
+ - `permissions` — review permission level, blacklist, snippet inventory.
788
+ - `schema-overview` — orient in an unfamiliar database.
789
+
790
+ Flags:
791
+ - `--format json|markdown` (default: json)
792
+ - `--brief` — drop rationale + expects fields
793
+ - `--for-agent` — shortcut for `--format json --brief`
794
+ - `--list` — list available goals and exit
795
+ - `--probe` — refresh inspect context via live probe (default: cache-first)
796
+ - `--probe-timeout <ms>` (default 1500, inherited from inspect)
797
+
798
+ Examples:
799
+
800
+ dbcli guide slow-query
801
+ dbcli guide capacity --format markdown
802
+ dbcli guide --list
803
+ dbcli guide health --for-agent
804
+ dbcli guide schema-overview --probe
805
+
806
+ Boundaries:
807
+ - Read-only. Guide plans commands; it does not execute them.
808
+ - Goal vocabulary is fixed in v1.14.0; user-supplied goals are rejected.
809
+ - Each step carries `risk: 'readonly'` in v1.14.0 (forward-compatible with v1.15.0 recovery).
810
+ - Coexists with `dbcli skill tasks plan` (template-driven). Use guide for ad-hoc goals; use task packs for repeatable workflows.
811
+
812
+ **Permission:** query-only+
813
+
814
+ #### guide missing-index-for (v1.23)
815
+
816
+ A single-query composite-index advisor. Parses one `SELECT`, combines a real
817
+ `EXPLAIN` plan with existing indexes, and emits index candidates each carrying a
818
+ `confidence` (`high` / `medium` / `low`) and a `reason`. Read-only (EXPLAIN +
819
+ index introspection only). MySQL/MariaDB + PostgreSQL.
820
+
821
+ ```bash
822
+ dbcli guide missing-index-for "SELECT ... FROM betting_logs b JOIN hoster_machines hm ON ..."
823
+ dbcli guide missing-index-for @analytics/live-summary # @saved-query
824
+ dbcli guide missing-index-for "..." --format json # yaml (default) | json | markdown
825
+ dbcli guide missing-index-for "..." --min-confidence medium # drop candidates below low|medium|high
826
+ ```
827
+
828
+ **Options:** `--format <yaml|json|markdown>` (default `yaml`), `--min-confidence <low|medium|high>`.
829
+
830
+ Behaviour:
831
+ - Detects existing-index collisions (a single-column index that can be extended into a composite).
832
+ - Functional/expression columns (e.g. `DATE(settled_at)`) and SQL it cannot parse are reported under `warnings`, never as recommendations.
833
+ - Single `SELECT` only — no INSERT/UPDATE/DELETE, stored procedures, or view bodies.
834
+ - Dialects beyond node-sql-parser support fall back to EXPLAIN-only heuristics.
835
+
836
+ **Permission:** query-only+
837
+
838
+ ### recovery
839
+
840
+ Machine-readable error envelope. Two surfaces share one `RecoveryEnvelope`
841
+ shape (`schemaVersion: 1`):
842
+
843
+ 1. **Standalone lookup**: `dbcli recovery --code <CODE>` synthesizes an
844
+ envelope for any known recovery code without needing a real failure.
845
+ 2. **Failing-command opt-in**: pass `--recovery` to `dbcli query` or
846
+ `dbcli q`. On failure, the envelope is written to stdout as JSON, the
847
+ human stderr message is suppressed, and the process exits non-zero.
848
+
849
+ Recovery codes (fixed in v1.15.0):
850
+ - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
851
+ - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
852
+ - `PERMISSION_DENIED` — active permission level forbids the operation.
853
+ - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
854
+ - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
855
+ - `SCHEMA_CACHE_MISSING` — local schema cache missing or stale.
856
+ - `UNKNOWN` — fallback for unclassified errors.
857
+
858
+ Flags (lookup mode):
859
+ - `--code <CODE>` — required unless `--list` is set.
860
+ - `--list` — list all codes and exit.
861
+ - `--format json|markdown` (default: json).
862
+ - `--brief` — drop `rationale` + `expects` from steps.
863
+ - `--for-agent` — shortcut for `--format json --brief`.
864
+ - `--hint <text>` — bind into placeholder steps.
865
+ - `--snippet <name>` — bind snippet placeholder.
866
+ - `--table <name>` — bind table placeholder.
867
+
868
+ Examples:
869
+
870
+ dbcli recovery --code CONN_REFUSED
871
+ dbcli recovery --code BLACKLIST_TABLE --table users --format markdown
872
+ dbcli recovery --list --for-agent
873
+ dbcli query "SELECT * FROM users" --recovery
874
+ dbcli q @diag/missing --recovery
875
+
876
+ Boundaries:
877
+ - Recovery only **suggests** commands; agents (or humans) execute them. No automatic remediation in v1.15.0.
878
+ - As of v1.16.0, `--recovery` is honored on `query`, `q`, `insert`, `update`, `delete`, `export`, `schema`, and `inspect`. Other commands (`report`, `guide`, `doctor`, `migrate`, `init`, `use`, `status`, `list`, `check`, `diff`, `plan`, `shell`, `blacklist`, `completion`, `upgrade`, `skill`) keep their existing error behavior.
879
+ - `dbcli inspect --require-schema-cache` throws `SCHEMA_CACHE_MISSING` when the active SQL connection has no usable schema cache. Combine with `--recovery` for the structured envelope.
880
+ - `BLACKLIST_COLUMN_WRITE` and `PERMISSION_DENIED` envelopes prepend a `risk: 'dry-run'` step (e.g. `dbcli insert <table> --dry-run`) when the failing operation was an INSERT / UPDATE / DELETE.
881
+ - Recovery steps reuse the v1.14.0 `GuideStep` shape, including the full `risk` enum (`readonly` / `dry-run` / `write` / `unknown`).
882
+
883
+ **Permission:** n/a
884
+
885
+ ### recover
886
+
887
+ (v1.17.0+) Inspect or apply the last recovery plan saved by `--recovery`.
888
+
889
+ | Flag | Purpose | Default |
890
+ |---|---|---|
891
+ | `--apply` | Execute the saved plan under risk gating. | off (inspect only) |
892
+ | `--from <path>` | Read the envelope from this file instead of `.dbcli/last-recovery.json`. Accepts raw `RecoveryEnvelope` or `SavedRecoveryEnvelope`. | — |
893
+ | `--allow-write <tier>` | Open the risk gate. Values: `readonly-cmd` (local-side writes) \| `write-cmd` (database writes). | `none` |
894
+ | `--no-verify` | Skip the verify step appended after a successful `--apply`. | off (verify runs by default) |
895
+ | `--format <format>` | `markdown` \| `json`. | `markdown` for inspect, `json` for `--apply` |
896
+
897
+ #### Plan source resolution
898
+
899
+ 1. `--from <path>` if provided. The file must be either a raw `RecoveryEnvelope` or a `SavedRecoveryEnvelope` wrapper. When the file is a `SavedRecoveryEnvelope`, its `cwd` is reused for child-process execution. Strict zod validation; malformed → exit 2 with structured reason.
900
+ 2. Otherwise, `.dbcli/last-recovery.json` (auto-saved on every recovery emission). Validated with the same schema; missing fields, unknown `error.code`, or `cwd` that no longer exists → exit 2.
901
+ 3. Otherwise, exits 2 with `No recovery plan available. Run a command with --recovery to generate one, or pass --from <file>.`
902
+
903
+ #### Code-owned tier (trust boundary)
904
+
905
+ `--apply` derives the canonical execution tier from the per-`error.code` allowlist after parsing argv, **not** from the envelope's `risk` / `dbWrite` / `interactive` fields. Envelope hints can only widen safety (skip more steps); they cannot escalate execution.
906
+
907
+ | Allowlist tier | Meaning | Example commands |
908
+ |---|---|---|
909
+ | `readonly` | local read-only | `dbcli inspect`, `dbcli doctor`, `dbcli blacklist list`, `dbcli schema <table>` |
910
+ | `dry-run` | write subcommand invoked with `--dry-run` | `dbcli update orders --where id=1 --dry-run`, `dbcli q @x --dry-run` |
911
+ | `local-write` | writes local config / cache / blacklist | `dbcli blacklist remove <table>`, `dbcli use <name>`, `dbcli schema --refresh` |
912
+ | `db-write` | mutates the connected database | `dbcli update orders --where id=1 --set …` (no `--dry-run`), `dbcli q @x` (no `--dry-run`) |
913
+ | `interactive` | requires TTY | `dbcli init`, `dbcli init --force` |
914
+
915
+ `insert` / `update` / `delete` / `q` are tier `dry-run` only when argv contains `--dry-run`; otherwise they are tier `db-write` regardless of envelope `risk` claim.
916
+
917
+ #### Risk gate matrix
918
+
919
+ | Allowlist tier | Default | `--allow-write=readonly-cmd` | `--allow-write=write-cmd` |
920
+ |---|---|---|---|
921
+ | `readonly` | run | run | run |
922
+ | `dry-run` | run | run | run |
923
+ | `local-write` | `skipped:risk` | run | run |
924
+ | `db-write` | `skipped:risk` | `skipped:risk` | run |
925
+ | `interactive` | `skipped:interactive` | `skipped:interactive` | `skipped:interactive` |
926
+ | unresolved placeholder in `command` | `skipped:placeholder` | `skipped:placeholder` | `skipped:placeholder` |
927
+ | command fails parse / allowlist | `skipped:unsafe-command` | `skipped:unsafe-command` | `skipped:unsafe-command` |
928
+
929
+ Precedence: envelope `interactive: true` > `placeholder` > `unsafe-command` > allowlist `interactive` > tier-based gating.
930
+
931
+ #### Exit codes
932
+
933
+ | Code | Condition |
934
+ |---|---|
935
+ | 0 | At least one step ran successfully and no step failed. |
936
+ | 1 | A step exited non-zero (fail-fast); see `stoppedAt`. |
937
+ | 2 | Envelope missing or malformed (failed schema validation, or saved `cwd` missing). |
938
+ | 3 | Every step was skipped — open `--allow-write` or fill placeholders. |
939
+
940
+ #### Auto-saved envelope
941
+
942
+ Every command that emits a `RecoveryEnvelope` (`query`, `q`, `insert`, `update`, `delete`, `export`, `schema`, `inspect` — all with `--recovery`) atomically writes the envelope to `.dbcli/last-recovery.json`. The wrapper carries `schemaVersion`, `savedAt`, a sanitized `command` summary, the workspace `cwd`, and the envelope itself. SQL text and `--where` / `--set` / `--data` / `--param` values are redacted as `<sql>` or `<redacted>`. `.dbcli/` is gitignored.
943
+
944
+ #### Verification (P4)
945
+
946
+ Each `RecoveryEnvelope` now carries an optional `verify: GuideStep` (always
947
+ `risk: 'readonly'`, never carries placeholders). `dbcli recover --apply` runs
948
+ the verify step after the main plan, only when `finalStatus === 'ok'` and
949
+ `--no-verify` is not set.
950
+
951
+ | Recovery code | Verify command | Heuristic |
952
+ |---|---|---|
953
+ | CONFIG_MISSING | `dbcli inspect --no-connect --format json` | `connection.name` truthy → passed |
954
+ | CONN_REFUSED / CONN_TIMEOUT / CONN_UNKNOWN / CONN_AUTH_FAILED / CONN_HOST_NOT_FOUND | `dbcli doctor --format json` | exit 0 → passed |
955
+ | PERMISSION_DENIED | `dbcli inspect --for-agent` | exit 0 → passed |
956
+ | BLACKLIST_TABLE | `dbcli inspect --for-agent` | exit 0 → passed |
957
+ | BLACKLIST_COLUMN_WRITE | `dbcli inspect --for-agent` | exit 0 → passed |
958
+ | SNIPPET_NOT_FOUND / SNIPPET_AMBIGUOUS / SNIPPET_PARAM_MISSING | `dbcli queries list --format json` | exit 0 → passed |
959
+ | SCHEMA_CACHE_MISSING | `dbcli inspect --format json` | `schemaCache.available === true` → passed |
960
+ | UNKNOWN | `dbcli doctor --format json` | exit 0 → passed |
961
+
962
+ `verifyStatus` values:
963
+
964
+ - `passed` — heuristic confirmed.
965
+ - `failed` — verifier exited non-zero or timed out.
966
+ - `indeterminate` — verifier exited 0 but expected shape not present, or the
967
+ step was gated (placeholder / unsafe-command); agents should re-check.
968
+
969
+ Exit codes are unchanged — `verifyStatus` is signal, not gate.
970
+
971
+ **Schema additions.** `RecoveryEnvelope.verify?: GuideStep` is additive (no
972
+ `schemaVersion` bump). v1.16 consumers ignore the field.
973
+
974
+ #### Multi-turn `--next` (P2)
975
+
976
+ `dbcli recover --next` returns the single next step in a saved recovery plan,
977
+ given which step the agent just executed and the result of that step. v1 walks
978
+ the plan linearly; future codes may branch on `prevResult.stdoutSummary`
979
+ deterministically.
980
+
981
+ | Flag | Required | Description |
982
+ |---|---|---|
983
+ | `--next` | yes | Activate the multi-turn lookup. |
984
+ | `--after-step <n>` | yes | 1-based order of the step the agent just executed. Range: `[1, envelope.recovery.length]` (or `[1, branches[id].steps.length]` when `--branch` is set). |
985
+ | `--result <value>` | yes | JSON `StepResultSummary` (inline) or `@<path>` to read from a file. |
986
+ | `--branch <id>` | no | Walk a specific branch by id (required on `--next` calls after a fork). See *Connection branching* below. |
987
+ | `--from <path>` | no | Override the auto-saved envelope. |
988
+ | `--format <fmt>` | no | `json` (default) or `markdown`. |
989
+
990
+ `--next` and `--apply` cannot be combined. `--allow-write` and `--no-verify`
991
+ are silently ignored under `--next` (no execution, no verification).
992
+
993
+ **`StepResultSummary` shape**
994
+
995
+ ```ts
996
+ interface StepResultSummary {
997
+ status: 'ok' | 'failed' | 'skipped'
998
+ exitCode?: number
999
+ stdoutSummary?: string // last 4 KB; longer rejected
1000
+ stderrSummary?: string // last 4 KB; longer rejected
1001
+ }
1002
+ ```
1003
+
1004
+ `@<path>` resolves relative to the dbcli invocation cwd. File whole-size cap is
1005
+ 64 KB; per-field 4 KB cap still applies.
1006
+
1007
+ **`NextResult` shape (output)**
1008
+
1009
+ ```ts
1010
+ interface NextResult {
1011
+ schemaVersion: 1
1012
+ kind: 'step' | 'done'
1013
+ source: { kind: 'auto' | 'from'; path: string }
1014
+ errorCode: RecoveryCode
1015
+ cursor: number // step.order when kind='step'; totalSteps when 'done'
1016
+ totalSteps: number
1017
+ step?: GuideStep // present iff kind='step'
1018
+ branchId?: string // set iff agent is currently traversing a branch
1019
+ branchDescription?: string // mirror of branches[branchId].description
1020
+ }
1021
+ ```
1022
+
1023
+ **Connection branching**
1024
+
1025
+ For `CONN_*` recovery codes, the envelope ships an additional `branches` map and a `branchFork` descriptor. Step 1 (`dbcli doctor --format json`) is the fork point: pass the doctor JSON in `--result.stdoutSummary` and `--next` will pick one of four labeled branches:
1026
+
1027
+ | Branch id | When chosen |
1028
+ |---|---|
1029
+ | `doctor-clean` | Doctor reports no errors — likely transient; verify baseline state, then retry. |
1030
+ | `doctor-config-missing` | Doctor flagged a config-level failure (missing / invalid config). Re-init before reconnecting. |
1031
+ | `doctor-auth-error` | Doctor confirms credentials were rejected. Re-init with `--force` to overwrite credentials. |
1032
+ | `doctor-network-error` | Doctor confirms a network-level failure (host / port / DNS / timeout). Inspect and re-init host/port. |
1033
+
1034
+ NextResult sets `branchId` and `branchDescription` after the fork; subsequent `--next` calls must echo `--branch <id>` to walk that branch. If the doctor JSON cannot be parsed or no keyword matches, `--next` falls back to the linear `recovery` plan — branching never causes `--next` to fail. `--apply` ignores `branches` entirely (linear walk unchanged).
1035
+
1036
+ **Exit codes**
1037
+
1038
+ | Exit | Condition |
1039
+ |---|---|
1040
+ | 0 | Returned a step or `done`. |
1041
+ | 2 | Envelope missing/malformed; `--after-step` missing/out-of-range; `--result` missing/malformed; `--next` combined with `--apply`. |
1042
+
1043
+ **Examples**
1044
+
1045
+ ```bash
1046
+ # Walk a 3-step plan to completion
1047
+ dbcli recover --next --after-step 1 --result '{"status":"ok"}' # → step 2
1048
+ dbcli recover --next --after-step 2 --result '{"status":"ok"}' # → step 3
1049
+ dbcli recover --next --after-step 3 --result '{"status":"ok"}' # → done
1050
+
1051
+ # Result read from file (when stdout is large)
1052
+ dbcli recover --next --after-step 1 --result @/tmp/r1.json
1053
+
1054
+ # Markdown for human inspection
1055
+ dbcli recover --next --after-step 1 --result '{"status":"ok"}' --format markdown
1056
+ ```
1057
+
1058
+ **Permission:** n/a (always-allowed lookup; child processes inherit the active permission level).
1059
+
1060
+ ### audit
1061
+
1062
+ (v1.20.0+) Inspect, query, and manage the per-connection audit log written to `.dbcli/audit/<connection>.jsonl`.
1063
+
1064
+ Audit entries are metadata-only by design — never raw SQL bodies, `--param` values, or result cell contents (D3 lock). Redaction is sourced from `tests/helpers/sensitive-output.ts` (same source as `inspect` / `guide` / `recover` agent contracts).
1065
+
1066
+ #### Subcommands
1067
+
1068
+ | Subcommand | Side-effect tier | Purpose |
1069
+ |---|---|---|
1070
+ | `audit tail` | `readonly` | List most recent entries on the current (or `--all`) connection. |
1071
+ | `audit show` | `readonly` | Print a single full entry by id prefix or `--recovery-ref`. |
1072
+ | `audit clear` | `local-write` | Delete `<conn>.jsonl` + rotated `.jsonl.1` from local disk. Requires `--yes` or interactive confirm. |
1073
+ | `audit health` | `readonly` | Render `AuditLogger.getHealth()` snapshot (writer state, lock state, rotation usage). |
1074
+
1075
+ #### `audit tail`
1076
+
1077
+ | Flag | Purpose | Default |
1078
+ |---|---|---|
1079
+ | `--n <N>` | Number of recent entries to print (latest at bottom — D5). | `10` |
1080
+ | `--all` | Merge entries across all connections; output is an envelope array `[{ connection, entry }, ...]` (D-39). | off (current connection only) |
1081
+ | `--for-agent` | Shortcut for `--format json --brief`. Single-connection JSON is a flat array; `--all` JSON is an envelope array. | off |
1082
+ | `--brief` | Drop large redaction fields from the entry; keep `ts / command / target / success` (D-33). | off |
1083
+ | `--format <fmt>` | `table` \| `json`. | `table` |
1084
+
1085
+ Reader behavior (D-41): tail merges `<conn>.jsonl.1` (rotated segment, if present) and `<conn>.jsonl`, sorts by `ts` ascending, then takes the last `--n` entries — so `--n 1000` can span a fresh rotation boundary.
1086
+
1087
+ Examples:
1088
+
1089
+ dbcli audit tail --n 10
1090
+ dbcli audit tail --all --for-agent --n 20
1091
+ dbcli audit tail --format json --brief
1092
+
1093
+ #### `audit show`
1094
+
1095
+ | Flag | Purpose | Default |
1096
+ |---|---|---|
1097
+ | `<id-prefix>` | Positional. UUID or prefix ≥ 4 characters; ambiguous prefix exits 1 with disambiguation hint; prefix < 4 chars exits 1. | — |
1098
+ | `--recovery-ref <id>` | Find the audit entry whose `recovery_ref` field matches this id (exact, not prefix). Mutually exclusive with positional `<id-prefix>` (D-38). | — |
1099
+ | `--all` | Search across all connections. Output is an envelope `{ connection, entry }` (single-hit also envelope, for shape stability — D-36). | off |
1100
+ | `--format <fmt>` | `table` \| `json`. | `table` |
1101
+
1102
+ Examples:
1103
+
1104
+ dbcli audit show 1a2b
1105
+ dbcli audit show --recovery-ref 8f0e-1234-... --format json
1106
+ dbcli audit show 1a2b --all
1107
+
1108
+ #### `audit clear`
1109
+
1110
+ | Flag | Purpose | Default |
1111
+ |---|---|---|
1112
+ | `--yes` | Skip interactive confirmation. Required in non-TTY contexts. | off (interactive confirm) |
1113
+
1114
+ Behavior (D-45 / D-46 / D-47): deletes `<conn>.jsonl` + rotated `<conn>.jsonl.1` for the current connection. Does NOT touch other connections (`--all` is not supported — destructive op cross-connection blast-radius is too high; use `dbcli use` to switch and clear each). Does NOT reset `.dbcli/last-session-id` (D-48). In non-TTY contexts without `--yes`, exits 1 with `Cannot prompt for confirmation in non-interactive session. Use --yes to clear without prompt.`
1115
+
1116
+ Examples:
1117
+
1118
+ dbcli audit clear # interactive (TTY only)
1119
+ dbcli audit clear --yes # CI / scripted
1120
+
1121
+ #### `audit health`
1122
+
1123
+ | Flag | Purpose | Default |
1124
+ |---|---|---|
1125
+ | `--format <fmt>` | `table` \| `json`. | `table` |
1126
+
1127
+ Output reports: writer enabled/disabled, last write result, file-lock state, rotation cap usage (`max_bytes` / `max_entries`). When `audit.enabled = false` (D1 opt-out), `tail` / `show` / `health` still exit 0 and print `Audit is disabled (audit.enabled = false in .dbcli). Use 'dbcli audit health' for details.` (E note).
1128
+
1129
+ #### Boundaries
1130
+
1131
+ - Entries are append-only JSONL; rotation triggers at `~10 MB` or `~1000` entries (whichever first). Previous segment is preserved as `.jsonl.1`.
1132
+ - Bi-directional `recovery_ref` / `audit_ref` linkage is wired on every command that accepts `--recovery`: `query`, `inspect`, `insert`, `update`, `delete`, `export`, `q`, and `schema`. Use `audit tail --recovery-ref <id>` to find the audit entry an envelope was emitted alongside.
1133
+ - Audit writer failures are non-fatal (D6): main command result and exit code are preserved; a stderr warning is emitted. `audit health` surfaces the failure reason.
1134
+ - Reader truncation tolerance: a crash-truncated last line is skipped with a stderr warn `[dbcli audit] skipping truncated last line in <file>`; a mid-file non-JSON line is treated as corruption, exits 1, and points at `dbcli audit clear`.
1135
+
1136
+ #### Exit codes
1137
+
1138
+ | Code | Condition |
1139
+ |---|---|
1140
+ | 0 | Read/list/clear/health succeeded; also `audit.enabled = false` opt-out path (E note). |
1141
+ | 1 | `audit show` — id prefix < 4 chars, ambiguous, or not found; `--recovery-ref` not found; `<id>` and `--recovery-ref` both supplied (D-35 / D-37 / D-38). |
1142
+ | 1 | `audit clear` — non-TTY without `--yes` (D-46). |
1143
+ | 1 | Reader corruption — mid-file non-JSON line in a `.jsonl` segment. |
1144
+
1145
+ **Permission:** n/a
1146
+
1147
+ ### verify
1148
+
1149
+ Run a verification scenario. `verify` **runs** verification scenarios (safe-backfill,
1150
+ migration) and never executes writes/DDL. `verification` **inspects and manages** the
1151
+ local result artifacts those scenarios produce under `.dbcli/verification/`.
1152
+
1153
+ ```bash
1154
+ # Preflight (default): read-only guards + the exact after-write command. No artifact.
1155
+ dbcli verify safe-backfill \
1156
+ --table users \
1157
+ --query "UPDATE users SET status = 1 WHERE status IS NULL" \
1158
+ --verify-query "SELECT count(*)::int AS n FROM users WHERE status IS NULL" \
1159
+ --expect "value == 0"
1160
+
1161
+ # After-write: re-run guards, run the read-back assertion, write a v1 artifact.
1162
+ dbcli verify safe-backfill ... --after-write
1163
+
1164
+ # JSON for agents.
1165
+ dbcli verify safe-backfill ... --format json
1166
+ ```
1167
+
1168
+ Options: `--table` (req), `--query` (req, analyzed not executed), `--verify-query`
1169
+ (req, **plain SELECT only**), `--expect` (req), `--after-write`, `--format <table|json>`,
1170
+ `--subject-name <name>`, `--summary <text>`.
1171
+
1172
+ Guard constraints (fail closed): `--verify-query` must be a **plain `SELECT`** —
1173
+ `EXPLAIN`/`EXPLAIN ANALYZE`, `SHOW`, `DESCRIBE`, and data-modifying CTEs are rejected
1174
+ (on PostgreSQL `EXPLAIN ANALYZE <write>` actually performs the write). The `--query`
1175
+ **UPDATE target must equal `--table`**, compared schema-aware (`public.users` ≠
1176
+ `audit.users`). The persisted artifact stores only a bounded, literal-free label of the
1177
+ verify-query and `--expect` — string, numeric, and dollar-quoted literals are stripped,
1178
+ so raw SQL/values are never written to disk. The printed after-write
1179
+ command is shell-escaped and carries through `--subject-name`/`--summary`/non-default
1180
+ `--format`. For repeated backfills on the same table, pass a unique `--subject-name` so
1181
+ each operation is independently traceable (the subject defaults to `backfill:<table>`).
1182
+
1183
+ Status: `ready`/`blocked` in preflight (no artifact); `verified`, `not_verified`,
1184
+ `blocked`, or `indeterminate` in after-write (artifact written). `blocked` = a guard
1185
+ failed (blacklist/schema/plan/verify-query-not-plain-SELECT/target-table-mismatch);
1186
+ `not_verified` = the read-back contradicted `--expect`; `indeterminate` = the assertion
1187
+ could not produce a trustworthy verdict. Inspect the result with
1188
+ `dbcli verification show <artifact-id>`.
1189
+
1190
+ #### `verify migration`
1191
+
1192
+ Preflight or after-write verification for a schema migration. **This command never
1193
+ executes DDL** — it analyzes the proposed `ALTER TABLE`, runs read-only guards, and
1194
+ (in after-write mode) records evidence after you apply the migration externally.
1195
+
1196
+ ```bash
1197
+ # Preflight: read-only guards + the exact after-write command. Returns ready or blocked.
1198
+ dbcli verify migration \
1199
+ --table users \
1200
+ --ddl "ALTER TABLE users ADD COLUMN verified_at TIMESTAMPTZ" \
1201
+ --verify-query "SELECT count(*)::int AS n FROM users WHERE verified_at IS NOT NULL" \
1202
+ --expect "value == 0"
1203
+
1204
+ # After the migration is applied externally, record evidence:
1205
+ dbcli verify migration ... --after-write
1206
+
1207
+ # JSON for agents.
1208
+ dbcli verify migration ... --format json
1209
+ ```
1210
+
1211
+ | Option | Required | Description |
1212
+ | --- | --- | --- |
1213
+ | `--table <table>` | yes | Table affected by the migration. |
1214
+ | `--ddl <sql>` | yes | Proposed migration DDL, analyzed but never executed. MVP accepts `ALTER TABLE` only. |
1215
+ | `--verify-query <sql>` | yes | Plain `SELECT` for post-migration read-back verification. |
1216
+ | `--expect <expr>` | yes | Assertion expression for the read-back result. |
1217
+ | `--after-write` | no | Run the post-migration assertion and write a v1 artifact. |
1218
+ | `--format <table\|json>` | no | Output format, default `table`. |
1219
+ | `--subject-name <name>` | no | Artifact subject name. Default is the table name. |
1220
+ | `--summary <text>` | no | Optional artifact summary override. |
1221
+
1222
+ Preflight returns `ready` or `blocked` and prints the exact after-write command;
1223
+ **`ready` is not `verified`** — it only means the guards passed. After-write maps the
1224
+ read-back assertion to `verified` / `not_verified` / `indeterminate`, and a failed
1225
+ guard to `blocked`. `CREATE TABLE`, `DROP TABLE`, `CREATE INDEX`, and multi-statement
1226
+ DDL are blocked in the MVP.
1227
+
1228
+ The `ALTER TABLE` target may be `table`, `schema.table`, or `catalog.schema.table`.
1229
+ Each segment is a simple unquoted name (`[A-Za-z_][A-Za-z0-9_]*`) or a quoted
1230
+ identifier — double-quoted (`"…"`), backtick-quoted (`` `…` ``), or bracket-quoted
1231
+ (`[…]`) — so `"user accounts"` or `"tenant-1"."orders"` are accepted. Targets that
1232
+ cannot be fully parsed under this contract (unterminated quotes, unsupported escapes,
1233
+ or more than three parts) are blocked before the after-write assertion with a
1234
+ "could not be parsed" reason, distinct from the `must match --table` mismatch reason.
1235
+
1236
+ ### verification
1237
+
1238
+ (v1.33.0+) Local **VerificationArtifact** inspection and lifecycle surface over
1239
+ `<cwd>/.dbcli/verification/` (always relative to the current working directory,
1240
+ regardless of `--config` location). `list`, `show`, and `summary` are read-only;
1241
+ `prune` is a local lifecycle command — dry-run by default, deleting only with
1242
+ `--execute --force`. Requires no database connection and performs no audit writes.
1243
+
1244
+ **Subcommands:** `list` · `show` · `summary` · `prune`
1245
+
1246
+ #### `verification list`
1247
+
1248
+ List verification artifacts on disk, with optional filters.
1249
+
1250
+ ```bash
1251
+ dbcli verification list --format json
1252
+ dbcli verification list --status verified
1253
+ dbcli verification list --subject backfill
1254
+ dbcli verification list --subject backfill:safe-backfill-verify
1255
+ dbcli verification list --limit 20 --format json
1256
+ dbcli verification list --include-invalid --format json
1257
+ ```
1258
+
1259
+ | Flag | Purpose | Default |
1260
+ |---|---|---|
1261
+ | `--format <json\|table>` | Output format. | `json` |
1262
+ | `--limit <n>` | Maximum number of entries to return. | `20` |
1263
+ | `--status <status>` | Filter by status. One of: `verified`, `not_verified`, `indeterminate`, `blocked`. | all |
1264
+ | `--subject <kind[:name]>` | Filter by subject kind or exact `kind:name`. Allowed kinds: `recovery`, `task-pack`, `assertion`, `migration`, `backfill`, `manual`. | all |
1265
+ | `--include-invalid` | Surface malformed artifact files (normally skipped silently). Invalid files are returned as a separate top-level `invalid` array in JSON output, each entry shaped `{ "path": "...", "filename": "...", "error": "..." }`. When off, `invalid` is `[]`. | off |
1266
+
1267
+ **Missing directory:** if `.dbcli/verification/` does not exist, exits `0` with an
1268
+ empty result (list: `[]`, summary: zero counts).
1269
+
1270
+ **Malformed files:** by default, files that cannot be parsed as valid VerificationArtifact
1271
+ JSON are silently skipped. Pass `--include-invalid` to surface them.
1272
+
1273
+ #### `verification show`
1274
+
1275
+ Print a single verification artifact by its id (the artifact's `id` field) or by
1276
+ the path to the artifact file.
1277
+
1278
+ ```bash
1279
+ dbcli verification show abc123 --format json
1280
+ dbcli verification show abc123 --format table
1281
+ dbcli verification show .dbcli/verification/abc123.json --format json
1282
+ ```
1283
+
1284
+ | Flag | Purpose | Default |
1285
+ |---|---|---|
1286
+ | `<id-or-path>` | Positional. The artifact `id` (format `ver_<base36>_<hex>`), a unique id prefix, the artifact filename, or a path to the file inside `.dbcli/verification/`. | required |
1287
+ | `--format <json\|table>` | Output format. | `json` |
1288
+
1289
+ **Exit codes:**
1290
+ - `0` — artifact found and valid.
1291
+ - `1` — id or path not found, or the artifact file is malformed (parse error).
1292
+
1293
+ #### `verification summary`
1294
+
1295
+ Aggregate verification artifacts into status counts, optionally filtered.
1296
+
1297
+ ```bash
1298
+ dbcli verification summary --format json
1299
+ dbcli verification summary --status not_verified --format json
1300
+ dbcli verification summary --subject migration --format json
1301
+ dbcli verification summary --subject migration:add-status-column --format json
1302
+ ```
1303
+
1304
+ | Flag | Purpose | Default |
1305
+ |---|---|---|
1306
+ | `--format <json\|table>` | Output format. | `json` |
1307
+ | `--status <status>` | Filter to a single status before summarising. | all |
1308
+ | `--subject <kind[:name]>` | Filter by subject kind or exact `kind:name`. | all |
1309
+ | `--latest-only` | Narrow to the latest matching valid artifact plus status counts; the `subjects` breakdown is omitted. Missing artifacts return exit `0` with `latest: null`. | off |
1310
+
1311
+ **Output shape (JSON):**
1312
+ ```json
1313
+ {
1314
+ "storageDir": "/abs/path/.dbcli/verification",
1315
+ "latest": {
1316
+ "path": "...",
1317
+ "id": "ver_...",
1318
+ "createdAt": "2026-06-19T01:02:03.000Z",
1319
+ "status": "verified",
1320
+ "subject": { "kind": "backfill", "name": "safe-backfill-verify" },
1321
+ "summary": "..."
1322
+ },
1323
+ "counts": { "total": 4, "verified": 2, "not_verified": 1, "indeterminate": 0, "blocked": 1, "invalid": 0 },
1324
+ "subjects": [
1325
+ { "subject": { "kind": "backfill", "name": "safe-backfill-verify" }, "total": 3, "latestStatus": "verified", "latestCreatedAt": "2026-06-19T01:02:03.000Z" }
1326
+ ]
1327
+ }
1328
+ ```
1329
+
1330
+ `latest` is `null` when no valid artifacts match the filters.
1331
+
1332
+ #### `verification prune`
1333
+
1334
+ Preview or delete local verification artifacts under `<cwd>/.dbcli/verification/` by
1335
+ explicit retention criteria. **Dry-run by default**; deletes only with `--execute --force`.
1336
+
1337
+ ```bash
1338
+ dbcli verification prune --older-than 30d --format json # preview candidates
1339
+ dbcli verification prune --older-than 30d --execute --force # delete after preview
1340
+ dbcli verification prune --older-than 90d --status verified --keep-latest 50 --execute --force
1341
+ ```
1342
+
1343
+ | Option | Default | Meaning |
1344
+ | --- | --- | --- |
1345
+ | `--format <format>` | `json` | `json` or `table`. JSON is the authoritative contract. |
1346
+ | `--older-than <Nd>` | required | Minimum age in whole days (`7d`, `30d`, `365d`). |
1347
+ | `--keep-latest <n>` | `20` | Always protect the latest N valid artifacts across all subjects/statuses before filters. `0` protects none. |
1348
+ | `--status <status>` | none | Select only valid artifacts with this status. |
1349
+ | `--subject <kind:name>` | none | Select only valid artifacts with this subject. |
1350
+ | `--include-invalid` | `false` | Also select malformed `verification-*.json` files, by file mtime. |
1351
+ | `--execute` | `false` | Delete instead of preview. Requires `--force`. |
1352
+ | `--force` | `false` | Acknowledge deletion; required with `--execute`. |
1353
+
1354
+ Safety: deletion is scoped to regular `verification-*.json` files inside
1355
+ `.dbcli/verification/`; symlinks, directories, and path escapes are skipped with a
1356
+ reason. No database connection is opened and no audit entry is written. JSON output
1357
+ includes `storageDir`, `dryRun`, `cutoff`, `criteria`, `protected`, `candidates`,
1358
+ `deleted`, and `skipped`.
1359
+
1360
+ **Statuses:**
1361
+
1362
+ | Status | Meaning |
1363
+ |---|---|
1364
+ | `verified` | The assertion ran and evidence matched the expected state. |
1365
+ | `not_verified` | The assertion ran and evidence contradicted the expected state. |
1366
+ | `indeterminate` | The assertion ran but evidence was ambiguous (JSON parse failure, missing field, gate skip). |
1367
+ | `blocked` | Verification could not run due to config, permission, schema, placeholder, or safety gates. |
1368
+
1369
+ **Subject kinds:**
1370
+
1371
+ | Kind | Produced by |
1372
+ |---|---|
1373
+ | `recovery` | Post-recovery verification assertions. |
1374
+ | `task-pack` | Assertions generated by task pack plans. |
1375
+ | `assertion` | General-purpose inline assertions. |
1376
+ | `migration` | Schema migration pre/post checks. |
1377
+ | `backfill` | Data backfill verification assertions. |
1378
+ | `manual` | Manually triggered or ad-hoc verification runs. |
1379
+
1380
+ **Storage root:** `<cwd>/.dbcli/verification/` (cwd-relative; independent of `--config`).
1381
+
1382
+ **Permission:** n/a
1383
+
1384
+ ### doctor
1385
+
1386
+ Run diagnostic checks on environment, configuration, connection, and data.
1387
+
1388
+ ```bash
1389
+ dbcli doctor # Colored text output
1390
+ dbcli doctor --format json # JSON output for AI agents
1391
+ ```
1392
+
1393
+ **Checks:**
1394
+ - Environment: Bun version, dbcli version (compares with npm registry)
1395
+ - Configuration: config file exists/valid, permission level, blacklist completeness (detects unprotected sensitive columns)
1396
+ - Connection & Data: database connectivity, schema cache freshness (warns if > 7 days), large table warnings (> 1M rows)
1397
+
1398
+ > **MongoDB SRV diagnostics:** When the active connection uses `mongodb+srv://`, `doctor` reports whether the current runtime can resolve SRV records directly or only through the DNS-over-HTTPS fallback used by dbcli. This helps spot execution-environment DNS restrictions even when Compass can connect.
1399
+
1400
+ **Exit code:** 0 if all pass or warnings only, 1 if any error
1401
+ **Options:** `--format <text|json>`
1402
+
1403
+ ### completion
1404
+
1405
+ Generate shell completion scripts for tab auto-complete.
1406
+
1407
+ ```bash
1408
+ dbcli completion bash # Output bash completion script
1409
+ dbcli completion zsh # Output zsh completion script
1410
+ dbcli completion fish # Output fish completion script
1411
+ dbcli completion --install # Auto-detect shell and install
1412
+ dbcli completion --install zsh # Install for specific shell
1413
+ ```
1414
+
1415
+ **Supported shells:** bash, zsh, fish
1416
+
1417
+ ### upgrade
1418
+
1419
+ Check for updates and self-upgrade dbcli to the latest version from npm.
1420
+
1421
+ ```bash
1422
+ dbcli upgrade # Check and upgrade if newer version available
1423
+ dbcli upgrade --check # Only check, do not upgrade
1424
+ ```
1425
+
1426
+ **Options:** `--check`
1427
+
1428
+ **Background check:** Every command silently checks the npm registry for a newer version (at most once per 24 hours, cached in `.dbcli/version-check.json`). If a newer version is found, a one-line hint is printed to stderr after the command completes. Pass `-q` / `--quiet` to suppress the hint.
1429
+
1430
+ ### `dbcli shell`
1431
+
1432
+ Start an interactive database shell.
1433
+
1434
+ ```bash
1435
+ dbcli shell # Interactive mode with SQL + dbcli commands
1436
+ dbcli shell --sql # SQL-only mode
1437
+ ```
1438
+
1439
+ Inside the shell:
1440
+ - Type SQL statements ending with `;` to execute
1441
+ - Type dbcli commands without the `dbcli` prefix (e.g., `schema users`)
1442
+ - Use Tab for auto-completion (SQL keywords, table names, column names)
1443
+ - Type `.help` for meta commands (.quit, .clear, .format, .history, .timing)
1444
+ - Multi-line SQL: keeps accumulating until `;` is found
1445
+ - History persists across sessions (~/.dbcli_history)
1446
+
1447
+ The REPL flavor depends on the active engine: SQL engines and MongoDB use the
1448
+ form above; **Redis** opens a single-line command REPL (see [Redis › Interactive
1449
+ shell](#interactive-shell)); **Elasticsearch** opens a Kibana Dev Tools-style
1450
+ REPL (v1.22, see [Elasticsearch › Interactive shell](#interactive-shell-v122)).
1451
+
1452
+ ### migrate
1453
+
1454
+ Schema DDL operations. **All commands default to dry-run** — use `--execute` to actually run the SQL. Destructive operations (DROP) also require `--force`.
1455
+
1456
+ ```bash
1457
+ # Create table
1458
+ dbcli migrate create posts \
1459
+ --column "id:serial:pk" \
1460
+ --column "title:varchar(200):not-null" \
1461
+ --column "body:text" \
1462
+ --column "created_at:timestamp:default=now()"
1463
+
1464
+ # Drop table (dry-run by default)
1465
+ dbcli migrate drop posts
1466
+ dbcli migrate drop posts --execute --force # Actually drop
1467
+
1468
+ # Add/drop/alter column
1469
+ dbcli migrate add-column users bio text --nullable
1470
+ dbcli migrate drop-column users temp_field --execute --force
1471
+ dbcli migrate alter-column users name --type "varchar(200)"
1472
+ dbcli migrate alter-column users email --rename user_email
1473
+ dbcli migrate alter-column users status --set-default "'active'"
1474
+ dbcli migrate alter-column users bio --drop-default
1475
+ dbcli migrate alter-column users bio --set-nullable
1476
+ dbcli migrate alter-column users email --drop-nullable
1477
+
1478
+ # Index management
1479
+ dbcli migrate add-index users --columns email --unique
1480
+ dbcli migrate add-index users --columns "last_name,first_name" --name idx_fullname
1481
+ dbcli migrate drop-index idx_fullname --execute --force
1482
+
1483
+ # Constraint management
1484
+ dbcli migrate add-constraint orders --fk user_id --references users.id --on-delete cascade
1485
+ dbcli migrate add-constraint users --unique email
1486
+ dbcli migrate add-constraint users --check "age >= 0"
1487
+ dbcli migrate drop-constraint orders fk_orders_user_id --execute --force
1488
+
1489
+ # Enum (PostgreSQL only — MySQL uses inline ENUM in column type)
1490
+ dbcli migrate add-enum status active inactive suspended
1491
+ dbcli migrate alter-enum status --add-value archived
1492
+ dbcli migrate drop-enum status --execute --force
1493
+ ```
1494
+
1495
+ **Column spec format:** `name:type[:modifier[:modifier...]]`
1496
+ - Modifiers: `pk`, `not-null`, `unique`, `auto-increment`, `default=<value>`, `references=<table>.<column>`
1497
+ - Serial types: `serial`, `bigserial`, `smallserial` (auto-expand per DB dialect)
1498
+
1499
+ **Options (all subcommands):** `--execute`, `--force`, `--config <path>`
1500
+ **Permission:** admin
1501
+
1502
+ **AI agent note:** Always use dry-run first (no `--execute`) to preview generated SQL. Only add `--execute` after confirming the SQL is correct. For DROP operations, both `--execute` and `--force` are required.
1503
+
1504
+ ### skill
1505
+
1506
+ Emit `SKILL.md` (and the companion `reference.md`) to stdout, a file, or an
1507
+ AI-agent platform directory. The skill is the source of truth that lets
1508
+ Claude Code / Gemini / Antigravity / Copilot / Cursor know how to drive dbcli safely.
1509
+
1510
+ ```bash
1511
+ dbcli skill # print SKILL.md to stdout
1512
+ dbcli skill --output ./SKILL.md # write to a file (no platform install)
1513
+ dbcli skill --install claude # install to ~/.claude/skills/dbcli/
1514
+ dbcli skill --install gemini # install to ~/.gemini/skills/dbcli/ (being phased out)
1515
+ dbcli skill --install antigravity # install to ~/.gemini/antigravity-cli/skills/dbcli/
1516
+ dbcli skill --install copilot # install to .github/skills/dbcli/ (repo-local)
1517
+ dbcli skill --install cursor # install to .cursor/skills/dbcli/ (repo-local)
1518
+ dbcli skill --install codex # install to ~/.codex/skills/dbcli/
1519
+ ```
1520
+
1521
+ **Options:**
1522
+ - `--install <platform>` — `claude` | `gemini` | `antigravity` | `copilot` | `cursor` | `codex` | `windsurf`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
1523
+ - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
1524
+
1525
+ **Notes:**
1526
+ - Both files come straight from `assets/SKILL.md` + `assets/reference.md` inside the dbcli package — no runtime rendering. Keep these in sync when shipping a release.
1527
+ - `claude` / `gemini` / `antigravity` install paths are user-global; `copilot` / `cursor` are repo-local under `.github/` / `.cursor/`.
1528
+ - Cursor can install through `/add-plugin dbcli-agent` when available in Cursor's plugin marketplace; this repo includes `.cursor-plugin/plugin.json`. Instruction-file fallback options remain documented in `plugins/dbcli-agent/INSTALL.md#cursor`.
1529
+ - Codex can consume the repo through the Ponytail-style marketplace layout at `.agents/plugins/marketplace.json` and `.codex-plugin/plugin.json`; plugin installs provide the skill from `skills/dbcli/`, and the skill falls back to `bunx @carllee1983/dbcli` when `dbcli` is not on `PATH`.
1530
+ - Agent plugin installation details live in `plugins/dbcli-agent/INSTALL.md`, including Codex, Claude Code, GitHub Copilot CLI, Antigravity (`agy`), and Cursor targets.
1531
+ - `gemini` (Gemini CLI) is retained for now but is being phased out in favour of `antigravity` (Antigravity CLI), Google's successor terminal agent.
1532
+ - Re-running `--install` overwrites the existing skill atomically; no prompt.
1533
+
1534
+ **Permission:** n/a.
1535
+
1536
+ ### skill tasks (Agent Task Packs)
1537
+
1538
+ ```bash
1539
+ dbcli skill tasks list # human table
1540
+ dbcli skill tasks list --format json --tag diagnostics
1541
+ dbcli skill tasks list --engine postgres --source builtin
1542
+ dbcli skill tasks show diagnose-slow-query
1543
+ dbcli skill tasks show diagnose-slow-query --format json
1544
+ dbcli skill tasks plan diagnose-slow-query --param query="SELECT 1"
1545
+ dbcli skill tasks plan diagnose-slow-query --param query="..." --format json
1546
+ ```
1547
+
1548
+ - **list filters:** `--tag <tag>`, `--engine <postgres|mysql|mongodb|redis|elasticsearch>`, `--source <builtin|shared|local>`, `--format <table|json>`.
1549
+ - **show:** prints the full task definition (frontmatter + Agent Notes). Use `--format json` for an agent-friendly contract.
1550
+ - **plan:** resolves `{{param}}` placeholders, validates required parameters, and emits a stable plan. Plans are **plan-only** in this version — dbcli will never execute the resulting commands automatically.
1551
+
1552
+ **Builtin packs:** `diagnose-slow-query` and **(v1.23)** `analyze-table-perf` —
1553
+ a read-only (`plan-only`) pack taking a required `table` parameter that walks
1554
+ `blacklist list` → `schema <table> --format json` → `guide index-usage --format json`.
1555
+ `dbcli inspect` suggests `analyze-table-perf` automatically for the hottest table
1556
+ in recent audit activity. Additional read-only packs ship for common agent
1557
+ workflows: `audit-permissions` (permission/blacklist audit), `safe-backfill`
1558
+ (plan a write with blacklist+schema+risk checks), `schema-drift-review` (cached
1559
+ vs live schema diff), and `connection-health` (reachability/config/capacity
1560
+ triage). Run `dbcli skill tasks list` for the full set.
1561
+
1562
+ ```bash
1563
+ dbcli skill tasks plan analyze-table-perf --param table=betting_logs --format json
1564
+ ```
1565
+
1566
+ Task storage layers:
1567
+
1568
+ | Source | Path | Notes |
1569
+ | --- | --- | --- |
1570
+ | builtin | `assets/tasks/` | shipped with dbcli |
1571
+ | shared | `.dbcli-shared/tasks/` | team-managed, version-controlled |
1572
+ | local | `.dbcli/tasks/` | personal, gitignored |
1573
+
1574
+ Higher tiers override lower tiers by task name. Task name is derived from the
1575
+ file path under the tier root (e.g. `diag/inspect.md` → `diag/inspect`).
1576
+
1577
+ ## Recovery Cookbook (agent walkthroughs)
1578
+
1579
+ End-to-end recovery sessions for the most common failure codes. All examples
1580
+ assume the agent invoked a `--recovery`-capable command and received a
1581
+ `RecoveryEnvelope` (or hit the same envelope via `dbcli recovery --code <CODE>`
1582
+ lookup). See [§recovery](#recovery) for the envelope shape, [§recover](#recover)
1583
+ for `--apply` / `--next` / risk-gate semantics, and [§audit](#audit) for the
1584
+ bi-directional `audit_ref` ⇄ `recovery_ref` pivot.
1585
+
1586
+ ### Scenario index
1587
+
1588
+ | Code | Trigger | Primary remediation | Risk tier |
1589
+ |------|---------|---------------------|-----------|
1590
+ | `CONN_REFUSED` | Database process down or wrong host/port. | `dbcli doctor` → fix host/port → retry. | `readonly` |
1591
+ | `CONN_AUTH_FAILED` | Credentials rejected. | Re-check `.dbcli`/env, rotate credentials, `dbcli init --force` only on explicit user nod. | `readonly` → `interactive` |
1592
+ | `PERMISSION_DENIED` | Active permission level forbids the verb. | `dbcli inspect` to confirm level → escalate via `dbcli init` (human) or run a `--dry-run` instead. | `readonly` + `dry-run` |
1593
+ | `BLACKLIST_TABLE` | Target table is blacklisted. | `dbcli blacklist list` → `blacklist table remove <name>` (local-write tier). | `readonly` + `local-write` |
1594
+ | `BLACKLIST_COLUMN_WRITE` | INSERT/UPDATE touches a blacklisted column. | Re-shape payload to drop the column, or `blacklist column remove`. Envelope prepends a `--dry-run` preview step. | `dry-run` + `local-write` |
1595
+ | `SCHEMA_CACHE_MISSING` | Fresh checkout / new v2 connection / cache wiped. | `dbcli schema --refresh --force` (or `--use <conn>` per-connection). | `readonly` |
1596
+ | `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` | Typo or duplicate snippet name. | `dbcli queries list` → `queries search <kw>` → run correct `@name`. | `readonly` |
1597
+ | `SNIPPET_PARAM_MISSING` | `--param k=v` not supplied. | `dbcli queries show @name` lists required params → re-run with full set. | `readonly` |
1598
+ | `CONFIG_MISSING` | No `.dbcli` in cwd. | `dbcli init` (human-driven). | `interactive` |
1599
+
1600
+ `risk` enum: `readonly` / `dry-run` / `write` / `unknown` (see §recovery boundaries).
1601
+ Allowlist tier: `readonly` / `dry-run` / `local-write` / `db-write` / `interactive` (see [§recover Risk gate matrix](#risk-gate-matrix)).
1602
+
1603
+ ### S1 — CONN_REFUSED end-to-end
1604
+
1605
+ ```bash
1606
+ # 1. Failing call writes envelope to stdout AND .dbcli/last-recovery.json
1607
+ $ dbcli query "SELECT 1" --recovery --format json
1608
+ {
1609
+ "schemaVersion": 1,
1610
+ "error": { "code": "CONN_REFUSED", "message": "..." },
1611
+ "audit_ref": "1f8e...c4d2",
1612
+ "recovery": [
1613
+ { "order": 1, "command": "dbcli doctor --format json", "risk": "readonly", ... },
1614
+ { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly", ... }
1615
+ ],
1616
+ "verify": { "command": "dbcli doctor --format json", "risk": "readonly", ... }
1617
+ }
1618
+
1619
+ # 2. One-shot apply (only readonly + dry-run run by default)
1620
+ $ dbcli recover --apply --format json
1621
+ { "finalStatus": "ok", "executed": [...], "verifyStatus": "passed" }
1622
+ # Exit 0 → root cause cleared (verify probe succeeded).
1623
+
1624
+ # 3. If verify reported `failed` / `indeterminate`, drop into --next for control
1625
+ $ dbcli recover --next --after-step 1 --result '{"status":"failed","exitCode":1}'
1626
+ # → returns a refined step 2 or `kind:"done"` based on the prevResult
1627
+ ```
1628
+
1629
+ ### S2 — PERMISSION_DENIED with implicit `--dry-run` preview
1630
+
1631
+ ```bash
1632
+ $ dbcli update orders --where "id=1" --set '{"status":"shipped"}' --recovery --format json
1633
+ {
1634
+ "error": { "code": "PERMISSION_DENIED", ... },
1635
+ "audit_ref": "9ab0...e711",
1636
+ "recovery": [
1637
+ { "order": 1, "command": "dbcli update orders --where 'id=1' --set '<redacted>' --dry-run", "risk": "dry-run" },
1638
+ { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly" }
1639
+ ]
1640
+ }
1641
+
1642
+ # Default apply runs both steps (dry-run is in-tier).
1643
+ $ dbcli recover --apply
1644
+ ```
1645
+
1646
+ When the failing operation is INSERT/UPDATE/DELETE, the envelope prepends a
1647
+ `risk: 'dry-run'` step (the same write subcommand with `--dry-run`). Run it
1648
+ before any escalation — it both teaches the agent what the SQL looks like and
1649
+ proves the change is well-formed before raising the permission tier.
1650
+
1651
+ ### S3 — BLACKLIST_TABLE (local-write remediation)
1652
+
1653
+ ```bash
1654
+ $ dbcli query "SELECT * FROM audit_logs" --recovery --format json
1655
+ # error.code: BLACKLIST_TABLE
1656
+ # recovery[0]: dbcli blacklist list (risk: readonly)
1657
+ # recovery[1]: dbcli blacklist table remove audit_logs (risk: write — local-write tier)
1658
+
1659
+ # Default --apply: step 1 runs, step 2 skipped:risk → exit 3.
1660
+ $ dbcli recover --apply
1661
+ # To proceed: open the gate to local-write tier ONLY (does not touch DB).
1662
+ $ dbcli recover --apply --allow-write=readonly-cmd
1663
+ { "finalStatus": "ok", "executed": [step1, step2], "verifyStatus": "passed" }
1664
+ ```
1665
+
1666
+ ### S4 — BLACKLIST_COLUMN_WRITE (preview-then-drop)
1667
+
1668
+ ```bash
1669
+ $ dbcli insert users --data '{"name":"a","ssn":"123"}' --recovery
1670
+ # recovery[0]: dbcli insert users --data '<redacted>' --dry-run (risk: dry-run)
1671
+ # recovery[1]: dbcli blacklist list (risk: readonly)
1672
+ # recovery[2]: dbcli blacklist column remove users.ssn (risk: write — local-write)
1673
+
1674
+ # Preferred path: don't widen the blacklist — re-shape the agent's payload to drop ssn.
1675
+ # Apply only the diagnostic prefix (steps 1+2) to confirm what columns are masked:
1676
+ $ dbcli recover --apply
1677
+ # Then re-issue insert without `ssn`.
1678
+ ```
1679
+
1680
+ ### S5 — SCHEMA_CACHE_MISSING (fresh / multi-conn)
1681
+
1682
+ ```bash
1683
+ $ dbcli inspect --require-schema-cache --recovery --format json
1684
+ # error.code: SCHEMA_CACHE_MISSING
1685
+ # recovery[0]: dbcli schema --refresh --force (risk: readonly — populates .dbcli/schemas/)
1686
+ # verify: dbcli inspect --format json (schemaCache.available === true)
1687
+
1688
+ $ dbcli recover --apply
1689
+ # Per-connection cache lives at .dbcli/schemas/<connection>/. If the failure was on
1690
+ # a v2 named connection, the envelope's command already carries `--use <name>`.
1691
+ ```
1692
+
1693
+ ### S6 — SNIPPET_NOT_FOUND with disambiguation
1694
+
1695
+ ```bash
1696
+ $ dbcli q @anaytics/revenue --recovery
1697
+ # typo: anaytics → analytics
1698
+ # recovery[0]: dbcli queries list --format json
1699
+ # recovery[1]: dbcli queries search analytics (or whatever --hint suggests)
1700
+ $ dbcli recover --apply
1701
+ # Agent reads stdoutSummary, identifies the correct @name, then re-issues:
1702
+ $ dbcli q @analytics/revenue --param days=30
1703
+ ```
1704
+
1705
+ ### Multi-turn `--next` walkthrough (3-step plan)
1706
+
1707
+ Use `--next` instead of `--apply` when:
1708
+
1709
+ - `--apply` is too coarse-grained (the agent wants step-by-step inspection).
1710
+ - The plan contains an `interactive` step that `--apply` would skip.
1711
+ - The agent uses its own runner / sandbox and just wants dbcli to drive cursoring.
1712
+
1713
+ `--next` returns one step at a time, given which step the agent **just executed**
1714
+ and a `StepResultSummary` of how it went. dbcli does not persist the cursor —
1715
+ the agent owns `--after-step`.
1716
+
1717
+ ```bash
1718
+ # Envelope already saved at .dbcli/last-recovery.json (3-step plan, CONN_REFUSED).
1719
+
1720
+ # Round 1 — agent reads step 1 from the envelope, executes it itself, then asks
1721
+ # dbcli for the next step.
1722
+ $ dbcli recover --next --after-step 1 --result '{"status":"ok","exitCode":0}' --format json
1723
+ {
1724
+ "schemaVersion": 1,
1725
+ "kind": "step",
1726
+ "errorCode": "CONN_REFUSED",
1727
+ "cursor": 2,
1728
+ "totalSteps": 3,
1729
+ "step": { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly", ... }
1730
+ }
1731
+
1732
+ # Round 2 — bigger stdout, save to file and reference it.
1733
+ $ ./run-step.sh > /tmp/r2.json # agent's own runner; result is StepResultSummary JSON
1734
+ $ dbcli recover --next --after-step 2 --result @/tmp/r2.json
1735
+ { "kind": "step", "cursor": 3, "step": { "order": 3, ... } }
1736
+
1737
+ # Round 3 — last step done.
1738
+ $ dbcli recover --next --after-step 3 --result '{"status":"ok"}'
1739
+ { "kind": "done", "cursor": 3, "totalSteps": 3 }
1740
+ ```
1741
+
1742
+ `StepResultSummary` contract (recap of [§recover Multi-turn](#multi-turn---next-p2)):
1743
+
1744
+ ```ts
1745
+ interface StepResultSummary {
1746
+ status: 'ok' | 'failed' | 'skipped'
1747
+ exitCode?: number
1748
+ stdoutSummary?: string // last 4 KB
1749
+ stderrSummary?: string // last 4 KB
1750
+ }
1751
+ ```
1752
+
1753
+ Truncate to the **last** 4 KB before passing — the head of a huge stdout is
1754
+ usually not what disambiguates next steps.
1755
+
1756
+ Verification is **not** automatic under `--next`. If the agent wants the same
1757
+ verify probe `--apply` runs, it must execute the envelope's `verify` step
1758
+ itself after the plan completes.
1759
+
1760
+ ### Bi-directional pivot (envelope ⇄ audit)
1761
+
1762
+ Every `--recovery`-capable failure (`query`, `inspect`, `insert`, `update`,
1763
+ `delete`, `export`, `q`, `schema`) writes **both** sides of a UUID link:
1764
+
1765
+ - `RecoveryEnvelope.audit_ref` → the `audit.id` for the same failure.
1766
+ - `AuditEntry.recovery_ref` → the envelope's id (also the auto-saved
1767
+ `.dbcli/last-recovery.json` filename trace).
1768
+
1769
+ ```bash
1770
+ # From envelope → audit (forensics on a saved failure)
1771
+ $ ENV_ID=$(jq -r '.id' .dbcli/last-recovery.json) # or read from stdout
1772
+ $ dbcli audit show --recovery-ref "$ENV_ID" --format json
1773
+ # Returns the matching audit entry (full, not brief).
1774
+
1775
+ # From audit → envelope (you have an audit hit, want the structured plan)
1776
+ $ AUDIT_ID=$(dbcli audit tail --for-agent --n 1 | jq -r '.[0].id')
1777
+ $ dbcli audit show "$AUDIT_ID" --format json
1778
+ # Read `recovery_ref` from the entry, then either re-run --recovery against
1779
+ # the original command or load the saved envelope:
1780
+ $ jq '.recovery_ref' .dbcli/last-recovery.json | grep -q "$RECOVERY_REF" \
1781
+ && dbcli recover --format markdown # human inspect
1782
+ || dbcli recover --from /path/to/archived.json --format markdown
1783
+ ```
1784
+
1785
+ Session handoff: a fresh agent that opens `dbcli inspect --for-agent`,
1786
+ `dbcli guide`, `dbcli recover`, or `dbcli recover --apply` gets an
1787
+ `audit_recent: AuditEntryBrief[]` field (last 5 entries) embedded in the JSON
1788
+ output — no extra round-trip to the audit CLI needed for immediate history
1789
+ context.
1790
+
1791
+ ### Risk gate cheat sheet
1792
+
1793
+ Quick reference for what `--apply` runs at each `--allow-write` level. The
1794
+ canonical matrix lives at [§recover Risk gate matrix](#risk-gate-matrix); this
1795
+ table maps it onto common agent intents.
1796
+
1797
+ | Agent intent | Recommended flag | What runs | What's skipped |
1798
+ |---|---|---|---|
1799
+ | Probe-only (read state, learn) | `--apply` (default) | `readonly` + `dry-run` steps | `local-write`, `db-write`, `interactive` |
1800
+ | Local config remediation (e.g. `blacklist remove`) | `--apply --allow-write=readonly-cmd` | + `local-write` | `db-write`, `interactive` |
1801
+ | Database write recovery (rare; trusted plan) | `--apply --allow-write=write-cmd` | + `db-write` | `interactive` |
1802
+ | Interactive step (e.g. `dbcli init`) | Drive manually OR use `--next` | n/a | All interactive steps always skip under `--apply` |
1803
+ | Walk plan step-by-step with own runner | `--next --after-step N --result …` | one step per call | n/a — agent owns cursor + execution |
1804
+
1805
+ Three rules that always apply regardless of `--allow-write`:
1806
+
1807
+ 1. **Tier is code-owned, not envelope-claimed.** The risk gate reads the
1808
+ per-error-code allowlist after parsing argv. An envelope cannot escalate
1809
+ itself by setting `risk: 'readonly'` on a write subcommand — argv decides.
1810
+ 2. **Placeholders block.** A step with unresolved `<token>` placeholders is
1811
+ skipped as `skipped:placeholder` even at `--allow-write=write-cmd`. Bind
1812
+ them at `recovery` lookup time with `--hint` / `--snippet` / `--table`, or
1813
+ ask the user.
1814
+ 3. **Verify is signal, not gate.** `verifyStatus` ∈ `{passed, failed,
1815
+ indeterminate}` reports whether the original failure looks resolved.
1816
+ `recover --apply` exit code is set by step execution, not verification.
1817
+
1818
+ ### Common pitfalls
1819
+
1820
+ - **Stale `.dbcli/last-recovery.json`.** `recover` (no `--apply`) shows the
1821
+ *saved* plan, which may be hours old. Re-run the original command with
1822
+ `--recovery` to refresh it, or pass `--from <file>` to load an archived one.
1823
+ - **`.dbcli/` is gitignored.** Do not check `last-recovery.json` into a repo
1824
+ for "reproducibility"; it contains sanitized command snapshots but the
1825
+ workspace `cwd` only makes sense locally. Use `recover --from <archived.json>`
1826
+ for cross-machine replay.
1827
+ - **`--apply` exit 3 means every step skipped.** Not a failure — it means the
1828
+ default gate was too tight. Either widen with `--allow-write`, fill
1829
+ placeholders, or fall back to `--next` and drive steps manually.
1830
+ - **`--next` does not run verify.** Re-run the original failing command with
1831
+ `--recovery` once the plan is done; if it now succeeds (no envelope on
1832
+ stdout), recovery is complete. Or invoke `envelope.verify.command` yourself.
1833
+ - **Audit writer failures are non-fatal.** If `audit health` reports
1834
+ `lastWriteOk: false`, the main command still completed — but `recovery_ref`
1835
+ ⇄ `audit_ref` linkage is broken for that one call. `audit health` surfaces
1836
+ the underlying error (disk full, EACCES, etc.).
1837
+ - **Cross-connection forensics.** `audit tail --all --for-agent` merges all
1838
+ connections; `audit show <id-prefix> --all` returns an envelope `{connection,
1839
+ entry}` so a fresh agent can tell which DB the failure was against.
1840
+
1841
+ ## Interactive HTML dashboard
1842
+
1843
+ `query`, `q`, and `export` can render results as a single, fully self-contained
1844
+ HTML file backed by a bundled React + Recharts template. The template lives at
1845
+ `assets/ui-template.html` and is installed alongside the binary; no external
1846
+ network, CDN, or runtime is required to view the report.
1847
+
1848
+ ### Entry points
1849
+
1850
+ | Command form | Behaviour |
1851
+ |--------------|-----------|
1852
+ | `dbcli query "<sql>" --ui` | Render to a temp file under `$TMPDIR/dbcli-query-<ts>.html` and open with `open` / `xdg-open` / `start`. |
1853
+ | `dbcli q @<name> --ui` | Same, with snippet metadata (`name`, `description`, `visual:` block). |
1854
+ | `dbcli query "<sql>" --format html` | Print HTML to stdout (pipe, redirect, attach). |
1855
+ | `dbcli q @<name> --format html` | Same, snippet-aware. |
1856
+ | `dbcli export "<sql>" --format html --output report.html` | Write HTML to an explicit path; respects `--force` / overwrite confirmation. |
1857
+
1858
+ `--ui` is a convenience flag — it implies `--format html` and then opens the
1859
+ file. `--ui` and `--format` are mutually compatible; passing both is allowed and
1860
+ behaves as `--ui`.
1861
+
1862
+ ### Data injection contract
1863
+
1864
+ The template ships with a single placeholder, `/*DBCLI_PAYLOAD*/`, which dbcli
1865
+ replaces with:
1866
+
1867
+ ```js
1868
+ window.__DBCLI_PAYLOAD__ = { "meta": {...}, "rows": [...] };
1869
+ ```
1870
+
1871
+ Hardening rules applied before injection:
1872
+
1873
+ - Payload is `JSON.stringify(...)`-encoded.
1874
+ - Every `<` is replaced with `<` so a malicious row containing `</script>`
1875
+ cannot terminate the inline script tag.
1876
+ - Blacklist redaction (`dbcli blacklist`) runs **before** the formatter — masked
1877
+ columns never reach the dashboard.
1878
+ - The replacement uses a function callback (`html.replace(..., () => injection)`)
1879
+ so `$&`-style backreferences in the payload are not interpreted.
1880
+
1881
+ ### `meta` shape
1882
+
1883
+ `meta` is the `SavedQueryMeta` object (see `dbcli queries show @<name> --format json`):
1884
+
1885
+ ```jsonc
1886
+ {
1887
+ "name": "Revenue Trend", // display title
1888
+ "key": "@analytics/revenue", // snippet key, or "raw-sql" / "export"
1889
+ "description": "...", // free text (SQL preview for raw query)
1890
+ "params": [...], // ParamSpec[]
1891
+ "tags": ["analytics"],
1892
+ "intent": "perf.slow-query",
1893
+ "visual": { ... } // optional, see below
1894
+ }
1895
+ ```
1896
+
1897
+ For raw `query` / `export` invocations, `meta.params` is `[]` and
1898
+ `meta.visual` is absent — the dashboard renders a sortable / filterable table.
1899
+
1900
+ ### `visual:` block (snippet frontmatter)
1901
+
1902
+ ```yaml
1903
+ visual:
1904
+ title: Revenue (last :days days) # optional override of meta.name
1905
+ kpis:
1906
+ - label: Total Revenue
1907
+ value_column: total_revenue # must exist in result rows
1908
+ format: currency # currency | number | percent (optional)
1909
+ - label: Orders
1910
+ value_column: order_count
1911
+ format: number
1912
+ charts:
1913
+ - type: line # line | bar | area | pie | scatter
1914
+ title: Daily Revenue
1915
+ x: day # column for X axis
1916
+ y: [revenue] # 1..N columns for series
1917
+ - type: bar
1918
+ title: By Channel
1919
+ x: channel
1920
+ y: [revenue, refunds]
1921
+ ```
1922
+
1923
+ Parser behaviour (`src/core/saved-queries/parser.ts::normaliseVisual`):
1924
+
1925
+ - The block is **optional**. Missing → table-only render.
1926
+ - Items missing required fields (`kpi.label` + `kpi.value_column`, or
1927
+ `chart.type` + `chart.x` + `chart.y[]`) are silently dropped.
1928
+ - Unknown `format` / `type` values are forwarded as strings; the dashboard
1929
+ decides how to render them (unknown chart types fall back gracefully).
1930
+ - The snippet still executes as a normal SQL/DSL query — `visual:` only affects
1931
+ the HTML renderer.
1932
+
1933
+ ### Limitations
1934
+
1935
+ - The dashboard is read-only; there is no in-page editor or re-run button.
1936
+ - Raw `query` / `export` HTML output never shows KPIs or charts (no snippet
1937
+ metadata is available). Use `dbcli q @<name>` for the charted view.
1938
+ - Engine support follows the underlying command: SQL, MongoDB (`--collection`),
1939
+ Redis, and Elasticsearch (`--collection`) all render through the same template.
1940
+ - Very wide / very long result sets render as a single client-side table; for
1941
+ >10k rows prefer `--format csv` / `--format jsonl` and a downstream tool.
1942
+
1943
+ ## MongoDB Support
1944
+
1945
+ MongoDB connections use a JSON-based query model instead of SQL. Treat MongoDB support as a narrower document-database path, not as a full SQL feature equivalent.
1946
+
1947
+ Atlas-style `mongodb+srv://` URIs are supported. `list` and `query` run against the database configured for the connection, and `query` always requires `--collection <name>`.
1948
+
1949
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `q`, `insert`, `update`, `delete`, `export`, `status`, `shell`, `doctor`, `upgrade`, `completion`
1950
+
1951
+ **Limited support:**
1952
+
1953
+ - `schema` samples collection documents to infer field names/types. It does not provide relational constraints, primary keys, foreign keys, or reliable index metadata.
1954
+ - `query` accepts only JSON object filters or aggregation pipeline arrays and always requires `--collection <name>`.
1955
+ - `q` saved-query execution accepts JSON `find` / `aggregate` bodies, requires a `collection` frontmatter field (CLI `--collection` overrides), JSON-encodes every `{{param}}` substitution, and enforces table-level blacklist plus document field masking before rendering.
1956
+ - `insert` inserts one JSON document into the named collection.
1957
+ - `update` accepts a JSON filter in `--where` or simple `key=value` conditions. If `--set` does not use MongoDB update operators such as `$set`, dbcli wraps it in `$set`.
1958
+ - `delete` deletes all documents matching the JSON/simple filter.
1959
+ - `export` accepts the same JSON filter / aggregation syntax as `query`.
1960
+ - MongoDB write paths do not currently provide the same SQL dry-run, relational schema validation, or column-level blacklist filtering guarantees as SQL writes.
1961
+ - `shell` blocks raw SQL for MongoDB; use `query <json> --collection <name>` inside the shell.
1962
+
1963
+ **Not supported (exit with error):** `diff`, `migrate`
1964
+
1965
+ **Not a supported MongoDB target:** `check` is designed for relational health checks and emits SQL-style checks.
1966
+
1967
+ ### MongoDB-specific workflow
1968
+
1969
+ ```bash
1970
+ # 1. Initialize (URI or individual params)
1971
+ dbcli init --system mongodb --uri "mongodb+srv://user:pass@cluster.example.mongodb.net/mydb"
1972
+
1973
+ # 2. List collections
1974
+ dbcli list --format json
1975
+
1976
+ # 3. Query with JSON filter (find) or pipeline (aggregate)
1977
+ dbcli query '{}' --collection orders --format json # All documents
1978
+ dbcli query '{"status": "paid"}' --collection orders # Filter
1979
+ dbcli query '[{"$match": {"status":"paid"}}, {"$count":"total"}]' --collection orders # Pipeline
1980
+
1981
+ # 4. Document writes (permission-gated; no SQL dry-run semantics)
1982
+ dbcli insert orders --data '{"status":"paid","total":42}'
1983
+ dbcli update orders --where '{"status":"pending"}' --set '{"status":"paid"}'
1984
+ dbcli delete orders --where '{"status":"cancelled"}' --force
1985
+ ```
1986
+
1987
+ ### Query syntax
1988
+
1989
+ | Intent | Syntax |
1990
+ |--------|--------|
1991
+ | All documents | `'{}'` |
1992
+ | Field filter | `'{"field": "value"}'` |
1993
+ | Comparison | `'{"age": {"$gt": 18}}'` |
1994
+ | Aggregation | `'[{"$match": {...}}, {"$group": {...}}]'` |
1995
+
1996
+ ## Redis Support
1997
+
1998
+ Redis connections speak Redis commands rather than SQL. The adapter uses Bun's native `Bun.RedisClient` and exposes a permission-gated surface with a query size guard and key-glob blacklist enforcement.
1999
+
2000
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `shell`, `status`, `doctor`, `upgrade`, `completion`
2001
+
2002
+ **Saved queries:** `q` is supported for read-only Redis commands (see "Redis snippets" below).
2003
+
2004
+ **Not supported (exit with error or unsupported error):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`. For writes, run the equivalent Redis command via `query` — the same permission gate applies.
2005
+
2006
+ ### Connection and configuration
2007
+
2008
+ - Required fields: `system: redis`, `host`, `port`. `password` and `database` are optional.
2009
+ - `database` is the **logical DB index** (`"0"` … `"15"`), kept as a string to play nicely with env-ref bindings. `list` and the connection metadata both label it as the active DB.
2010
+ - `connection.timeout` (ms, default 5000) maps to the client's `connectionTimeout`.
2011
+
2012
+ ### Permission classification
2013
+
2014
+ Permission is derived from the command's first token (case-insensitive). Unknown commands are denied even at `admin` tier — they must be added to the allow-list.
2015
+
2016
+ | Tier | Commands |
2017
+ |------|----------|
2018
+ | `query-only` | `GET`, `MGET`, `STRLEN`, `EXISTS`, `TTL`, `PTTL`, `TYPE`, `SCAN`, `HGET`, `HGETALL`, `HKEYS`, `HVALS`, `HLEN`, `HEXISTS`, `HMGET`, `LRANGE`, `LLEN`, `LINDEX`, `SMEMBERS`, `SCARD`, `SISMEMBER`, `ZRANGE`, `ZREVRANGE`, `ZRANGEBYSCORE`, `ZCARD`, `ZSCORE`, `PING`, `ECHO` |
2019
+ | `read-write` | `SET`, `SETEX`, `SETNX`, `PSETEX`, `MSET`, `MSETNX`, `APPEND`, `INCR`/`INCRBY`, `DECR`/`DECRBY`, `HSET`/`HSETNX`/`HMSET`/`HINCRBY`, `LPUSH`/`RPUSH`/`LPOP`/`RPOP`/`LSET`, `SADD`/`SREM`, `ZADD`/`ZREM`, `EXPIRE`/`EXPIREAT`/`PEXPIRE`/`PERSIST`, `RENAME` |
2020
+ | `data-admin` | `DEL`, `UNLINK`, `HDEL` |
2021
+ | `admin` | `FLUSHDB`, `FLUSHALL`, `CONFIG`, `INFO`, `CLIENT`, `DEBUG`, `SHUTDOWN`, `KEYS`, `MONITOR`, `SAVE`, `BGSAVE`, `BGREWRITEAOF`, `REPLICAOF`, `SLAVEOF`, `ACL` |
2022
+
2023
+ ### Schema inspection
2024
+
2025
+ `schema <key>` returns one synthetic row per key with these columns:
2026
+
2027
+ | column | meaning |
2028
+ |--------|---------|
2029
+ | `type` | Redis type (`string` / `hash` / `list` / `set` / `zset` / `stream` / `none`) |
2030
+ | `ttl` | `<n>s`, `no expiry`, or `missing` |
2031
+ | `size` | `STRLEN` / `HLEN` / `LLEN` / `SCARD` / `ZCARD` / `XLEN` depending on type |
2032
+ | `sample` | First 5 hash field names (hash only) |
2033
+
2034
+ `schema` (no key) and `--refresh` / `--reset` are rejected — there is no full-database schema cache for Redis.
2035
+
2036
+ ### Recommended `query` patterns
2037
+
2038
+ ```bash
2039
+ # Read
2040
+ dbcli query "GET feature:flag"
2041
+ dbcli query "HGETALL user:42" --format json
2042
+ dbcli query "LRANGE queue:jobs 0 9"
2043
+
2044
+ # Iterate keys (paginated; never use KEYS — admin-only)
2045
+ dbcli query "SCAN 0 MATCH session:* COUNT 200"
2046
+
2047
+ # Write (requires read-write+)
2048
+ dbcli query "SET counter 1"
2049
+ dbcli query "EXPIRE session:abc 3600"
2050
+ dbcli query "HSET user:42 name Alice"
2051
+
2052
+ # Delete (requires data-admin+)
2053
+ dbcli query "DEL temp:lock"
2054
+ dbcli query "HDEL user:42 lastLogin"
2055
+ ```
2056
+
2057
+ ### Size guard (`query --no-limit` / shell `.no-limit`)
2058
+
2059
+ The adapter rewrites unbounded reads before dispatch and truncates oversized replies after:
2060
+
2061
+ | Strategy | Commands | Behavior |
2062
+ |----------|----------|----------|
2063
+ | inject/cap `COUNT` | `SCAN`, `HSCAN`, `SSCAN`, `ZSCAN` | adds `COUNT 1000` when absent; caps a larger `COUNT` to 1000 |
2064
+ | clamp `stop` | `LRANGE`, `ZRANGE`, `ZREVRANGE` | rewrites `stop` so the span ≤ 1000 (`-1` becomes `start+999`) |
2065
+ | inject/cap `LIMIT` | `ZRANGEBYSCORE` | appends `LIMIT 0 1000` when absent; caps a larger count |
2066
+ | client truncate | `HGETALL`, `HKEYS`, `HVALS`, `SMEMBERS`, `KEYS` | keeps the first 1000 entries |
2067
+
2068
+ Rewrites emit a `REDIS_SIZE_REWRITE` warning; truncations emit `REDIS_SIZE_TRUNCATE`. Both surface in the result's `warnings[]`. Pass `--no-limit` (CLI) or toggle `.no-limit on` (shell) to disable all guards.
2069
+
2070
+ ### Blacklist enforcement
2071
+
2072
+ Blacklist rules are enforced as **Redis-native key globs** (`*`, `?`, `[abc]`, `[a-z]`):
2073
+
2074
+ ```bash
2075
+ dbcli blacklist add 'secrets:*' # register a key-glob rule
2076
+ dbcli query "GET secrets:api_key" # → BlacklistRejection (exit non-zero)
2077
+ dbcli query "MGET safe:k secrets:api" # → rejected (any matching key fails the whole command)
2078
+ dbcli query "KEYS secrets:*" # → rejected (pattern overlaps a rule)
2079
+ dbcli query "KEYS *" # → returns only non-blacklisted keys
2080
+ ```
2081
+
2082
+ Rejections are written to the audit log with `success: false` and `metadata.rejection_reason: 'blacklist'` + `matched_pattern`.
2083
+
2084
+ ### Value / hash-field masking (v1.22)
2085
+
2086
+ Where the key-glob blacklist *rejects*, masking instead *redacts*: a matched read still
2087
+ runs, but the sensitive value comes back as `[REDACTED]` so an agent can use the command
2088
+ without ever seeing it. Add an optional `redis.mask` block to `.dbcli`:
2089
+
2090
+ ```yaml
2091
+ redis:
2092
+ mask:
2093
+ - keyPattern: 'session:*' # whole value redacted on read
2094
+ - keyPattern: 'user:*'
2095
+ fields: [password, token] # only these hash fields redacted
2096
+ ```
2097
+
2098
+ - Applies on reads: `GET`, `GETRANGE`, `HGETALL`, `HGET`, `HMGET`, `HVALS`.
2099
+ - A rule without `fields` redacts the entire value; with `fields` only the named hash fields are redacted.
2100
+ - Masking and key-glob rejection coexist, and **rejection always wins over masking** — a key that matches a blacklist rule is rejected, never merely masked.
2101
+
2102
+ ### Interactive shell
2103
+
2104
+ `dbcli shell` on a Redis connection opens a single-line REPL:
2105
+
2106
+ ```text
2107
+ $ dbcli --use local-redis shell
2108
+ Redis shell: single-line commands; SCAN/LRANGE auto-capped at 1000. Type `.no-limit on` to bypass (unsafe).
2109
+ redis> SCAN 0 # wire args become: SCAN 0 COUNT 1000 (REDIS_SIZE_REWRITE)
2110
+ redis> HGETALL bighash # >1000 fields → kept 1000 (REDIS_SIZE_TRUNCATE)
2111
+ redis> .no-limit on # bypass size guard for this session
2112
+ redis> GET secrets:api_key # → REDIS_BLACKLIST / BlacklistRejection if blacklisted
2113
+ redis> .exit
2114
+ ```
2115
+
2116
+ Tab completion offers Redis command names and known key prefixes; history persists to `~/.dbcli_history`.
2117
+
2118
+ ### Limitations
2119
+
2120
+ - No `--dry-run` for writes — Redis commands execute immediately. Pair writes with a confirming read (`GET`, `HGETALL`, `EXISTS`).
2121
+ - No transaction wrapping (`MULTI`/`EXEC`). Submit one command at a time.
2122
+ - `KEYS` requires `admin`. Prefer `SCAN` for routine work.
2123
+ - Blacklist enforcement covers **keys** (Redis-native globs); value / hash-field **masking** is available via the `redis.mask` config block (v1.22).
2124
+
2125
+ ## Elasticsearch Support
2126
+
2127
+ Elasticsearch connections speak the REST API. The adapter is fetch-based (no SDK) and supports HTTPS, custom CA, API key, basic auth, and Cloud ID.
2128
+
2129
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `export` (v1.22), `shell` (v1.22), `status`, `doctor`, `upgrade`, `completion`
2130
+
2131
+ **Saved queries:** `q` is supported for ES JSON DSL bodies (see "Elasticsearch snippets" below).
2132
+
2133
+ **Not supported (use external tooling):** `insert`, `update`, `delete`, `check`, `diff`, `migrate`. The permission classifier already understands `_doc` / `_update` / `_bulk` so future write surfaces can be wired in without changing tiers.
2134
+
2135
+ ### Connection and configuration
2136
+
2137
+ - Either `host` + `port` (default `https://localhost:9200`) or `nodes: [...]` (first node is used) or `cloudId`.
2138
+ - Auth precedence: `apiKey` → `user`/`password` (HTTP Basic). Leave both unset for an open cluster.
2139
+ - `protocol` defaults to `https`. For TLS quirks: `caPath` (path to a PEM bundle) and `rejectUnauthorized: false` (last resort).
2140
+ - `connection.timeout` (ms, default 5000) is wired to `AbortController` on every request.
2141
+
2142
+ ### Permission classification
2143
+
2144
+ Each REST request is mapped to a SQL-shaped tier based on method + path:
2145
+
2146
+ | ES surface | Mapped to | Permission |
2147
+ |------------|-----------|------------|
2148
+ | `GET _search` / `_count` / `_mapping` / `_settings` / `_alias` / `GET _doc` / `_source` | `SELECT` | `query-only` |
2149
+ | `POST _update` / `POST _doc` | `UPDATE` | `read-write` |
2150
+ | `PUT _doc` / `_create` | `INSERT` | `read-write` |
2151
+ | `DELETE` (any) | `DELETE` | `data-admin` |
2152
+ | `_bulk` | highest tier among the NDJSON actions (`delete` ⇒ `data-admin`) | derived |
2153
+ | Anything else | `DROP` | `admin` (deny by default) |
2154
+
2155
+ ### Schema inspection
2156
+
2157
+ `schema [index]` calls `GET /<index>/_mapping` and flattens nested properties into dotted-path columns. Multi-fields under `.fields` (e.g. `text` → `text.keyword`) are emitted as separate columns. All fields are reported as nullable. There is no PK / FK / index info.
2158
+
2159
+ `schema` (no argument) iterates all non-system indices through the standard full-scan code path and writes per-connection caches under `.dbcli/schemas/<connection>/`.
2160
+
2161
+ ### Query semantics
2162
+
2163
+ - `--collection <index>` (or `--index <index>`) is required.
2164
+ - Body that starts with `{` → sent as JSON DSL via `POST /<index>/_search`. Body otherwise → URL-encoded into `?q=...` (Lucene query string) on `GET`.
2165
+ - Hits are flattened: each row carries `_id` plus dotted-path fields lifted from `_source`. Use `--format json` to inspect raw nested structure.
2166
+ - Query-only mode caps `size` at 1000. `--no-limit` is internally capped at 10 000; for deeper pagination use the API directly with `search_after` or PIT.
2167
+
2168
+ ### Recommended `query` patterns
2169
+
2170
+ ```bash
2171
+ # DSL match
2172
+ dbcli query '{"query":{"match":{"status":"active"}}}' --collection orders --format json
2173
+
2174
+ # DSL with sort + size
2175
+ dbcli query '{"query":{"range":{"created_at":{"gte":"2026-01-01"}}},"sort":[{"created_at":"desc"}],"size":50}' \
2176
+ --collection orders
2177
+
2178
+ # Aggregation
2179
+ dbcli query '{"size":0,"aggs":{"by_status":{"terms":{"field":"status.keyword"}}}}' \
2180
+ --collection orders --format json
2181
+
2182
+ # Lucene query string
2183
+ dbcli query 'status:active AND amount:>100' --index orders --limit 100
2184
+ ```
2185
+
2186
+ ### Export (v1.22)
2187
+
2188
+ `dbcli export` supports two shapes on an ES connection:
2189
+
2190
+ ```bash
2191
+ # (a) search DSL + --index → export the hits
2192
+ dbcli export '{"query":{"match":{"status":"active"}}}' --index orders --format jsonl --output orders.ndjson
2193
+
2194
+ # (b) index name as the query → match_all over the whole index (scroll)
2195
+ dbcli export orders --format csv --output orders.csv
2196
+ dbcli export orders --no-limit --format jsonl # full index, scrolled in batches
2197
+ ```
2198
+
2199
+ - Outputs JSON / JSONL / CSV. Default cap is 1000 rows; `--no-limit` streams the full index via the scroll API in batches.
2200
+ - Index-level blacklist is checked before export and the run is written to the audit log.
2201
+
2202
+ ### Interactive shell (v1.22)
2203
+
2204
+ `dbcli shell` on an ES connection opens a Kibana Dev Tools-style REPL:
2205
+
2206
+ ```text
2207
+ $ dbcli --use local-es shell
2208
+ GET /orders/_search
2209
+ {
2210
+ "query": { "match": { "status": "active" } }
2211
+ }
2212
+ # ← blank line submits the whole block
2213
+ ```
2214
+
2215
+ - Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
2216
+ - Read-focused: index-level blacklist rejects protected indices at the front end; a `_search` whose body omits `size` is auto-capped at 1000 hits.
2217
+
2218
+ ### Doctor and diagnostics
2219
+
2220
+ `dbcli doctor` runs a dedicated Elasticsearch path:
2221
+
2222
+ - Verifies REST connectivity to `GET /`.
2223
+ - Reads `version.number` and runs the standard version freshness check.
2224
+ - Walks every index via `listTables()` + `getTableSchema()` to feed the blacklist completeness check and the large-table heuristic (using `documentCount`).
2225
+ - Standard schema-cache freshness using `schemaLastUpdated`.
2226
+
2227
+ ### Limitations
2228
+
2229
+ - Writes (`insert`/`update`/`delete`) are not exposed yet — the adapter implements them, but the CLI currently only routes them for SQL and MongoDB. Read-only `export` (v1.22) and the interactive `shell` (v1.22) are available.
2230
+ - No `_search/scroll` or PIT pagination at the CLI layer; large pulls need a saved external script.
2231
+ - `check`, `diff`, `migrate`, and `q` are SQL-only and exit with errors (or fall through to a generic "unsupported" path).
2232
+ - Blacklist column rules are applied to flattened hit rows on `query`; table-level blacklist rejects an index up front.