@carllee1983/dbcli 1.20.1 → 1.23.1

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.
@@ -100,13 +100,14 @@ dbcli schema --use staging # Scan staging DB; saves to .dbcli/schemas/s
100
100
  dbcli schema --use prod # Scan prod DB; saves to .dbcli/schemas/prod/
101
101
  ```
102
102
 
103
- **Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`, `--use <connection>`
103
+ **Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`, `--use <connection>`, `--sample-size <n>` (mongo only), `--sample-method <random|natural>` (mongo only)
104
104
  **Permission:** query-only+
105
105
 
106
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
107
 
108
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
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.
110
111
 
111
112
  ### query
112
113
 
@@ -160,6 +161,40 @@ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdou
160
161
  > - Hits are flattened: each result row contains `_id` plus dotted-path fields from `_source`. Pass `--format json` to keep nested structures readable.
161
162
  > - Query-only mode caps at 1000 hits; `--no-limit` is internally capped at 10 000 (use saved searches / `search_after` for deeper pagination).
162
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
+
163
198
  ### plan
164
199
 
165
200
  Static SQL risk analyzer. Classifies a statement into the same permission tiers
@@ -328,6 +363,57 @@ Substitution rules: pure raw text — `:name` becomes the value's `String()` for
328
363
 
329
364
  Size guard: `LRANGE` / `ZRANGE` stop overridden when `< 0` or `> 1000`; `SCAN` / `HSCAN` / `SSCAN` / `ZSCAN` get `COUNT 1000` injected if absent. `--no-limit` disables.
330
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
+
331
417
  ### queries
332
418
 
333
419
  Manage saved snippets — discover, inspect, scaffold, and edit local copies. Mutating
@@ -428,13 +514,20 @@ dbcli export "SELECT * FROM users" --format csv --output users.csv --force # Sk
428
514
  dbcli export "SELECT * FROM users" --format json | jq '.[]'
429
515
  dbcli export "SELECT * FROM users" --format jsonl --output users.ndjson
430
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
431
522
  ```
432
523
 
433
- **Options:** `--format <json|jsonl|csv|html>` (required), `--output <path>`, `--force`, `--recovery`
434
- **Permission:** query-only+
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.
435
526
 
436
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.
437
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
+
438
531
  ### blacklist
439
532
 
440
533
  Manage sensitive data blacklist to prevent AI access to restricted tables/columns.
@@ -502,6 +595,8 @@ Read-only snapshot for AI agents. Never emits credentials or blacklisted values.
502
595
  | `--for-agent` | Shortcut for `--format json --brief` |
503
596
  | `--no-connect` | Skip the cheap version/object probe (no DB traffic) |
504
597
  | `--probe-timeout <ms>` | Hard timeout for the version/object probe (default 1500) |
598
+ | `--require-schema-cache` | Throw `SCHEMA_CACHE_MISSING` (recovery code) when the active SQL connection has no usable schema cache |
599
+ | `--recovery` | On failure, emit a structured `RecoveryEnvelope` to stdout |
505
600
 
506
601
  Example:
507
602
 
@@ -509,7 +604,14 @@ Example:
509
604
  dbcli inspect --for-agent
510
605
  ```
511
606
 
512
- Output schema is locked at `schemaVersion: 1`. Sections: `connection`, `permission`, `blacklist`, `objects`, `schemaCache`, `snippets`, `suggestedCommands`, `warnings`.
607
+ Output schema is locked at `schemaVersion: 1`. Sections: `connection`, `permission`, `blacklist`, `objects`, `schemaCache`, `snippets`, `suggestedCommands`, `hints` **(v1.23)**, `warnings`.
608
+
609
+ **`suggestedCommands` (context-aware, v1.23)** — a three-tier weighted list:
610
+ 1. *Bootstrap* — always-safe orientation commands (`blacklist list`, `schema <table>`, ...).
611
+ 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.
612
+ 3. *Discovery* — broader exploration commands.
613
+
614
+ **`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).
513
615
 
514
616
  **Permission:** query-only+
515
617
 
@@ -581,6 +683,30 @@ Boundaries:
581
683
 
582
684
  **Permission:** query-only+
583
685
 
686
+ #### guide missing-index-for (v1.23)
687
+
688
+ A single-query composite-index advisor. Parses one `SELECT`, combines a real
689
+ `EXPLAIN` plan with existing indexes, and emits index candidates each carrying a
690
+ `confidence` (`high` / `medium` / `low`) and a `reason`. Read-only (EXPLAIN +
691
+ index introspection only). MySQL/MariaDB + PostgreSQL.
692
+
693
+ ```bash
694
+ dbcli guide missing-index-for "SELECT ... FROM betting_logs b JOIN hoster_machines hm ON ..."
695
+ dbcli guide missing-index-for @analytics/live-summary # @saved-query
696
+ dbcli guide missing-index-for "..." --format json # yaml (default) | json | markdown
697
+ dbcli guide missing-index-for "..." --min-confidence medium # drop candidates below low|medium|high
698
+ ```
699
+
700
+ **Options:** `--format <yaml|json|markdown>` (default `yaml`), `--min-confidence <low|medium|high>`.
701
+
702
+ Behaviour:
703
+ - Detects existing-index collisions (a single-column index that can be extended into a composite).
704
+ - Functional/expression columns (e.g. `DATE(settled_at)`) and SQL it cannot parse are reported under `warnings`, never as recommendations.
705
+ - Single `SELECT` only — no INSERT/UPDATE/DELETE, stored procedures, or view bodies.
706
+ - Dialects beyond node-sql-parser support fall back to EXPLAIN-only heuristics.
707
+
708
+ **Permission:** query-only+
709
+
584
710
  ### recovery
585
711
 
586
712
  Machine-readable error envelope. Two surfaces share one `RecoveryEnvelope`
@@ -727,8 +853,9 @@ deterministically.
727
853
  | Flag | Required | Description |
728
854
  |---|---|---|
729
855
  | `--next` | yes | Activate the multi-turn lookup. |
730
- | `--after-step <n>` | yes | 1-based order of the step the agent just executed. Range: `[1, envelope.recovery.length]`. |
856
+ | `--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). |
731
857
  | `--result <value>` | yes | JSON `StepResultSummary` (inline) or `@<path>` to read from a file. |
858
+ | `--branch <id>` | no | Walk a specific branch by id (required on `--next` calls after a fork). See *Connection branching* below. |
732
859
  | `--from <path>` | no | Override the auto-saved envelope. |
733
860
  | `--format <fmt>` | no | `json` (default) or `markdown`. |
734
861
 
@@ -760,9 +887,24 @@ interface NextResult {
760
887
  cursor: number // step.order when kind='step'; totalSteps when 'done'
761
888
  totalSteps: number
762
889
  step?: GuideStep // present iff kind='step'
890
+ branchId?: string // set iff agent is currently traversing a branch
891
+ branchDescription?: string // mirror of branches[branchId].description
763
892
  }
764
893
  ```
765
894
 
895
+ **Connection branching**
896
+
897
+ 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:
898
+
899
+ | Branch id | When chosen |
900
+ |---|---|
901
+ | `doctor-clean` | Doctor reports no errors — likely transient; verify baseline state, then retry. |
902
+ | `doctor-config-missing` | Doctor flagged a config-level failure (missing / invalid config). Re-init before reconnecting. |
903
+ | `doctor-auth-error` | Doctor confirms credentials were rejected. Re-init with `--force` to overwrite credentials. |
904
+ | `doctor-network-error` | Doctor confirms a network-level failure (host / port / DNS / timeout). Inspect and re-init host/port. |
905
+
906
+ 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).
907
+
766
908
  **Exit codes**
767
909
 
768
910
  | Exit | Condition |
@@ -937,6 +1079,11 @@ Inside the shell:
937
1079
  - Multi-line SQL: keeps accumulating until `;` is found
938
1080
  - History persists across sessions (~/.dbcli_history)
939
1081
 
1082
+ The REPL flavor depends on the active engine: SQL engines and MongoDB use the
1083
+ form above; **Redis** opens a single-line command REPL (see [Redis › Interactive
1084
+ shell](#interactive-shell)); **Elasticsearch** opens a Kibana Dev Tools-style
1085
+ REPL (v1.22, see [Elasticsearch › Interactive shell](#interactive-shell-v122)).
1086
+
940
1087
  ### migrate
941
1088
 
942
1089
  Schema DDL operations. **All commands default to dry-run** — use `--execute` to actually run the SQL. Destructive operations (DROP) also require `--force`.
@@ -1031,6 +1178,16 @@ dbcli skill tasks plan diagnose-slow-query --param query="..." --format json
1031
1178
  - **show:** prints the full task definition (frontmatter + Agent Notes). Use `--format json` for an agent-friendly contract.
1032
1179
  - **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.
1033
1180
 
1181
+ **Builtin packs:** `diagnose-slow-query` and **(v1.23)** `analyze-table-perf` —
1182
+ a read-only (`plan-only`) pack taking a required `table` parameter that walks
1183
+ `blacklist list` → `schema <table> --format json` → `guide index-usage --format json`.
1184
+ `dbcli inspect` suggests `analyze-table-perf` automatically for the hottest table
1185
+ in recent audit activity.
1186
+
1187
+ ```bash
1188
+ dbcli skill tasks plan analyze-table-perf --param table=betting_logs --format json
1189
+ ```
1190
+
1034
1191
  Task storage layers:
1035
1192
 
1036
1193
  | Source | Path | Notes |
@@ -1042,6 +1199,270 @@ Task storage layers:
1042
1199
  Higher tiers override lower tiers by task name. Task name is derived from the
1043
1200
  file path under the tier root (e.g. `diag/inspect.md` → `diag/inspect`).
1044
1201
 
1202
+ ## Recovery Cookbook (agent walkthroughs)
1203
+
1204
+ End-to-end recovery sessions for the most common failure codes. All examples
1205
+ assume the agent invoked a `--recovery`-capable command and received a
1206
+ `RecoveryEnvelope` (or hit the same envelope via `dbcli recovery --code <CODE>`
1207
+ lookup). See [§recovery](#recovery) for the envelope shape, [§recover](#recover)
1208
+ for `--apply` / `--next` / risk-gate semantics, and [§audit](#audit) for the
1209
+ bi-directional `audit_ref` ⇄ `recovery_ref` pivot.
1210
+
1211
+ ### Scenario index
1212
+
1213
+ | Code | Trigger | Primary remediation | Risk tier |
1214
+ |------|---------|---------------------|-----------|
1215
+ | `CONN_REFUSED` | Database process down or wrong host/port. | `dbcli doctor` → fix host/port → retry. | `readonly` |
1216
+ | `CONN_AUTH_FAILED` | Credentials rejected. | Re-check `.dbcli`/env, rotate credentials, `dbcli init --force` only on explicit user nod. | `readonly` → `interactive` |
1217
+ | `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` |
1218
+ | `BLACKLIST_TABLE` | Target table is blacklisted. | `dbcli blacklist list` → `blacklist table remove <name>` (local-write tier). | `readonly` + `local-write` |
1219
+ | `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` |
1220
+ | `SCHEMA_CACHE_MISSING` | Fresh checkout / new v2 connection / cache wiped. | `dbcli schema --refresh --force` (or `--use <conn>` per-connection). | `readonly` |
1221
+ | `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` | Typo or duplicate snippet name. | `dbcli queries list` → `queries search <kw>` → run correct `@name`. | `readonly` |
1222
+ | `SNIPPET_PARAM_MISSING` | `--param k=v` not supplied. | `dbcli queries show @name` lists required params → re-run with full set. | `readonly` |
1223
+ | `CONFIG_MISSING` | No `.dbcli` in cwd. | `dbcli init` (human-driven). | `interactive` |
1224
+
1225
+ `risk` enum: `readonly` / `dry-run` / `write` / `unknown` (see §recovery boundaries).
1226
+ Allowlist tier: `readonly` / `dry-run` / `local-write` / `db-write` / `interactive` (see [§recover Risk gate matrix](#risk-gate-matrix)).
1227
+
1228
+ ### S1 — CONN_REFUSED end-to-end
1229
+
1230
+ ```bash
1231
+ # 1. Failing call writes envelope to stdout AND .dbcli/last-recovery.json
1232
+ $ dbcli query "SELECT 1" --recovery --format json
1233
+ {
1234
+ "schemaVersion": 1,
1235
+ "error": { "code": "CONN_REFUSED", "message": "..." },
1236
+ "audit_ref": "1f8e...c4d2",
1237
+ "recovery": [
1238
+ { "order": 1, "command": "dbcli doctor --format json", "risk": "readonly", ... },
1239
+ { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly", ... }
1240
+ ],
1241
+ "verify": { "command": "dbcli doctor --format json", "risk": "readonly", ... }
1242
+ }
1243
+
1244
+ # 2. One-shot apply (only readonly + dry-run run by default)
1245
+ $ dbcli recover --apply --format json
1246
+ { "finalStatus": "ok", "executed": [...], "verifyStatus": "passed" }
1247
+ # Exit 0 → root cause cleared (verify probe succeeded).
1248
+
1249
+ # 3. If verify reported `failed` / `indeterminate`, drop into --next for control
1250
+ $ dbcli recover --next --after-step 1 --result '{"status":"failed","exitCode":1}'
1251
+ # → returns a refined step 2 or `kind:"done"` based on the prevResult
1252
+ ```
1253
+
1254
+ ### S2 — PERMISSION_DENIED with implicit `--dry-run` preview
1255
+
1256
+ ```bash
1257
+ $ dbcli update orders --where "id=1" --set '{"status":"shipped"}' --recovery --format json
1258
+ {
1259
+ "error": { "code": "PERMISSION_DENIED", ... },
1260
+ "audit_ref": "9ab0...e711",
1261
+ "recovery": [
1262
+ { "order": 1, "command": "dbcli update orders --where 'id=1' --set '<redacted>' --dry-run", "risk": "dry-run" },
1263
+ { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly" }
1264
+ ]
1265
+ }
1266
+
1267
+ # Default apply runs both steps (dry-run is in-tier).
1268
+ $ dbcli recover --apply
1269
+ ```
1270
+
1271
+ When the failing operation is INSERT/UPDATE/DELETE, the envelope prepends a
1272
+ `risk: 'dry-run'` step (the same write subcommand with `--dry-run`). Run it
1273
+ before any escalation — it both teaches the agent what the SQL looks like and
1274
+ proves the change is well-formed before raising the permission tier.
1275
+
1276
+ ### S3 — BLACKLIST_TABLE (local-write remediation)
1277
+
1278
+ ```bash
1279
+ $ dbcli query "SELECT * FROM audit_logs" --recovery --format json
1280
+ # error.code: BLACKLIST_TABLE
1281
+ # recovery[0]: dbcli blacklist list (risk: readonly)
1282
+ # recovery[1]: dbcli blacklist table remove audit_logs (risk: write — local-write tier)
1283
+
1284
+ # Default --apply: step 1 runs, step 2 skipped:risk → exit 3.
1285
+ $ dbcli recover --apply
1286
+ # To proceed: open the gate to local-write tier ONLY (does not touch DB).
1287
+ $ dbcli recover --apply --allow-write=readonly-cmd
1288
+ { "finalStatus": "ok", "executed": [step1, step2], "verifyStatus": "passed" }
1289
+ ```
1290
+
1291
+ ### S4 — BLACKLIST_COLUMN_WRITE (preview-then-drop)
1292
+
1293
+ ```bash
1294
+ $ dbcli insert users --data '{"name":"a","ssn":"123"}' --recovery
1295
+ # recovery[0]: dbcli insert users --data '<redacted>' --dry-run (risk: dry-run)
1296
+ # recovery[1]: dbcli blacklist list (risk: readonly)
1297
+ # recovery[2]: dbcli blacklist column remove users.ssn (risk: write — local-write)
1298
+
1299
+ # Preferred path: don't widen the blacklist — re-shape the agent's payload to drop ssn.
1300
+ # Apply only the diagnostic prefix (steps 1+2) to confirm what columns are masked:
1301
+ $ dbcli recover --apply
1302
+ # Then re-issue insert without `ssn`.
1303
+ ```
1304
+
1305
+ ### S5 — SCHEMA_CACHE_MISSING (fresh / multi-conn)
1306
+
1307
+ ```bash
1308
+ $ dbcli inspect --require-schema-cache --recovery --format json
1309
+ # error.code: SCHEMA_CACHE_MISSING
1310
+ # recovery[0]: dbcli schema --refresh --force (risk: readonly — populates .dbcli/schemas/)
1311
+ # verify: dbcli inspect --format json (schemaCache.available === true)
1312
+
1313
+ $ dbcli recover --apply
1314
+ # Per-connection cache lives at .dbcli/schemas/<connection>/. If the failure was on
1315
+ # a v2 named connection, the envelope's command already carries `--use <name>`.
1316
+ ```
1317
+
1318
+ ### S6 — SNIPPET_NOT_FOUND with disambiguation
1319
+
1320
+ ```bash
1321
+ $ dbcli q @anaytics/revenue --recovery
1322
+ # typo: anaytics → analytics
1323
+ # recovery[0]: dbcli queries list --format json
1324
+ # recovery[1]: dbcli queries search analytics (or whatever --hint suggests)
1325
+ $ dbcli recover --apply
1326
+ # Agent reads stdoutSummary, identifies the correct @name, then re-issues:
1327
+ $ dbcli q @analytics/revenue --param days=30
1328
+ ```
1329
+
1330
+ ### Multi-turn `--next` walkthrough (3-step plan)
1331
+
1332
+ Use `--next` instead of `--apply` when:
1333
+
1334
+ - `--apply` is too coarse-grained (the agent wants step-by-step inspection).
1335
+ - The plan contains an `interactive` step that `--apply` would skip.
1336
+ - The agent uses its own runner / sandbox and just wants dbcli to drive cursoring.
1337
+
1338
+ `--next` returns one step at a time, given which step the agent **just executed**
1339
+ and a `StepResultSummary` of how it went. dbcli does not persist the cursor —
1340
+ the agent owns `--after-step`.
1341
+
1342
+ ```bash
1343
+ # Envelope already saved at .dbcli/last-recovery.json (3-step plan, CONN_REFUSED).
1344
+
1345
+ # Round 1 — agent reads step 1 from the envelope, executes it itself, then asks
1346
+ # dbcli for the next step.
1347
+ $ dbcli recover --next --after-step 1 --result '{"status":"ok","exitCode":0}' --format json
1348
+ {
1349
+ "schemaVersion": 1,
1350
+ "kind": "step",
1351
+ "errorCode": "CONN_REFUSED",
1352
+ "cursor": 2,
1353
+ "totalSteps": 3,
1354
+ "step": { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly", ... }
1355
+ }
1356
+
1357
+ # Round 2 — bigger stdout, save to file and reference it.
1358
+ $ ./run-step.sh > /tmp/r2.json # agent's own runner; result is StepResultSummary JSON
1359
+ $ dbcli recover --next --after-step 2 --result @/tmp/r2.json
1360
+ { "kind": "step", "cursor": 3, "step": { "order": 3, ... } }
1361
+
1362
+ # Round 3 — last step done.
1363
+ $ dbcli recover --next --after-step 3 --result '{"status":"ok"}'
1364
+ { "kind": "done", "cursor": 3, "totalSteps": 3 }
1365
+ ```
1366
+
1367
+ `StepResultSummary` contract (recap of [§recover Multi-turn](#multi-turn---next-p2)):
1368
+
1369
+ ```ts
1370
+ interface StepResultSummary {
1371
+ status: 'ok' | 'failed' | 'skipped'
1372
+ exitCode?: number
1373
+ stdoutSummary?: string // last 4 KB
1374
+ stderrSummary?: string // last 4 KB
1375
+ }
1376
+ ```
1377
+
1378
+ Truncate to the **last** 4 KB before passing — the head of a huge stdout is
1379
+ usually not what disambiguates next steps.
1380
+
1381
+ Verification is **not** automatic under `--next`. If the agent wants the same
1382
+ verify probe `--apply` runs, it must execute the envelope's `verify` step
1383
+ itself after the plan completes.
1384
+
1385
+ ### Bi-directional pivot (envelope ⇄ audit)
1386
+
1387
+ Every `--recovery`-capable failure (`query`, `inspect`, `insert`, `update`,
1388
+ `delete`, `export`, `q`, `schema`) writes **both** sides of a UUID link:
1389
+
1390
+ - `RecoveryEnvelope.audit_ref` → the `audit.id` for the same failure.
1391
+ - `AuditEntry.recovery_ref` → the envelope's id (also the auto-saved
1392
+ `.dbcli/last-recovery.json` filename trace).
1393
+
1394
+ ```bash
1395
+ # From envelope → audit (forensics on a saved failure)
1396
+ $ ENV_ID=$(jq -r '.id' .dbcli/last-recovery.json) # or read from stdout
1397
+ $ dbcli audit show --recovery-ref "$ENV_ID" --format json
1398
+ # Returns the matching audit entry (full, not brief).
1399
+
1400
+ # From audit → envelope (you have an audit hit, want the structured plan)
1401
+ $ AUDIT_ID=$(dbcli audit tail --for-agent --n 1 | jq -r '.[0].id')
1402
+ $ dbcli audit show "$AUDIT_ID" --format json
1403
+ # Read `recovery_ref` from the entry, then either re-run --recovery against
1404
+ # the original command or load the saved envelope:
1405
+ $ jq '.recovery_ref' .dbcli/last-recovery.json | grep -q "$RECOVERY_REF" \
1406
+ && dbcli recover --format markdown # human inspect
1407
+ || dbcli recover --from /path/to/archived.json --format markdown
1408
+ ```
1409
+
1410
+ Session handoff: a fresh agent that opens `dbcli inspect --for-agent`,
1411
+ `dbcli guide`, `dbcli recover`, or `dbcli recover --apply` gets an
1412
+ `audit_recent: AuditEntryBrief[]` field (last 5 entries) embedded in the JSON
1413
+ output — no extra round-trip to the audit CLI needed for immediate history
1414
+ context.
1415
+
1416
+ ### Risk gate cheat sheet
1417
+
1418
+ Quick reference for what `--apply` runs at each `--allow-write` level. The
1419
+ canonical matrix lives at [§recover Risk gate matrix](#risk-gate-matrix); this
1420
+ table maps it onto common agent intents.
1421
+
1422
+ | Agent intent | Recommended flag | What runs | What's skipped |
1423
+ |---|---|---|---|
1424
+ | Probe-only (read state, learn) | `--apply` (default) | `readonly` + `dry-run` steps | `local-write`, `db-write`, `interactive` |
1425
+ | Local config remediation (e.g. `blacklist remove`) | `--apply --allow-write=readonly-cmd` | + `local-write` | `db-write`, `interactive` |
1426
+ | Database write recovery (rare; trusted plan) | `--apply --allow-write=write-cmd` | + `db-write` | `interactive` |
1427
+ | Interactive step (e.g. `dbcli init`) | Drive manually OR use `--next` | n/a | All interactive steps always skip under `--apply` |
1428
+ | Walk plan step-by-step with own runner | `--next --after-step N --result …` | one step per call | n/a — agent owns cursor + execution |
1429
+
1430
+ Three rules that always apply regardless of `--allow-write`:
1431
+
1432
+ 1. **Tier is code-owned, not envelope-claimed.** The risk gate reads the
1433
+ per-error-code allowlist after parsing argv. An envelope cannot escalate
1434
+ itself by setting `risk: 'readonly'` on a write subcommand — argv decides.
1435
+ 2. **Placeholders block.** A step with unresolved `<token>` placeholders is
1436
+ skipped as `skipped:placeholder` even at `--allow-write=write-cmd`. Bind
1437
+ them at `recovery` lookup time with `--hint` / `--snippet` / `--table`, or
1438
+ ask the user.
1439
+ 3. **Verify is signal, not gate.** `verifyStatus` ∈ `{passed, failed,
1440
+ indeterminate}` reports whether the original failure looks resolved.
1441
+ `recover --apply` exit code is set by step execution, not verification.
1442
+
1443
+ ### Common pitfalls
1444
+
1445
+ - **Stale `.dbcli/last-recovery.json`.** `recover` (no `--apply`) shows the
1446
+ *saved* plan, which may be hours old. Re-run the original command with
1447
+ `--recovery` to refresh it, or pass `--from <file>` to load an archived one.
1448
+ - **`.dbcli/` is gitignored.** Do not check `last-recovery.json` into a repo
1449
+ for "reproducibility"; it contains sanitized command snapshots but the
1450
+ workspace `cwd` only makes sense locally. Use `recover --from <archived.json>`
1451
+ for cross-machine replay.
1452
+ - **`--apply` exit 3 means every step skipped.** Not a failure — it means the
1453
+ default gate was too tight. Either widen with `--allow-write`, fill
1454
+ placeholders, or fall back to `--next` and drive steps manually.
1455
+ - **`--next` does not run verify.** Re-run the original failing command with
1456
+ `--recovery` once the plan is done; if it now succeeds (no envelope on
1457
+ stdout), recovery is complete. Or invoke `envelope.verify.command` yourself.
1458
+ - **Audit writer failures are non-fatal.** If `audit health` reports
1459
+ `lastWriteOk: false`, the main command still completed — but `recovery_ref`
1460
+ ⇄ `audit_ref` linkage is broken for that one call. `audit health` surfaces
1461
+ the underlying error (disk full, EACCES, etc.).
1462
+ - **Cross-connection forensics.** `audit tail --all --for-agent` merges all
1463
+ connections; `audit show <id-prefix> --all` returns an envelope `{connection,
1464
+ entry}` so a fresh agent can tell which DB the failure was against.
1465
+
1045
1466
  ## Interactive HTML dashboard
1046
1467
 
1047
1468
  `query`, `q`, and `export` can render results as a single, fully self-contained
@@ -1150,19 +1571,21 @@ MongoDB connections use a JSON-based query model instead of SQL. Treat MongoDB s
1150
1571
 
1151
1572
  Atlas-style `mongodb+srv://` URIs are supported. `list` and `query` run against the database configured for the connection, and `query` always requires `--collection <name>`.
1152
1573
 
1153
- **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `insert`, `update`, `delete`, `status`, `shell`, `doctor`, `upgrade`, `completion`
1574
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `q`, `insert`, `update`, `delete`, `export`, `status`, `shell`, `doctor`, `upgrade`, `completion`
1154
1575
 
1155
1576
  **Limited support:**
1156
1577
 
1157
1578
  - `schema` samples collection documents to infer field names/types. It does not provide relational constraints, primary keys, foreign keys, or reliable index metadata.
1158
1579
  - `query` accepts only JSON object filters or aggregation pipeline arrays and always requires `--collection <name>`.
1580
+ - `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.
1159
1581
  - `insert` inserts one JSON document into the named collection.
1160
1582
  - `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`.
1161
1583
  - `delete` deletes all documents matching the JSON/simple filter.
1584
+ - `export` accepts the same JSON filter / aggregation syntax as `query`.
1162
1585
  - MongoDB write paths do not currently provide the same SQL dry-run, relational schema validation, or column-level blacklist filtering guarantees as SQL writes.
1163
1586
  - `shell` blocks raw SQL for MongoDB; use `query <json> --collection <name>` inside the shell.
1164
1587
 
1165
- **Not supported (exit with error):** `q` saved-query execution, `export`, `diff`, `migrate`
1588
+ **Not supported (exit with error):** `diff`, `migrate`
1166
1589
 
1167
1590
  **Not a supported MongoDB target:** `check` is designed for relational health checks and emits SQL-style checks.
1168
1591
 
@@ -1197,19 +1620,19 @@ dbcli delete orders --where '{"status":"cancelled"}' --force
1197
1620
 
1198
1621
  ## Redis Support
1199
1622
 
1200
- Redis connections speak Redis commands rather than SQL. The adapter uses the `ioredis` driver and exposes a narrow, permission-gated surface.
1623
+ 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.
1201
1624
 
1202
- **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `status`, `doctor`, `upgrade`, `completion`
1625
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `shell`, `status`, `doctor`, `upgrade`, `completion`
1203
1626
 
1204
1627
  **Saved queries:** `q` is supported for read-only Redis commands (see "Redis snippets" below).
1205
1628
 
1206
- **Not supported (exit with error or unsupported error):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `shell`. For writes, run the equivalent Redis command via `query` — the same permission gate applies.
1629
+ **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.
1207
1630
 
1208
1631
  ### Connection and configuration
1209
1632
 
1210
1633
  - Required fields: `system: redis`, `host`, `port`. `password` and `database` are optional.
1211
1634
  - `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.
1212
- - `connection.timeout` (ms, default 5000) maps to ioredis's `connectTimeout`.
1635
+ - `connection.timeout` (ms, default 5000) maps to the client's `connectionTimeout`.
1213
1636
 
1214
1637
  ### Permission classification
1215
1638
 
@@ -1256,22 +1679,83 @@ dbcli query "DEL temp:lock"
1256
1679
  dbcli query "HDEL user:42 lastLogin"
1257
1680
  ```
1258
1681
 
1682
+ ### Size guard (`query --no-limit` / shell `.no-limit`)
1683
+
1684
+ The adapter rewrites unbounded reads before dispatch and truncates oversized replies after:
1685
+
1686
+ | Strategy | Commands | Behavior |
1687
+ |----------|----------|----------|
1688
+ | inject/cap `COUNT` | `SCAN`, `HSCAN`, `SSCAN`, `ZSCAN` | adds `COUNT 1000` when absent; caps a larger `COUNT` to 1000 |
1689
+ | clamp `stop` | `LRANGE`, `ZRANGE`, `ZREVRANGE` | rewrites `stop` so the span ≤ 1000 (`-1` becomes `start+999`) |
1690
+ | inject/cap `LIMIT` | `ZRANGEBYSCORE` | appends `LIMIT 0 1000` when absent; caps a larger count |
1691
+ | client truncate | `HGETALL`, `HKEYS`, `HVALS`, `SMEMBERS`, `KEYS` | keeps the first 1000 entries |
1692
+
1693
+ 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.
1694
+
1695
+ ### Blacklist enforcement
1696
+
1697
+ Blacklist rules are enforced as **Redis-native key globs** (`*`, `?`, `[abc]`, `[a-z]`):
1698
+
1699
+ ```bash
1700
+ dbcli blacklist add 'secrets:*' # register a key-glob rule
1701
+ dbcli query "GET secrets:api_key" # → BlacklistRejection (exit non-zero)
1702
+ dbcli query "MGET safe:k secrets:api" # → rejected (any matching key fails the whole command)
1703
+ dbcli query "KEYS secrets:*" # → rejected (pattern overlaps a rule)
1704
+ dbcli query "KEYS *" # → returns only non-blacklisted keys
1705
+ ```
1706
+
1707
+ Rejections are written to the audit log with `success: false` and `metadata.rejection_reason: 'blacklist'` + `matched_pattern`.
1708
+
1709
+ ### Value / hash-field masking (v1.22)
1710
+
1711
+ Where the key-glob blacklist *rejects*, masking instead *redacts*: a matched read still
1712
+ runs, but the sensitive value comes back as `[REDACTED]` so an agent can use the command
1713
+ without ever seeing it. Add an optional `redis.mask` block to `.dbcli`:
1714
+
1715
+ ```yaml
1716
+ redis:
1717
+ mask:
1718
+ - keyPattern: 'session:*' # whole value redacted on read
1719
+ - keyPattern: 'user:*'
1720
+ fields: [password, token] # only these hash fields redacted
1721
+ ```
1722
+
1723
+ - Applies on reads: `GET`, `GETRANGE`, `HGETALL`, `HGET`, `HMGET`, `HVALS`.
1724
+ - A rule without `fields` redacts the entire value; with `fields` only the named hash fields are redacted.
1725
+ - Masking and key-glob rejection coexist, and **rejection always wins over masking** — a key that matches a blacklist rule is rejected, never merely masked.
1726
+
1727
+ ### Interactive shell
1728
+
1729
+ `dbcli shell` on a Redis connection opens a single-line REPL:
1730
+
1731
+ ```text
1732
+ $ dbcli --use local-redis shell
1733
+ Redis shell: single-line commands; SCAN/LRANGE auto-capped at 1000. Type `.no-limit on` to bypass (unsafe).
1734
+ redis> SCAN 0 # wire args become: SCAN 0 COUNT 1000 (REDIS_SIZE_REWRITE)
1735
+ redis> HGETALL bighash # >1000 fields → kept 1000 (REDIS_SIZE_TRUNCATE)
1736
+ redis> .no-limit on # bypass size guard for this session
1737
+ redis> GET secrets:api_key # → REDIS_BLACKLIST / BlacklistRejection if blacklisted
1738
+ redis> .exit
1739
+ ```
1740
+
1741
+ Tab completion offers Redis command names and known key prefixes; history persists to `~/.dbcli_history`.
1742
+
1259
1743
  ### Limitations
1260
1744
 
1261
1745
  - No `--dry-run` for writes — Redis commands execute immediately. Pair writes with a confirming read (`GET`, `HGETALL`, `EXISTS`).
1262
1746
  - No transaction wrapping (`MULTI`/`EXEC`). Submit one command at a time.
1263
1747
  - `KEYS` requires `admin`. Prefer `SCAN` for routine work.
1264
- - Blacklist rules are not enforced for Redis (there is no concept of "column" / "table" the validator can map). Be careful with sensitive key prefixes.
1748
+ - Blacklist enforcement covers **keys** (Redis-native globs); value / hash-field **masking** is available via the `redis.mask` config block (v1.22).
1265
1749
 
1266
1750
  ## Elasticsearch Support
1267
1751
 
1268
1752
  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.
1269
1753
 
1270
- **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `status`, `doctor`, `upgrade`, `completion`
1754
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `export` (v1.22), `shell` (v1.22), `status`, `doctor`, `upgrade`, `completion`
1271
1755
 
1272
1756
  **Saved queries:** `q` is supported for ES JSON DSL bodies (see "Elasticsearch snippets" below).
1273
1757
 
1274
- **Not supported (use external tooling):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `shell`. The permission classifier already understands `_doc` / `_update` / `_bulk` so future write surfaces can be wired in without changing tiers.
1758
+ **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.
1275
1759
 
1276
1760
  ### Connection and configuration
1277
1761
 
@@ -1324,6 +1808,38 @@ dbcli query '{"size":0,"aggs":{"by_status":{"terms":{"field":"status.keyword"}}}
1324
1808
  dbcli query 'status:active AND amount:>100' --index orders --limit 100
1325
1809
  ```
1326
1810
 
1811
+ ### Export (v1.22)
1812
+
1813
+ `dbcli export` supports two shapes on an ES connection:
1814
+
1815
+ ```bash
1816
+ # (a) search DSL + --index → export the hits
1817
+ dbcli export '{"query":{"match":{"status":"active"}}}' --index orders --format jsonl --output orders.ndjson
1818
+
1819
+ # (b) index name as the query → match_all over the whole index (scroll)
1820
+ dbcli export orders --format csv --output orders.csv
1821
+ dbcli export orders --no-limit --format jsonl # full index, scrolled in batches
1822
+ ```
1823
+
1824
+ - Outputs JSON / JSONL / CSV. Default cap is 1000 rows; `--no-limit` streams the full index via the scroll API in batches.
1825
+ - Index-level blacklist is checked before export and the run is written to the audit log.
1826
+
1827
+ ### Interactive shell (v1.22)
1828
+
1829
+ `dbcli shell` on an ES connection opens a Kibana Dev Tools-style REPL:
1830
+
1831
+ ```text
1832
+ $ dbcli --use local-es shell
1833
+ GET /orders/_search
1834
+ {
1835
+ "query": { "match": { "status": "active" } }
1836
+ }
1837
+ # ← blank line submits the whole block
1838
+ ```
1839
+
1840
+ - 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.
1841
+ - Read-focused: index-level blacklist rejects protected indices at the front end; a `_search` whose body omits `size` is auto-capped at 1000 hits.
1842
+
1327
1843
  ### Doctor and diagnostics
1328
1844
 
1329
1845
  `dbcli doctor` runs a dedicated Elasticsearch path:
@@ -1335,7 +1851,7 @@ dbcli query 'status:active AND amount:>100' --index orders --limit 100
1335
1851
 
1336
1852
  ### Limitations
1337
1853
 
1338
- - Writes (`insert`/`update`/`delete`/`export`) are not exposed yet — the adapter implements them, but the CLI currently only routes them for SQL and MongoDB.
1854
+ - 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.
1339
1855
  - No `_search/scroll` or PIT pagination at the CLI layer; large pulls need a saved external script.
1340
1856
  - `check`, `diff`, `migrate`, and `q` are SQL-only and exit with errors (or fall through to a generic "unsupported" path).
1341
1857
  - Blacklist column rules are applied to flattened hit rows on `query`; table-level blacklist rejects an index up front.