@carllee1983/dbcli 1.30.0 → 1.32.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.
@@ -0,0 +1,1955 @@
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
+ ### proxy
620
+
621
+ Local-development **observability proxy** for MySQL/MariaDB/PostgreSQL. Inserts dbcli
622
+ between an existing application and its real database: it listens on a configurable
623
+ port, relays TCP frames to the real server, and appends one JSONL event per query to
624
+ `.dbcli/proxy/events.jsonl`. Observe-only — no rewrite, blocking, or query modification.
625
+ Not intended as a production gateway.
626
+
627
+ **Subcommands:** `mysql` · `mariadb` · `postgresql`
628
+
629
+ ```bash
630
+ dbcli proxy mysql --listen 127.0.0.1:3307 --target 127.0.0.1:3306
631
+ dbcli proxy postgresql --listen 127.0.0.1:5434 --target 127.0.0.1:5432
632
+ dbcli proxy mysql --slow-ms 500 --redact literals # redact SQL literals in events
633
+ dbcli proxy mariadb --events ./logs/proxy.jsonl # custom event file
634
+ dbcli proxy postgresql --use prod # infer target from named connection
635
+
636
+ dbcli proxy analyze # analyze .dbcli/proxy/events.jsonl (JSON)
637
+ dbcli proxy analyze --format text --top 10 # human-readable top-10 view
638
+ dbcli proxy analyze --slow-ms 200 --n-plus-one 5 # custom thresholds
639
+ ```
640
+
641
+ **Options:**
642
+ - `--listen <addr:port>` — Address dbcli will listen on (e.g. `127.0.0.1:3307`)
643
+ - `--target <addr:port>` — Address of the real database server to relay to. If omitted, inferred from the active (or `--use`) connection config.
644
+ - `--events <path>` — JSONL event log path (default: `.dbcli/proxy/events.jsonl`)
645
+ - `--slow-ms <ms>` — Threshold in milliseconds above which events are flagged `slow: true` (default: `1000`)
646
+ - `--redact <none|literals>` — Whether to strip SQL literal values from event records (default: `none`; `literals` removes quoted strings and numbers)
647
+ - `--format <text|json>` — Startup / status output format (default: `text`)
648
+ - `--use <name>` — Target a named v2 connection for `--target` inference
649
+
650
+ **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:
651
+ ```json
652
+ { "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": [] }
653
+ ```
654
+ `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.
655
+
656
+ **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.
657
+
658
+ **`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.
659
+
660
+ **Engines:** MySQL / MariaDB / PostgreSQL
661
+ **Permission:** n/a (acts as a TCP relay; does not use dbcli's SQL permission model)
662
+
663
+ ### status
664
+
665
+ Show current configuration status (safe for AI agents, no credentials exposed).
666
+
667
+ ```bash
668
+ dbcli status # JSON output (default)
669
+ dbcli status --format text # Human-readable text output
670
+ ```
671
+
672
+ **Output:** `permission`, `system`, `blacklist` summary, `version`
673
+ **Permission:** query-only+
674
+
675
+ ### inspect
676
+
677
+ Read-only snapshot for AI agents. Never emits credentials or blacklisted values.
678
+
679
+ | Flag | Purpose |
680
+ |------|---------|
681
+ | `--format <json\|markdown>` | Output format (default `json`) |
682
+ | `--brief` | Drop sample arrays and trim suggested commands to ≤3 |
683
+ | `--for-agent` | Shortcut for `--format json --brief` |
684
+ | `--no-connect` | Skip the cheap version/object probe (no DB traffic) |
685
+ | `--probe-timeout <ms>` | Hard timeout for the version/object probe (default 1500) |
686
+ | `--require-schema-cache` | Throw `SCHEMA_CACHE_MISSING` (recovery code) when the active SQL connection has no usable schema cache |
687
+ | `--recovery` | On failure, emit a structured `RecoveryEnvelope` to stdout |
688
+
689
+ Example:
690
+
691
+ ```bash
692
+ dbcli inspect --for-agent
693
+ ```
694
+
695
+ Output schema is locked at `schemaVersion: 1`. Sections: `connection`, `permission`, `blacklist`, `objects`, `schemaCache`, `snippets`, `suggestedCommands`, `hints` **(v1.23)**, `warnings`.
696
+
697
+ **`suggestedCommands` (context-aware, v1.23)** — a three-tier weighted list:
698
+ 1. *Bootstrap* — always-safe orientation commands (`blacklist list`, `schema <table>`, ...).
699
+ 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.
700
+ 3. *Discovery* — broader exploration commands.
701
+
702
+ **`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).
703
+
704
+ **Permission:** query-only+
705
+
706
+ ### report
707
+
708
+ Diagnostic report built on top of `inspect`. Reuses inspect context (connection,
709
+ permission, blacklist, snippet inventory) and additionally runs curated built-in
710
+ `@diag/*` snippets grouped into sections.
711
+
712
+ Flags:
713
+ - `--format json|markdown` (default: json)
714
+ - `--section health,capacity,perf` (default: all three)
715
+ - `--brief` — drop evidence rows; keep counts and statuses
716
+ - `--for-agent` — shortcut for `--format json --brief`
717
+ - `--no-connect` — context-only snapshot (skip diagnostics + inspect probe)
718
+ - `--per-snippet-timeout <ms>` (default 3000)
719
+ - `--max-rows-per-evidence <n>` (default 50)
720
+ - `--probe-timeout <ms>` (default 1500, inherited from inspect)
721
+
722
+ Examples:
723
+
724
+ dbcli report --format json
725
+ dbcli report --format markdown --section health,capacity
726
+ dbcli report --for-agent
727
+ dbcli report --no-connect
728
+
729
+ Boundaries:
730
+ - Read-only. Skips snippets whose required params have no default value.
731
+ - Never connects in `--no-connect` mode.
732
+ - MongoDB and no-config workspaces emit a context-only snapshot with a warning.
733
+
734
+ **Permission:** query-only+
735
+
736
+ ### guide
737
+
738
+ Deterministic next-command planner for a fixed set of database goals. Reuses
739
+ `inspect` context (cache-first) and the workspace's saved-query inventory to
740
+ emit an ordered, read-only plan that an AI agent can follow step-by-step.
741
+
742
+ Goals (fixed list):
743
+ - `slow-query` — diagnose slow queries (long-running, locks, cache, indexes).
744
+ - `capacity` — audit storage and memory.
745
+ - `health` — connections, locks, cluster status.
746
+ - `index-usage` — index effectiveness audit.
747
+ - `permissions` — review permission level, blacklist, snippet inventory.
748
+ - `schema-overview` — orient in an unfamiliar database.
749
+
750
+ Flags:
751
+ - `--format json|markdown` (default: json)
752
+ - `--brief` — drop rationale + expects fields
753
+ - `--for-agent` — shortcut for `--format json --brief`
754
+ - `--list` — list available goals and exit
755
+ - `--probe` — refresh inspect context via live probe (default: cache-first)
756
+ - `--probe-timeout <ms>` (default 1500, inherited from inspect)
757
+
758
+ Examples:
759
+
760
+ dbcli guide slow-query
761
+ dbcli guide capacity --format markdown
762
+ dbcli guide --list
763
+ dbcli guide health --for-agent
764
+ dbcli guide schema-overview --probe
765
+
766
+ Boundaries:
767
+ - Read-only. Guide plans commands; it does not execute them.
768
+ - Goal vocabulary is fixed in v1.14.0; user-supplied goals are rejected.
769
+ - Each step carries `risk: 'readonly'` in v1.14.0 (forward-compatible with v1.15.0 recovery).
770
+ - Coexists with `dbcli skill tasks plan` (template-driven). Use guide for ad-hoc goals; use task packs for repeatable workflows.
771
+
772
+ **Permission:** query-only+
773
+
774
+ #### guide missing-index-for (v1.23)
775
+
776
+ A single-query composite-index advisor. Parses one `SELECT`, combines a real
777
+ `EXPLAIN` plan with existing indexes, and emits index candidates each carrying a
778
+ `confidence` (`high` / `medium` / `low`) and a `reason`. Read-only (EXPLAIN +
779
+ index introspection only). MySQL/MariaDB + PostgreSQL.
780
+
781
+ ```bash
782
+ dbcli guide missing-index-for "SELECT ... FROM betting_logs b JOIN hoster_machines hm ON ..."
783
+ dbcli guide missing-index-for @analytics/live-summary # @saved-query
784
+ dbcli guide missing-index-for "..." --format json # yaml (default) | json | markdown
785
+ dbcli guide missing-index-for "..." --min-confidence medium # drop candidates below low|medium|high
786
+ ```
787
+
788
+ **Options:** `--format <yaml|json|markdown>` (default `yaml`), `--min-confidence <low|medium|high>`.
789
+
790
+ Behaviour:
791
+ - Detects existing-index collisions (a single-column index that can be extended into a composite).
792
+ - Functional/expression columns (e.g. `DATE(settled_at)`) and SQL it cannot parse are reported under `warnings`, never as recommendations.
793
+ - Single `SELECT` only — no INSERT/UPDATE/DELETE, stored procedures, or view bodies.
794
+ - Dialects beyond node-sql-parser support fall back to EXPLAIN-only heuristics.
795
+
796
+ **Permission:** query-only+
797
+
798
+ ### recovery
799
+
800
+ Machine-readable error envelope. Two surfaces share one `RecoveryEnvelope`
801
+ shape (`schemaVersion: 1`):
802
+
803
+ 1. **Standalone lookup**: `dbcli recovery --code <CODE>` synthesizes an
804
+ envelope for any known recovery code without needing a real failure.
805
+ 2. **Failing-command opt-in**: pass `--recovery` to `dbcli query` or
806
+ `dbcli q`. On failure, the envelope is written to stdout as JSON, the
807
+ human stderr message is suppressed, and the process exits non-zero.
808
+
809
+ Recovery codes (fixed in v1.15.0):
810
+ - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
811
+ - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
812
+ - `PERMISSION_DENIED` — active permission level forbids the operation.
813
+ - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
814
+ - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
815
+ - `SCHEMA_CACHE_MISSING` — local schema cache missing or stale.
816
+ - `UNKNOWN` — fallback for unclassified errors.
817
+
818
+ Flags (lookup mode):
819
+ - `--code <CODE>` — required unless `--list` is set.
820
+ - `--list` — list all codes and exit.
821
+ - `--format json|markdown` (default: json).
822
+ - `--brief` — drop `rationale` + `expects` from steps.
823
+ - `--for-agent` — shortcut for `--format json --brief`.
824
+ - `--hint <text>` — bind into placeholder steps.
825
+ - `--snippet <name>` — bind snippet placeholder.
826
+ - `--table <name>` — bind table placeholder.
827
+
828
+ Examples:
829
+
830
+ dbcli recovery --code CONN_REFUSED
831
+ dbcli recovery --code BLACKLIST_TABLE --table users --format markdown
832
+ dbcli recovery --list --for-agent
833
+ dbcli query "SELECT * FROM users" --recovery
834
+ dbcli q @diag/missing --recovery
835
+
836
+ Boundaries:
837
+ - Recovery only **suggests** commands; agents (or humans) execute them. No automatic remediation in v1.15.0.
838
+ - 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.
839
+ - `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.
840
+ - `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.
841
+ - Recovery steps reuse the v1.14.0 `GuideStep` shape, including the full `risk` enum (`readonly` / `dry-run` / `write` / `unknown`).
842
+
843
+ **Permission:** n/a
844
+
845
+ ### recover
846
+
847
+ (v1.17.0+) Inspect or apply the last recovery plan saved by `--recovery`.
848
+
849
+ | Flag | Purpose | Default |
850
+ |---|---|---|
851
+ | `--apply` | Execute the saved plan under risk gating. | off (inspect only) |
852
+ | `--from <path>` | Read the envelope from this file instead of `.dbcli/last-recovery.json`. Accepts raw `RecoveryEnvelope` or `SavedRecoveryEnvelope`. | — |
853
+ | `--allow-write <tier>` | Open the risk gate. Values: `readonly-cmd` (local-side writes) \| `write-cmd` (database writes). | `none` |
854
+ | `--no-verify` | Skip the verify step appended after a successful `--apply`. | off (verify runs by default) |
855
+ | `--format <format>` | `markdown` \| `json`. | `markdown` for inspect, `json` for `--apply` |
856
+
857
+ #### Plan source resolution
858
+
859
+ 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.
860
+ 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.
861
+ 3. Otherwise, exits 2 with `No recovery plan available. Run a command with --recovery to generate one, or pass --from <file>.`
862
+
863
+ #### Code-owned tier (trust boundary)
864
+
865
+ `--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.
866
+
867
+ | Allowlist tier | Meaning | Example commands |
868
+ |---|---|---|
869
+ | `readonly` | local read-only | `dbcli inspect`, `dbcli doctor`, `dbcli blacklist list`, `dbcli schema <table>` |
870
+ | `dry-run` | write subcommand invoked with `--dry-run` | `dbcli update orders --where id=1 --dry-run`, `dbcli q @x --dry-run` |
871
+ | `local-write` | writes local config / cache / blacklist | `dbcli blacklist remove <table>`, `dbcli use <name>`, `dbcli schema --refresh` |
872
+ | `db-write` | mutates the connected database | `dbcli update orders --where id=1 --set …` (no `--dry-run`), `dbcli q @x` (no `--dry-run`) |
873
+ | `interactive` | requires TTY | `dbcli init`, `dbcli init --force` |
874
+
875
+ `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.
876
+
877
+ #### Risk gate matrix
878
+
879
+ | Allowlist tier | Default | `--allow-write=readonly-cmd` | `--allow-write=write-cmd` |
880
+ |---|---|---|---|
881
+ | `readonly` | run | run | run |
882
+ | `dry-run` | run | run | run |
883
+ | `local-write` | `skipped:risk` | run | run |
884
+ | `db-write` | `skipped:risk` | `skipped:risk` | run |
885
+ | `interactive` | `skipped:interactive` | `skipped:interactive` | `skipped:interactive` |
886
+ | unresolved placeholder in `command` | `skipped:placeholder` | `skipped:placeholder` | `skipped:placeholder` |
887
+ | command fails parse / allowlist | `skipped:unsafe-command` | `skipped:unsafe-command` | `skipped:unsafe-command` |
888
+
889
+ Precedence: envelope `interactive: true` > `placeholder` > `unsafe-command` > allowlist `interactive` > tier-based gating.
890
+
891
+ #### Exit codes
892
+
893
+ | Code | Condition |
894
+ |---|---|
895
+ | 0 | At least one step ran successfully and no step failed. |
896
+ | 1 | A step exited non-zero (fail-fast); see `stoppedAt`. |
897
+ | 2 | Envelope missing or malformed (failed schema validation, or saved `cwd` missing). |
898
+ | 3 | Every step was skipped — open `--allow-write` or fill placeholders. |
899
+
900
+ #### Auto-saved envelope
901
+
902
+ 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.
903
+
904
+ #### Verification (P4)
905
+
906
+ Each `RecoveryEnvelope` now carries an optional `verify: GuideStep` (always
907
+ `risk: 'readonly'`, never carries placeholders). `dbcli recover --apply` runs
908
+ the verify step after the main plan, only when `finalStatus === 'ok'` and
909
+ `--no-verify` is not set.
910
+
911
+ | Recovery code | Verify command | Heuristic |
912
+ |---|---|---|
913
+ | CONFIG_MISSING | `dbcli inspect --no-connect --format json` | `connection.name` truthy → passed |
914
+ | CONN_REFUSED / CONN_TIMEOUT / CONN_UNKNOWN / CONN_AUTH_FAILED / CONN_HOST_NOT_FOUND | `dbcli doctor --format json` | exit 0 → passed |
915
+ | PERMISSION_DENIED | `dbcli inspect --for-agent` | exit 0 → passed |
916
+ | BLACKLIST_TABLE | `dbcli inspect --for-agent` | exit 0 → passed |
917
+ | BLACKLIST_COLUMN_WRITE | `dbcli inspect --for-agent` | exit 0 → passed |
918
+ | SNIPPET_NOT_FOUND / SNIPPET_AMBIGUOUS / SNIPPET_PARAM_MISSING | `dbcli queries list --format json` | exit 0 → passed |
919
+ | SCHEMA_CACHE_MISSING | `dbcli inspect --format json` | `schemaCache.available === true` → passed |
920
+ | UNKNOWN | `dbcli doctor --format json` | exit 0 → passed |
921
+
922
+ `verifyStatus` values:
923
+
924
+ - `passed` — heuristic confirmed.
925
+ - `failed` — verifier exited non-zero or timed out.
926
+ - `indeterminate` — verifier exited 0 but expected shape not present, or the
927
+ step was gated (placeholder / unsafe-command); agents should re-check.
928
+
929
+ Exit codes are unchanged — `verifyStatus` is signal, not gate.
930
+
931
+ **Schema additions.** `RecoveryEnvelope.verify?: GuideStep` is additive (no
932
+ `schemaVersion` bump). v1.16 consumers ignore the field.
933
+
934
+ #### Multi-turn `--next` (P2)
935
+
936
+ `dbcli recover --next` returns the single next step in a saved recovery plan,
937
+ given which step the agent just executed and the result of that step. v1 walks
938
+ the plan linearly; future codes may branch on `prevResult.stdoutSummary`
939
+ deterministically.
940
+
941
+ | Flag | Required | Description |
942
+ |---|---|---|
943
+ | `--next` | yes | Activate the multi-turn lookup. |
944
+ | `--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). |
945
+ | `--result <value>` | yes | JSON `StepResultSummary` (inline) or `@<path>` to read from a file. |
946
+ | `--branch <id>` | no | Walk a specific branch by id (required on `--next` calls after a fork). See *Connection branching* below. |
947
+ | `--from <path>` | no | Override the auto-saved envelope. |
948
+ | `--format <fmt>` | no | `json` (default) or `markdown`. |
949
+
950
+ `--next` and `--apply` cannot be combined. `--allow-write` and `--no-verify`
951
+ are silently ignored under `--next` (no execution, no verification).
952
+
953
+ **`StepResultSummary` shape**
954
+
955
+ ```ts
956
+ interface StepResultSummary {
957
+ status: 'ok' | 'failed' | 'skipped'
958
+ exitCode?: number
959
+ stdoutSummary?: string // last 4 KB; longer rejected
960
+ stderrSummary?: string // last 4 KB; longer rejected
961
+ }
962
+ ```
963
+
964
+ `@<path>` resolves relative to the dbcli invocation cwd. File whole-size cap is
965
+ 64 KB; per-field 4 KB cap still applies.
966
+
967
+ **`NextResult` shape (output)**
968
+
969
+ ```ts
970
+ interface NextResult {
971
+ schemaVersion: 1
972
+ kind: 'step' | 'done'
973
+ source: { kind: 'auto' | 'from'; path: string }
974
+ errorCode: RecoveryCode
975
+ cursor: number // step.order when kind='step'; totalSteps when 'done'
976
+ totalSteps: number
977
+ step?: GuideStep // present iff kind='step'
978
+ branchId?: string // set iff agent is currently traversing a branch
979
+ branchDescription?: string // mirror of branches[branchId].description
980
+ }
981
+ ```
982
+
983
+ **Connection branching**
984
+
985
+ 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:
986
+
987
+ | Branch id | When chosen |
988
+ |---|---|
989
+ | `doctor-clean` | Doctor reports no errors — likely transient; verify baseline state, then retry. |
990
+ | `doctor-config-missing` | Doctor flagged a config-level failure (missing / invalid config). Re-init before reconnecting. |
991
+ | `doctor-auth-error` | Doctor confirms credentials were rejected. Re-init with `--force` to overwrite credentials. |
992
+ | `doctor-network-error` | Doctor confirms a network-level failure (host / port / DNS / timeout). Inspect and re-init host/port. |
993
+
994
+ 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).
995
+
996
+ **Exit codes**
997
+
998
+ | Exit | Condition |
999
+ |---|---|
1000
+ | 0 | Returned a step or `done`. |
1001
+ | 2 | Envelope missing/malformed; `--after-step` missing/out-of-range; `--result` missing/malformed; `--next` combined with `--apply`. |
1002
+
1003
+ **Examples**
1004
+
1005
+ ```bash
1006
+ # Walk a 3-step plan to completion
1007
+ dbcli recover --next --after-step 1 --result '{"status":"ok"}' # → step 2
1008
+ dbcli recover --next --after-step 2 --result '{"status":"ok"}' # → step 3
1009
+ dbcli recover --next --after-step 3 --result '{"status":"ok"}' # → done
1010
+
1011
+ # Result read from file (when stdout is large)
1012
+ dbcli recover --next --after-step 1 --result @/tmp/r1.json
1013
+
1014
+ # Markdown for human inspection
1015
+ dbcli recover --next --after-step 1 --result '{"status":"ok"}' --format markdown
1016
+ ```
1017
+
1018
+ **Permission:** n/a (always-allowed lookup; child processes inherit the active permission level).
1019
+
1020
+ ### audit
1021
+
1022
+ (v1.20.0+) Inspect, query, and manage the per-connection audit log written to `.dbcli/audit/<connection>.jsonl`.
1023
+
1024
+ 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).
1025
+
1026
+ #### Subcommands
1027
+
1028
+ | Subcommand | Side-effect tier | Purpose |
1029
+ |---|---|---|
1030
+ | `audit tail` | `readonly` | List most recent entries on the current (or `--all`) connection. |
1031
+ | `audit show` | `readonly` | Print a single full entry by id prefix or `--recovery-ref`. |
1032
+ | `audit clear` | `local-write` | Delete `<conn>.jsonl` + rotated `.jsonl.1` from local disk. Requires `--yes` or interactive confirm. |
1033
+ | `audit health` | `readonly` | Render `AuditLogger.getHealth()` snapshot (writer state, lock state, rotation usage). |
1034
+
1035
+ #### `audit tail`
1036
+
1037
+ | Flag | Purpose | Default |
1038
+ |---|---|---|
1039
+ | `--n <N>` | Number of recent entries to print (latest at bottom — D5). | `10` |
1040
+ | `--all` | Merge entries across all connections; output is an envelope array `[{ connection, entry }, ...]` (D-39). | off (current connection only) |
1041
+ | `--for-agent` | Shortcut for `--format json --brief`. Single-connection JSON is a flat array; `--all` JSON is an envelope array. | off |
1042
+ | `--brief` | Drop large redaction fields from the entry; keep `ts / command / target / success` (D-33). | off |
1043
+ | `--format <fmt>` | `table` \| `json`. | `table` |
1044
+
1045
+ 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.
1046
+
1047
+ Examples:
1048
+
1049
+ dbcli audit tail --n 10
1050
+ dbcli audit tail --all --for-agent --n 20
1051
+ dbcli audit tail --format json --brief
1052
+
1053
+ #### `audit show`
1054
+
1055
+ | Flag | Purpose | Default |
1056
+ |---|---|---|
1057
+ | `<id-prefix>` | Positional. UUID or prefix ≥ 4 characters; ambiguous prefix exits 1 with disambiguation hint; prefix < 4 chars exits 1. | — |
1058
+ | `--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). | — |
1059
+ | `--all` | Search across all connections. Output is an envelope `{ connection, entry }` (single-hit also envelope, for shape stability — D-36). | off |
1060
+ | `--format <fmt>` | `table` \| `json`. | `table` |
1061
+
1062
+ Examples:
1063
+
1064
+ dbcli audit show 1a2b
1065
+ dbcli audit show --recovery-ref 8f0e-1234-... --format json
1066
+ dbcli audit show 1a2b --all
1067
+
1068
+ #### `audit clear`
1069
+
1070
+ | Flag | Purpose | Default |
1071
+ |---|---|---|
1072
+ | `--yes` | Skip interactive confirmation. Required in non-TTY contexts. | off (interactive confirm) |
1073
+
1074
+ 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.`
1075
+
1076
+ Examples:
1077
+
1078
+ dbcli audit clear # interactive (TTY only)
1079
+ dbcli audit clear --yes # CI / scripted
1080
+
1081
+ #### `audit health`
1082
+
1083
+ | Flag | Purpose | Default |
1084
+ |---|---|---|
1085
+ | `--format <fmt>` | `table` \| `json`. | `table` |
1086
+
1087
+ 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).
1088
+
1089
+ #### Boundaries
1090
+
1091
+ - Entries are append-only JSONL; rotation triggers at `~10 MB` or `~1000` entries (whichever first). Previous segment is preserved as `.jsonl.1`.
1092
+ - 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.
1093
+ - 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.
1094
+ - 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`.
1095
+
1096
+ #### Exit codes
1097
+
1098
+ | Code | Condition |
1099
+ |---|---|
1100
+ | 0 | Read/list/clear/health succeeded; also `audit.enabled = false` opt-out path (E note). |
1101
+ | 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). |
1102
+ | 1 | `audit clear` — non-TTY without `--yes` (D-46). |
1103
+ | 1 | Reader corruption — mid-file non-JSON line in a `.jsonl` segment. |
1104
+
1105
+ **Permission:** n/a
1106
+
1107
+ ### doctor
1108
+
1109
+ Run diagnostic checks on environment, configuration, connection, and data.
1110
+
1111
+ ```bash
1112
+ dbcli doctor # Colored text output
1113
+ dbcli doctor --format json # JSON output for AI agents
1114
+ ```
1115
+
1116
+ **Checks:**
1117
+ - Environment: Bun version, dbcli version (compares with npm registry)
1118
+ - Configuration: config file exists/valid, permission level, blacklist completeness (detects unprotected sensitive columns)
1119
+ - Connection & Data: database connectivity, schema cache freshness (warns if > 7 days), large table warnings (> 1M rows)
1120
+
1121
+ > **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.
1122
+
1123
+ **Exit code:** 0 if all pass or warnings only, 1 if any error
1124
+ **Options:** `--format <text|json>`
1125
+
1126
+ ### completion
1127
+
1128
+ Generate shell completion scripts for tab auto-complete.
1129
+
1130
+ ```bash
1131
+ dbcli completion bash # Output bash completion script
1132
+ dbcli completion zsh # Output zsh completion script
1133
+ dbcli completion fish # Output fish completion script
1134
+ dbcli completion --install # Auto-detect shell and install
1135
+ dbcli completion --install zsh # Install for specific shell
1136
+ ```
1137
+
1138
+ **Supported shells:** bash, zsh, fish
1139
+
1140
+ ### upgrade
1141
+
1142
+ Check for updates and self-upgrade dbcli to the latest version from npm.
1143
+
1144
+ ```bash
1145
+ dbcli upgrade # Check and upgrade if newer version available
1146
+ dbcli upgrade --check # Only check, do not upgrade
1147
+ ```
1148
+
1149
+ **Options:** `--check`
1150
+
1151
+ **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.
1152
+
1153
+ ### `dbcli shell`
1154
+
1155
+ Start an interactive database shell.
1156
+
1157
+ ```bash
1158
+ dbcli shell # Interactive mode with SQL + dbcli commands
1159
+ dbcli shell --sql # SQL-only mode
1160
+ ```
1161
+
1162
+ Inside the shell:
1163
+ - Type SQL statements ending with `;` to execute
1164
+ - Type dbcli commands without the `dbcli` prefix (e.g., `schema users`)
1165
+ - Use Tab for auto-completion (SQL keywords, table names, column names)
1166
+ - Type `.help` for meta commands (.quit, .clear, .format, .history, .timing)
1167
+ - Multi-line SQL: keeps accumulating until `;` is found
1168
+ - History persists across sessions (~/.dbcli_history)
1169
+
1170
+ The REPL flavor depends on the active engine: SQL engines and MongoDB use the
1171
+ form above; **Redis** opens a single-line command REPL (see [Redis › Interactive
1172
+ shell](#interactive-shell)); **Elasticsearch** opens a Kibana Dev Tools-style
1173
+ REPL (v1.22, see [Elasticsearch › Interactive shell](#interactive-shell-v122)).
1174
+
1175
+ ### migrate
1176
+
1177
+ Schema DDL operations. **All commands default to dry-run** — use `--execute` to actually run the SQL. Destructive operations (DROP) also require `--force`.
1178
+
1179
+ ```bash
1180
+ # Create table
1181
+ dbcli migrate create posts \
1182
+ --column "id:serial:pk" \
1183
+ --column "title:varchar(200):not-null" \
1184
+ --column "body:text" \
1185
+ --column "created_at:timestamp:default=now()"
1186
+
1187
+ # Drop table (dry-run by default)
1188
+ dbcli migrate drop posts
1189
+ dbcli migrate drop posts --execute --force # Actually drop
1190
+
1191
+ # Add/drop/alter column
1192
+ dbcli migrate add-column users bio text --nullable
1193
+ dbcli migrate drop-column users temp_field --execute --force
1194
+ dbcli migrate alter-column users name --type "varchar(200)"
1195
+ dbcli migrate alter-column users email --rename user_email
1196
+ dbcli migrate alter-column users status --set-default "'active'"
1197
+ dbcli migrate alter-column users bio --drop-default
1198
+ dbcli migrate alter-column users bio --set-nullable
1199
+ dbcli migrate alter-column users email --drop-nullable
1200
+
1201
+ # Index management
1202
+ dbcli migrate add-index users --columns email --unique
1203
+ dbcli migrate add-index users --columns "last_name,first_name" --name idx_fullname
1204
+ dbcli migrate drop-index idx_fullname --execute --force
1205
+
1206
+ # Constraint management
1207
+ dbcli migrate add-constraint orders --fk user_id --references users.id --on-delete cascade
1208
+ dbcli migrate add-constraint users --unique email
1209
+ dbcli migrate add-constraint users --check "age >= 0"
1210
+ dbcli migrate drop-constraint orders fk_orders_user_id --execute --force
1211
+
1212
+ # Enum (PostgreSQL only — MySQL uses inline ENUM in column type)
1213
+ dbcli migrate add-enum status active inactive suspended
1214
+ dbcli migrate alter-enum status --add-value archived
1215
+ dbcli migrate drop-enum status --execute --force
1216
+ ```
1217
+
1218
+ **Column spec format:** `name:type[:modifier[:modifier...]]`
1219
+ - Modifiers: `pk`, `not-null`, `unique`, `auto-increment`, `default=<value>`, `references=<table>.<column>`
1220
+ - Serial types: `serial`, `bigserial`, `smallserial` (auto-expand per DB dialect)
1221
+
1222
+ **Options (all subcommands):** `--execute`, `--force`, `--config <path>`
1223
+ **Permission:** admin
1224
+
1225
+ **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.
1226
+
1227
+ ### skill
1228
+
1229
+ Emit `SKILL.md` (and the companion `reference.md`) to stdout, a file, or an
1230
+ AI-agent platform directory. The skill is the source of truth that lets
1231
+ Claude Code / Gemini / Antigravity / Copilot / Cursor know how to drive dbcli safely.
1232
+
1233
+ ```bash
1234
+ dbcli skill # print SKILL.md to stdout
1235
+ dbcli skill --output ./SKILL.md # write to a file (no platform install)
1236
+ dbcli skill --install claude # install to ~/.claude/skills/dbcli/
1237
+ dbcli skill --install gemini # install to ~/.gemini/skills/dbcli/ (being phased out)
1238
+ dbcli skill --install antigravity # install to ~/.gemini/antigravity-cli/skills/dbcli/
1239
+ dbcli skill --install copilot # install to .github/skills/dbcli/ (repo-local)
1240
+ dbcli skill --install cursor # install to .cursor/skills/dbcli/ (repo-local)
1241
+ dbcli skill --install codex # install to ~/.codex/skills/dbcli/
1242
+ ```
1243
+
1244
+ **Options:**
1245
+ - `--install <platform>` — `claude` | `gemini` | `antigravity` | `copilot` | `cursor` | `codex` | `windsurf`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
1246
+ - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
1247
+
1248
+ **Notes:**
1249
+ - 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.
1250
+ - `claude` / `gemini` / `antigravity` install paths are user-global; `copilot` / `cursor` are repo-local under `.github/` / `.cursor/`.
1251
+ - 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`.
1252
+ - 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`.
1253
+ - Agent plugin installation details live in `plugins/dbcli-agent/INSTALL.md`, including Codex, Claude Code, GitHub Copilot CLI, Antigravity (`agy`), and Cursor targets.
1254
+ - `gemini` (Gemini CLI) is retained for now but is being phased out in favour of `antigravity` (Antigravity CLI), Google's successor terminal agent.
1255
+ - Re-running `--install` overwrites the existing skill atomically; no prompt.
1256
+
1257
+ **Permission:** n/a.
1258
+
1259
+ ### skill tasks (Agent Task Packs)
1260
+
1261
+ ```bash
1262
+ dbcli skill tasks list # human table
1263
+ dbcli skill tasks list --format json --tag diagnostics
1264
+ dbcli skill tasks list --engine postgres --source builtin
1265
+ dbcli skill tasks show diagnose-slow-query
1266
+ dbcli skill tasks show diagnose-slow-query --format json
1267
+ dbcli skill tasks plan diagnose-slow-query --param query="SELECT 1"
1268
+ dbcli skill tasks plan diagnose-slow-query --param query="..." --format json
1269
+ ```
1270
+
1271
+ - **list filters:** `--tag <tag>`, `--engine <postgres|mysql|mongodb|redis|elasticsearch>`, `--source <builtin|shared|local>`, `--format <table|json>`.
1272
+ - **show:** prints the full task definition (frontmatter + Agent Notes). Use `--format json` for an agent-friendly contract.
1273
+ - **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.
1274
+
1275
+ **Builtin packs:** `diagnose-slow-query` and **(v1.23)** `analyze-table-perf` —
1276
+ a read-only (`plan-only`) pack taking a required `table` parameter that walks
1277
+ `blacklist list` → `schema <table> --format json` → `guide index-usage --format json`.
1278
+ `dbcli inspect` suggests `analyze-table-perf` automatically for the hottest table
1279
+ in recent audit activity. Additional read-only packs ship for common agent
1280
+ workflows: `audit-permissions` (permission/blacklist audit), `safe-backfill`
1281
+ (plan a write with blacklist+schema+risk checks), `schema-drift-review` (cached
1282
+ vs live schema diff), and `connection-health` (reachability/config/capacity
1283
+ triage). Run `dbcli skill tasks list` for the full set.
1284
+
1285
+ ```bash
1286
+ dbcli skill tasks plan analyze-table-perf --param table=betting_logs --format json
1287
+ ```
1288
+
1289
+ Task storage layers:
1290
+
1291
+ | Source | Path | Notes |
1292
+ | --- | --- | --- |
1293
+ | builtin | `assets/tasks/` | shipped with dbcli |
1294
+ | shared | `.dbcli-shared/tasks/` | team-managed, version-controlled |
1295
+ | local | `.dbcli/tasks/` | personal, gitignored |
1296
+
1297
+ Higher tiers override lower tiers by task name. Task name is derived from the
1298
+ file path under the tier root (e.g. `diag/inspect.md` → `diag/inspect`).
1299
+
1300
+ ## Recovery Cookbook (agent walkthroughs)
1301
+
1302
+ End-to-end recovery sessions for the most common failure codes. All examples
1303
+ assume the agent invoked a `--recovery`-capable command and received a
1304
+ `RecoveryEnvelope` (or hit the same envelope via `dbcli recovery --code <CODE>`
1305
+ lookup). See [§recovery](#recovery) for the envelope shape, [§recover](#recover)
1306
+ for `--apply` / `--next` / risk-gate semantics, and [§audit](#audit) for the
1307
+ bi-directional `audit_ref` ⇄ `recovery_ref` pivot.
1308
+
1309
+ ### Scenario index
1310
+
1311
+ | Code | Trigger | Primary remediation | Risk tier |
1312
+ |------|---------|---------------------|-----------|
1313
+ | `CONN_REFUSED` | Database process down or wrong host/port. | `dbcli doctor` → fix host/port → retry. | `readonly` |
1314
+ | `CONN_AUTH_FAILED` | Credentials rejected. | Re-check `.dbcli`/env, rotate credentials, `dbcli init --force` only on explicit user nod. | `readonly` → `interactive` |
1315
+ | `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` |
1316
+ | `BLACKLIST_TABLE` | Target table is blacklisted. | `dbcli blacklist list` → `blacklist table remove <name>` (local-write tier). | `readonly` + `local-write` |
1317
+ | `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` |
1318
+ | `SCHEMA_CACHE_MISSING` | Fresh checkout / new v2 connection / cache wiped. | `dbcli schema --refresh --force` (or `--use <conn>` per-connection). | `readonly` |
1319
+ | `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` | Typo or duplicate snippet name. | `dbcli queries list` → `queries search <kw>` → run correct `@name`. | `readonly` |
1320
+ | `SNIPPET_PARAM_MISSING` | `--param k=v` not supplied. | `dbcli queries show @name` lists required params → re-run with full set. | `readonly` |
1321
+ | `CONFIG_MISSING` | No `.dbcli` in cwd. | `dbcli init` (human-driven). | `interactive` |
1322
+
1323
+ `risk` enum: `readonly` / `dry-run` / `write` / `unknown` (see §recovery boundaries).
1324
+ Allowlist tier: `readonly` / `dry-run` / `local-write` / `db-write` / `interactive` (see [§recover Risk gate matrix](#risk-gate-matrix)).
1325
+
1326
+ ### S1 — CONN_REFUSED end-to-end
1327
+
1328
+ ```bash
1329
+ # 1. Failing call writes envelope to stdout AND .dbcli/last-recovery.json
1330
+ $ dbcli query "SELECT 1" --recovery --format json
1331
+ {
1332
+ "schemaVersion": 1,
1333
+ "error": { "code": "CONN_REFUSED", "message": "..." },
1334
+ "audit_ref": "1f8e...c4d2",
1335
+ "recovery": [
1336
+ { "order": 1, "command": "dbcli doctor --format json", "risk": "readonly", ... },
1337
+ { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly", ... }
1338
+ ],
1339
+ "verify": { "command": "dbcli doctor --format json", "risk": "readonly", ... }
1340
+ }
1341
+
1342
+ # 2. One-shot apply (only readonly + dry-run run by default)
1343
+ $ dbcli recover --apply --format json
1344
+ { "finalStatus": "ok", "executed": [...], "verifyStatus": "passed" }
1345
+ # Exit 0 → root cause cleared (verify probe succeeded).
1346
+
1347
+ # 3. If verify reported `failed` / `indeterminate`, drop into --next for control
1348
+ $ dbcli recover --next --after-step 1 --result '{"status":"failed","exitCode":1}'
1349
+ # → returns a refined step 2 or `kind:"done"` based on the prevResult
1350
+ ```
1351
+
1352
+ ### S2 — PERMISSION_DENIED with implicit `--dry-run` preview
1353
+
1354
+ ```bash
1355
+ $ dbcli update orders --where "id=1" --set '{"status":"shipped"}' --recovery --format json
1356
+ {
1357
+ "error": { "code": "PERMISSION_DENIED", ... },
1358
+ "audit_ref": "9ab0...e711",
1359
+ "recovery": [
1360
+ { "order": 1, "command": "dbcli update orders --where 'id=1' --set '<redacted>' --dry-run", "risk": "dry-run" },
1361
+ { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly" }
1362
+ ]
1363
+ }
1364
+
1365
+ # Default apply runs both steps (dry-run is in-tier).
1366
+ $ dbcli recover --apply
1367
+ ```
1368
+
1369
+ When the failing operation is INSERT/UPDATE/DELETE, the envelope prepends a
1370
+ `risk: 'dry-run'` step (the same write subcommand with `--dry-run`). Run it
1371
+ before any escalation — it both teaches the agent what the SQL looks like and
1372
+ proves the change is well-formed before raising the permission tier.
1373
+
1374
+ ### S3 — BLACKLIST_TABLE (local-write remediation)
1375
+
1376
+ ```bash
1377
+ $ dbcli query "SELECT * FROM audit_logs" --recovery --format json
1378
+ # error.code: BLACKLIST_TABLE
1379
+ # recovery[0]: dbcli blacklist list (risk: readonly)
1380
+ # recovery[1]: dbcli blacklist table remove audit_logs (risk: write — local-write tier)
1381
+
1382
+ # Default --apply: step 1 runs, step 2 skipped:risk → exit 3.
1383
+ $ dbcli recover --apply
1384
+ # To proceed: open the gate to local-write tier ONLY (does not touch DB).
1385
+ $ dbcli recover --apply --allow-write=readonly-cmd
1386
+ { "finalStatus": "ok", "executed": [step1, step2], "verifyStatus": "passed" }
1387
+ ```
1388
+
1389
+ ### S4 — BLACKLIST_COLUMN_WRITE (preview-then-drop)
1390
+
1391
+ ```bash
1392
+ $ dbcli insert users --data '{"name":"a","ssn":"123"}' --recovery
1393
+ # recovery[0]: dbcli insert users --data '<redacted>' --dry-run (risk: dry-run)
1394
+ # recovery[1]: dbcli blacklist list (risk: readonly)
1395
+ # recovery[2]: dbcli blacklist column remove users.ssn (risk: write — local-write)
1396
+
1397
+ # Preferred path: don't widen the blacklist — re-shape the agent's payload to drop ssn.
1398
+ # Apply only the diagnostic prefix (steps 1+2) to confirm what columns are masked:
1399
+ $ dbcli recover --apply
1400
+ # Then re-issue insert without `ssn`.
1401
+ ```
1402
+
1403
+ ### S5 — SCHEMA_CACHE_MISSING (fresh / multi-conn)
1404
+
1405
+ ```bash
1406
+ $ dbcli inspect --require-schema-cache --recovery --format json
1407
+ # error.code: SCHEMA_CACHE_MISSING
1408
+ # recovery[0]: dbcli schema --refresh --force (risk: readonly — populates .dbcli/schemas/)
1409
+ # verify: dbcli inspect --format json (schemaCache.available === true)
1410
+
1411
+ $ dbcli recover --apply
1412
+ # Per-connection cache lives at .dbcli/schemas/<connection>/. If the failure was on
1413
+ # a v2 named connection, the envelope's command already carries `--use <name>`.
1414
+ ```
1415
+
1416
+ ### S6 — SNIPPET_NOT_FOUND with disambiguation
1417
+
1418
+ ```bash
1419
+ $ dbcli q @anaytics/revenue --recovery
1420
+ # typo: anaytics → analytics
1421
+ # recovery[0]: dbcli queries list --format json
1422
+ # recovery[1]: dbcli queries search analytics (or whatever --hint suggests)
1423
+ $ dbcli recover --apply
1424
+ # Agent reads stdoutSummary, identifies the correct @name, then re-issues:
1425
+ $ dbcli q @analytics/revenue --param days=30
1426
+ ```
1427
+
1428
+ ### Multi-turn `--next` walkthrough (3-step plan)
1429
+
1430
+ Use `--next` instead of `--apply` when:
1431
+
1432
+ - `--apply` is too coarse-grained (the agent wants step-by-step inspection).
1433
+ - The plan contains an `interactive` step that `--apply` would skip.
1434
+ - The agent uses its own runner / sandbox and just wants dbcli to drive cursoring.
1435
+
1436
+ `--next` returns one step at a time, given which step the agent **just executed**
1437
+ and a `StepResultSummary` of how it went. dbcli does not persist the cursor —
1438
+ the agent owns `--after-step`.
1439
+
1440
+ ```bash
1441
+ # Envelope already saved at .dbcli/last-recovery.json (3-step plan, CONN_REFUSED).
1442
+
1443
+ # Round 1 — agent reads step 1 from the envelope, executes it itself, then asks
1444
+ # dbcli for the next step.
1445
+ $ dbcli recover --next --after-step 1 --result '{"status":"ok","exitCode":0}' --format json
1446
+ {
1447
+ "schemaVersion": 1,
1448
+ "kind": "step",
1449
+ "errorCode": "CONN_REFUSED",
1450
+ "cursor": 2,
1451
+ "totalSteps": 3,
1452
+ "step": { "order": 2, "command": "dbcli inspect --for-agent", "risk": "readonly", ... }
1453
+ }
1454
+
1455
+ # Round 2 — bigger stdout, save to file and reference it.
1456
+ $ ./run-step.sh > /tmp/r2.json # agent's own runner; result is StepResultSummary JSON
1457
+ $ dbcli recover --next --after-step 2 --result @/tmp/r2.json
1458
+ { "kind": "step", "cursor": 3, "step": { "order": 3, ... } }
1459
+
1460
+ # Round 3 — last step done.
1461
+ $ dbcli recover --next --after-step 3 --result '{"status":"ok"}'
1462
+ { "kind": "done", "cursor": 3, "totalSteps": 3 }
1463
+ ```
1464
+
1465
+ `StepResultSummary` contract (recap of [§recover Multi-turn](#multi-turn---next-p2)):
1466
+
1467
+ ```ts
1468
+ interface StepResultSummary {
1469
+ status: 'ok' | 'failed' | 'skipped'
1470
+ exitCode?: number
1471
+ stdoutSummary?: string // last 4 KB
1472
+ stderrSummary?: string // last 4 KB
1473
+ }
1474
+ ```
1475
+
1476
+ Truncate to the **last** 4 KB before passing — the head of a huge stdout is
1477
+ usually not what disambiguates next steps.
1478
+
1479
+ Verification is **not** automatic under `--next`. If the agent wants the same
1480
+ verify probe `--apply` runs, it must execute the envelope's `verify` step
1481
+ itself after the plan completes.
1482
+
1483
+ ### Bi-directional pivot (envelope ⇄ audit)
1484
+
1485
+ Every `--recovery`-capable failure (`query`, `inspect`, `insert`, `update`,
1486
+ `delete`, `export`, `q`, `schema`) writes **both** sides of a UUID link:
1487
+
1488
+ - `RecoveryEnvelope.audit_ref` → the `audit.id` for the same failure.
1489
+ - `AuditEntry.recovery_ref` → the envelope's id (also the auto-saved
1490
+ `.dbcli/last-recovery.json` filename trace).
1491
+
1492
+ ```bash
1493
+ # From envelope → audit (forensics on a saved failure)
1494
+ $ ENV_ID=$(jq -r '.id' .dbcli/last-recovery.json) # or read from stdout
1495
+ $ dbcli audit show --recovery-ref "$ENV_ID" --format json
1496
+ # Returns the matching audit entry (full, not brief).
1497
+
1498
+ # From audit → envelope (you have an audit hit, want the structured plan)
1499
+ $ AUDIT_ID=$(dbcli audit tail --for-agent --n 1 | jq -r '.[0].id')
1500
+ $ dbcli audit show "$AUDIT_ID" --format json
1501
+ # Read `recovery_ref` from the entry, then either re-run --recovery against
1502
+ # the original command or load the saved envelope:
1503
+ $ jq '.recovery_ref' .dbcli/last-recovery.json | grep -q "$RECOVERY_REF" \
1504
+ && dbcli recover --format markdown # human inspect
1505
+ || dbcli recover --from /path/to/archived.json --format markdown
1506
+ ```
1507
+
1508
+ Session handoff: a fresh agent that opens `dbcli inspect --for-agent`,
1509
+ `dbcli guide`, `dbcli recover`, or `dbcli recover --apply` gets an
1510
+ `audit_recent: AuditEntryBrief[]` field (last 5 entries) embedded in the JSON
1511
+ output — no extra round-trip to the audit CLI needed for immediate history
1512
+ context.
1513
+
1514
+ ### Risk gate cheat sheet
1515
+
1516
+ Quick reference for what `--apply` runs at each `--allow-write` level. The
1517
+ canonical matrix lives at [§recover Risk gate matrix](#risk-gate-matrix); this
1518
+ table maps it onto common agent intents.
1519
+
1520
+ | Agent intent | Recommended flag | What runs | What's skipped |
1521
+ |---|---|---|---|
1522
+ | Probe-only (read state, learn) | `--apply` (default) | `readonly` + `dry-run` steps | `local-write`, `db-write`, `interactive` |
1523
+ | Local config remediation (e.g. `blacklist remove`) | `--apply --allow-write=readonly-cmd` | + `local-write` | `db-write`, `interactive` |
1524
+ | Database write recovery (rare; trusted plan) | `--apply --allow-write=write-cmd` | + `db-write` | `interactive` |
1525
+ | Interactive step (e.g. `dbcli init`) | Drive manually OR use `--next` | n/a | All interactive steps always skip under `--apply` |
1526
+ | Walk plan step-by-step with own runner | `--next --after-step N --result …` | one step per call | n/a — agent owns cursor + execution |
1527
+
1528
+ Three rules that always apply regardless of `--allow-write`:
1529
+
1530
+ 1. **Tier is code-owned, not envelope-claimed.** The risk gate reads the
1531
+ per-error-code allowlist after parsing argv. An envelope cannot escalate
1532
+ itself by setting `risk: 'readonly'` on a write subcommand — argv decides.
1533
+ 2. **Placeholders block.** A step with unresolved `<token>` placeholders is
1534
+ skipped as `skipped:placeholder` even at `--allow-write=write-cmd`. Bind
1535
+ them at `recovery` lookup time with `--hint` / `--snippet` / `--table`, or
1536
+ ask the user.
1537
+ 3. **Verify is signal, not gate.** `verifyStatus` ∈ `{passed, failed,
1538
+ indeterminate}` reports whether the original failure looks resolved.
1539
+ `recover --apply` exit code is set by step execution, not verification.
1540
+
1541
+ ### Common pitfalls
1542
+
1543
+ - **Stale `.dbcli/last-recovery.json`.** `recover` (no `--apply`) shows the
1544
+ *saved* plan, which may be hours old. Re-run the original command with
1545
+ `--recovery` to refresh it, or pass `--from <file>` to load an archived one.
1546
+ - **`.dbcli/` is gitignored.** Do not check `last-recovery.json` into a repo
1547
+ for "reproducibility"; it contains sanitized command snapshots but the
1548
+ workspace `cwd` only makes sense locally. Use `recover --from <archived.json>`
1549
+ for cross-machine replay.
1550
+ - **`--apply` exit 3 means every step skipped.** Not a failure — it means the
1551
+ default gate was too tight. Either widen with `--allow-write`, fill
1552
+ placeholders, or fall back to `--next` and drive steps manually.
1553
+ - **`--next` does not run verify.** Re-run the original failing command with
1554
+ `--recovery` once the plan is done; if it now succeeds (no envelope on
1555
+ stdout), recovery is complete. Or invoke `envelope.verify.command` yourself.
1556
+ - **Audit writer failures are non-fatal.** If `audit health` reports
1557
+ `lastWriteOk: false`, the main command still completed — but `recovery_ref`
1558
+ ⇄ `audit_ref` linkage is broken for that one call. `audit health` surfaces
1559
+ the underlying error (disk full, EACCES, etc.).
1560
+ - **Cross-connection forensics.** `audit tail --all --for-agent` merges all
1561
+ connections; `audit show <id-prefix> --all` returns an envelope `{connection,
1562
+ entry}` so a fresh agent can tell which DB the failure was against.
1563
+
1564
+ ## Interactive HTML dashboard
1565
+
1566
+ `query`, `q`, and `export` can render results as a single, fully self-contained
1567
+ HTML file backed by a bundled React + Recharts template. The template lives at
1568
+ `assets/ui-template.html` and is installed alongside the binary; no external
1569
+ network, CDN, or runtime is required to view the report.
1570
+
1571
+ ### Entry points
1572
+
1573
+ | Command form | Behaviour |
1574
+ |--------------|-----------|
1575
+ | `dbcli query "<sql>" --ui` | Render to a temp file under `$TMPDIR/dbcli-query-<ts>.html` and open with `open` / `xdg-open` / `start`. |
1576
+ | `dbcli q @<name> --ui` | Same, with snippet metadata (`name`, `description`, `visual:` block). |
1577
+ | `dbcli query "<sql>" --format html` | Print HTML to stdout (pipe, redirect, attach). |
1578
+ | `dbcli q @<name> --format html` | Same, snippet-aware. |
1579
+ | `dbcli export "<sql>" --format html --output report.html` | Write HTML to an explicit path; respects `--force` / overwrite confirmation. |
1580
+
1581
+ `--ui` is a convenience flag — it implies `--format html` and then opens the
1582
+ file. `--ui` and `--format` are mutually compatible; passing both is allowed and
1583
+ behaves as `--ui`.
1584
+
1585
+ ### Data injection contract
1586
+
1587
+ The template ships with a single placeholder, `/*DBCLI_PAYLOAD*/`, which dbcli
1588
+ replaces with:
1589
+
1590
+ ```js
1591
+ window.__DBCLI_PAYLOAD__ = { "meta": {...}, "rows": [...] };
1592
+ ```
1593
+
1594
+ Hardening rules applied before injection:
1595
+
1596
+ - Payload is `JSON.stringify(...)`-encoded.
1597
+ - Every `<` is replaced with `<` so a malicious row containing `</script>`
1598
+ cannot terminate the inline script tag.
1599
+ - Blacklist redaction (`dbcli blacklist`) runs **before** the formatter — masked
1600
+ columns never reach the dashboard.
1601
+ - The replacement uses a function callback (`html.replace(..., () => injection)`)
1602
+ so `$&`-style backreferences in the payload are not interpreted.
1603
+
1604
+ ### `meta` shape
1605
+
1606
+ `meta` is the `SavedQueryMeta` object (see `dbcli queries show @<name> --format json`):
1607
+
1608
+ ```jsonc
1609
+ {
1610
+ "name": "Revenue Trend", // display title
1611
+ "key": "@analytics/revenue", // snippet key, or "raw-sql" / "export"
1612
+ "description": "...", // free text (SQL preview for raw query)
1613
+ "params": [...], // ParamSpec[]
1614
+ "tags": ["analytics"],
1615
+ "intent": "perf.slow-query",
1616
+ "visual": { ... } // optional, see below
1617
+ }
1618
+ ```
1619
+
1620
+ For raw `query` / `export` invocations, `meta.params` is `[]` and
1621
+ `meta.visual` is absent — the dashboard renders a sortable / filterable table.
1622
+
1623
+ ### `visual:` block (snippet frontmatter)
1624
+
1625
+ ```yaml
1626
+ visual:
1627
+ title: Revenue (last :days days) # optional override of meta.name
1628
+ kpis:
1629
+ - label: Total Revenue
1630
+ value_column: total_revenue # must exist in result rows
1631
+ format: currency # currency | number | percent (optional)
1632
+ - label: Orders
1633
+ value_column: order_count
1634
+ format: number
1635
+ charts:
1636
+ - type: line # line | bar | area | pie | scatter
1637
+ title: Daily Revenue
1638
+ x: day # column for X axis
1639
+ y: [revenue] # 1..N columns for series
1640
+ - type: bar
1641
+ title: By Channel
1642
+ x: channel
1643
+ y: [revenue, refunds]
1644
+ ```
1645
+
1646
+ Parser behaviour (`src/core/saved-queries/parser.ts::normaliseVisual`):
1647
+
1648
+ - The block is **optional**. Missing → table-only render.
1649
+ - Items missing required fields (`kpi.label` + `kpi.value_column`, or
1650
+ `chart.type` + `chart.x` + `chart.y[]`) are silently dropped.
1651
+ - Unknown `format` / `type` values are forwarded as strings; the dashboard
1652
+ decides how to render them (unknown chart types fall back gracefully).
1653
+ - The snippet still executes as a normal SQL/DSL query — `visual:` only affects
1654
+ the HTML renderer.
1655
+
1656
+ ### Limitations
1657
+
1658
+ - The dashboard is read-only; there is no in-page editor or re-run button.
1659
+ - Raw `query` / `export` HTML output never shows KPIs or charts (no snippet
1660
+ metadata is available). Use `dbcli q @<name>` for the charted view.
1661
+ - Engine support follows the underlying command: SQL, MongoDB (`--collection`),
1662
+ Redis, and Elasticsearch (`--collection`) all render through the same template.
1663
+ - Very wide / very long result sets render as a single client-side table; for
1664
+ >10k rows prefer `--format csv` / `--format jsonl` and a downstream tool.
1665
+
1666
+ ## MongoDB Support
1667
+
1668
+ 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.
1669
+
1670
+ Atlas-style `mongodb+srv://` URIs are supported. `list` and `query` run against the database configured for the connection, and `query` always requires `--collection <name>`.
1671
+
1672
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `q`, `insert`, `update`, `delete`, `export`, `status`, `shell`, `doctor`, `upgrade`, `completion`
1673
+
1674
+ **Limited support:**
1675
+
1676
+ - `schema` samples collection documents to infer field names/types. It does not provide relational constraints, primary keys, foreign keys, or reliable index metadata.
1677
+ - `query` accepts only JSON object filters or aggregation pipeline arrays and always requires `--collection <name>`.
1678
+ - `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.
1679
+ - `insert` inserts one JSON document into the named collection.
1680
+ - `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`.
1681
+ - `delete` deletes all documents matching the JSON/simple filter.
1682
+ - `export` accepts the same JSON filter / aggregation syntax as `query`.
1683
+ - MongoDB write paths do not currently provide the same SQL dry-run, relational schema validation, or column-level blacklist filtering guarantees as SQL writes.
1684
+ - `shell` blocks raw SQL for MongoDB; use `query <json> --collection <name>` inside the shell.
1685
+
1686
+ **Not supported (exit with error):** `diff`, `migrate`
1687
+
1688
+ **Not a supported MongoDB target:** `check` is designed for relational health checks and emits SQL-style checks.
1689
+
1690
+ ### MongoDB-specific workflow
1691
+
1692
+ ```bash
1693
+ # 1. Initialize (URI or individual params)
1694
+ dbcli init --system mongodb --uri "mongodb+srv://user:pass@cluster.example.mongodb.net/mydb"
1695
+
1696
+ # 2. List collections
1697
+ dbcli list --format json
1698
+
1699
+ # 3. Query with JSON filter (find) or pipeline (aggregate)
1700
+ dbcli query '{}' --collection orders --format json # All documents
1701
+ dbcli query '{"status": "paid"}' --collection orders # Filter
1702
+ dbcli query '[{"$match": {"status":"paid"}}, {"$count":"total"}]' --collection orders # Pipeline
1703
+
1704
+ # 4. Document writes (permission-gated; no SQL dry-run semantics)
1705
+ dbcli insert orders --data '{"status":"paid","total":42}'
1706
+ dbcli update orders --where '{"status":"pending"}' --set '{"status":"paid"}'
1707
+ dbcli delete orders --where '{"status":"cancelled"}' --force
1708
+ ```
1709
+
1710
+ ### Query syntax
1711
+
1712
+ | Intent | Syntax |
1713
+ |--------|--------|
1714
+ | All documents | `'{}'` |
1715
+ | Field filter | `'{"field": "value"}'` |
1716
+ | Comparison | `'{"age": {"$gt": 18}}'` |
1717
+ | Aggregation | `'[{"$match": {...}}, {"$group": {...}}]'` |
1718
+
1719
+ ## Redis Support
1720
+
1721
+ 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.
1722
+
1723
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `shell`, `status`, `doctor`, `upgrade`, `completion`
1724
+
1725
+ **Saved queries:** `q` is supported for read-only Redis commands (see "Redis snippets" below).
1726
+
1727
+ **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.
1728
+
1729
+ ### Connection and configuration
1730
+
1731
+ - Required fields: `system: redis`, `host`, `port`. `password` and `database` are optional.
1732
+ - `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.
1733
+ - `connection.timeout` (ms, default 5000) maps to the client's `connectionTimeout`.
1734
+
1735
+ ### Permission classification
1736
+
1737
+ 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.
1738
+
1739
+ | Tier | Commands |
1740
+ |------|----------|
1741
+ | `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` |
1742
+ | `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` |
1743
+ | `data-admin` | `DEL`, `UNLINK`, `HDEL` |
1744
+ | `admin` | `FLUSHDB`, `FLUSHALL`, `CONFIG`, `INFO`, `CLIENT`, `DEBUG`, `SHUTDOWN`, `KEYS`, `MONITOR`, `SAVE`, `BGSAVE`, `BGREWRITEAOF`, `REPLICAOF`, `SLAVEOF`, `ACL` |
1745
+
1746
+ ### Schema inspection
1747
+
1748
+ `schema <key>` returns one synthetic row per key with these columns:
1749
+
1750
+ | column | meaning |
1751
+ |--------|---------|
1752
+ | `type` | Redis type (`string` / `hash` / `list` / `set` / `zset` / `stream` / `none`) |
1753
+ | `ttl` | `<n>s`, `no expiry`, or `missing` |
1754
+ | `size` | `STRLEN` / `HLEN` / `LLEN` / `SCARD` / `ZCARD` / `XLEN` depending on type |
1755
+ | `sample` | First 5 hash field names (hash only) |
1756
+
1757
+ `schema` (no key) and `--refresh` / `--reset` are rejected — there is no full-database schema cache for Redis.
1758
+
1759
+ ### Recommended `query` patterns
1760
+
1761
+ ```bash
1762
+ # Read
1763
+ dbcli query "GET feature:flag"
1764
+ dbcli query "HGETALL user:42" --format json
1765
+ dbcli query "LRANGE queue:jobs 0 9"
1766
+
1767
+ # Iterate keys (paginated; never use KEYS — admin-only)
1768
+ dbcli query "SCAN 0 MATCH session:* COUNT 200"
1769
+
1770
+ # Write (requires read-write+)
1771
+ dbcli query "SET counter 1"
1772
+ dbcli query "EXPIRE session:abc 3600"
1773
+ dbcli query "HSET user:42 name Alice"
1774
+
1775
+ # Delete (requires data-admin+)
1776
+ dbcli query "DEL temp:lock"
1777
+ dbcli query "HDEL user:42 lastLogin"
1778
+ ```
1779
+
1780
+ ### Size guard (`query --no-limit` / shell `.no-limit`)
1781
+
1782
+ The adapter rewrites unbounded reads before dispatch and truncates oversized replies after:
1783
+
1784
+ | Strategy | Commands | Behavior |
1785
+ |----------|----------|----------|
1786
+ | inject/cap `COUNT` | `SCAN`, `HSCAN`, `SSCAN`, `ZSCAN` | adds `COUNT 1000` when absent; caps a larger `COUNT` to 1000 |
1787
+ | clamp `stop` | `LRANGE`, `ZRANGE`, `ZREVRANGE` | rewrites `stop` so the span ≤ 1000 (`-1` becomes `start+999`) |
1788
+ | inject/cap `LIMIT` | `ZRANGEBYSCORE` | appends `LIMIT 0 1000` when absent; caps a larger count |
1789
+ | client truncate | `HGETALL`, `HKEYS`, `HVALS`, `SMEMBERS`, `KEYS` | keeps the first 1000 entries |
1790
+
1791
+ 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.
1792
+
1793
+ ### Blacklist enforcement
1794
+
1795
+ Blacklist rules are enforced as **Redis-native key globs** (`*`, `?`, `[abc]`, `[a-z]`):
1796
+
1797
+ ```bash
1798
+ dbcli blacklist add 'secrets:*' # register a key-glob rule
1799
+ dbcli query "GET secrets:api_key" # → BlacklistRejection (exit non-zero)
1800
+ dbcli query "MGET safe:k secrets:api" # → rejected (any matching key fails the whole command)
1801
+ dbcli query "KEYS secrets:*" # → rejected (pattern overlaps a rule)
1802
+ dbcli query "KEYS *" # → returns only non-blacklisted keys
1803
+ ```
1804
+
1805
+ Rejections are written to the audit log with `success: false` and `metadata.rejection_reason: 'blacklist'` + `matched_pattern`.
1806
+
1807
+ ### Value / hash-field masking (v1.22)
1808
+
1809
+ Where the key-glob blacklist *rejects*, masking instead *redacts*: a matched read still
1810
+ runs, but the sensitive value comes back as `[REDACTED]` so an agent can use the command
1811
+ without ever seeing it. Add an optional `redis.mask` block to `.dbcli`:
1812
+
1813
+ ```yaml
1814
+ redis:
1815
+ mask:
1816
+ - keyPattern: 'session:*' # whole value redacted on read
1817
+ - keyPattern: 'user:*'
1818
+ fields: [password, token] # only these hash fields redacted
1819
+ ```
1820
+
1821
+ - Applies on reads: `GET`, `GETRANGE`, `HGETALL`, `HGET`, `HMGET`, `HVALS`.
1822
+ - A rule without `fields` redacts the entire value; with `fields` only the named hash fields are redacted.
1823
+ - Masking and key-glob rejection coexist, and **rejection always wins over masking** — a key that matches a blacklist rule is rejected, never merely masked.
1824
+
1825
+ ### Interactive shell
1826
+
1827
+ `dbcli shell` on a Redis connection opens a single-line REPL:
1828
+
1829
+ ```text
1830
+ $ dbcli --use local-redis shell
1831
+ Redis shell: single-line commands; SCAN/LRANGE auto-capped at 1000. Type `.no-limit on` to bypass (unsafe).
1832
+ redis> SCAN 0 # wire args become: SCAN 0 COUNT 1000 (REDIS_SIZE_REWRITE)
1833
+ redis> HGETALL bighash # >1000 fields → kept 1000 (REDIS_SIZE_TRUNCATE)
1834
+ redis> .no-limit on # bypass size guard for this session
1835
+ redis> GET secrets:api_key # → REDIS_BLACKLIST / BlacklistRejection if blacklisted
1836
+ redis> .exit
1837
+ ```
1838
+
1839
+ Tab completion offers Redis command names and known key prefixes; history persists to `~/.dbcli_history`.
1840
+
1841
+ ### Limitations
1842
+
1843
+ - No `--dry-run` for writes — Redis commands execute immediately. Pair writes with a confirming read (`GET`, `HGETALL`, `EXISTS`).
1844
+ - No transaction wrapping (`MULTI`/`EXEC`). Submit one command at a time.
1845
+ - `KEYS` requires `admin`. Prefer `SCAN` for routine work.
1846
+ - Blacklist enforcement covers **keys** (Redis-native globs); value / hash-field **masking** is available via the `redis.mask` config block (v1.22).
1847
+
1848
+ ## Elasticsearch Support
1849
+
1850
+ 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.
1851
+
1852
+ **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `export` (v1.22), `shell` (v1.22), `status`, `doctor`, `upgrade`, `completion`
1853
+
1854
+ **Saved queries:** `q` is supported for ES JSON DSL bodies (see "Elasticsearch snippets" below).
1855
+
1856
+ **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.
1857
+
1858
+ ### Connection and configuration
1859
+
1860
+ - Either `host` + `port` (default `https://localhost:9200`) or `nodes: [...]` (first node is used) or `cloudId`.
1861
+ - Auth precedence: `apiKey` → `user`/`password` (HTTP Basic). Leave both unset for an open cluster.
1862
+ - `protocol` defaults to `https`. For TLS quirks: `caPath` (path to a PEM bundle) and `rejectUnauthorized: false` (last resort).
1863
+ - `connection.timeout` (ms, default 5000) is wired to `AbortController` on every request.
1864
+
1865
+ ### Permission classification
1866
+
1867
+ Each REST request is mapped to a SQL-shaped tier based on method + path:
1868
+
1869
+ | ES surface | Mapped to | Permission |
1870
+ |------------|-----------|------------|
1871
+ | `GET _search` / `_count` / `_mapping` / `_settings` / `_alias` / `GET _doc` / `_source` | `SELECT` | `query-only` |
1872
+ | `POST _update` / `POST _doc` | `UPDATE` | `read-write` |
1873
+ | `PUT _doc` / `_create` | `INSERT` | `read-write` |
1874
+ | `DELETE` (any) | `DELETE` | `data-admin` |
1875
+ | `_bulk` | highest tier among the NDJSON actions (`delete` ⇒ `data-admin`) | derived |
1876
+ | Anything else | `DROP` | `admin` (deny by default) |
1877
+
1878
+ ### Schema inspection
1879
+
1880
+ `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.
1881
+
1882
+ `schema` (no argument) iterates all non-system indices through the standard full-scan code path and writes per-connection caches under `.dbcli/schemas/<connection>/`.
1883
+
1884
+ ### Query semantics
1885
+
1886
+ - `--collection <index>` (or `--index <index>`) is required.
1887
+ - Body that starts with `{` → sent as JSON DSL via `POST /<index>/_search`. Body otherwise → URL-encoded into `?q=...` (Lucene query string) on `GET`.
1888
+ - Hits are flattened: each row carries `_id` plus dotted-path fields lifted from `_source`. Use `--format json` to inspect raw nested structure.
1889
+ - 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.
1890
+
1891
+ ### Recommended `query` patterns
1892
+
1893
+ ```bash
1894
+ # DSL match
1895
+ dbcli query '{"query":{"match":{"status":"active"}}}' --collection orders --format json
1896
+
1897
+ # DSL with sort + size
1898
+ dbcli query '{"query":{"range":{"created_at":{"gte":"2026-01-01"}}},"sort":[{"created_at":"desc"}],"size":50}' \
1899
+ --collection orders
1900
+
1901
+ # Aggregation
1902
+ dbcli query '{"size":0,"aggs":{"by_status":{"terms":{"field":"status.keyword"}}}}' \
1903
+ --collection orders --format json
1904
+
1905
+ # Lucene query string
1906
+ dbcli query 'status:active AND amount:>100' --index orders --limit 100
1907
+ ```
1908
+
1909
+ ### Export (v1.22)
1910
+
1911
+ `dbcli export` supports two shapes on an ES connection:
1912
+
1913
+ ```bash
1914
+ # (a) search DSL + --index → export the hits
1915
+ dbcli export '{"query":{"match":{"status":"active"}}}' --index orders --format jsonl --output orders.ndjson
1916
+
1917
+ # (b) index name as the query → match_all over the whole index (scroll)
1918
+ dbcli export orders --format csv --output orders.csv
1919
+ dbcli export orders --no-limit --format jsonl # full index, scrolled in batches
1920
+ ```
1921
+
1922
+ - Outputs JSON / JSONL / CSV. Default cap is 1000 rows; `--no-limit` streams the full index via the scroll API in batches.
1923
+ - Index-level blacklist is checked before export and the run is written to the audit log.
1924
+
1925
+ ### Interactive shell (v1.22)
1926
+
1927
+ `dbcli shell` on an ES connection opens a Kibana Dev Tools-style REPL:
1928
+
1929
+ ```text
1930
+ $ dbcli --use local-es shell
1931
+ GET /orders/_search
1932
+ {
1933
+ "query": { "match": { "status": "active" } }
1934
+ }
1935
+ # ← blank line submits the whole block
1936
+ ```
1937
+
1938
+ - 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.
1939
+ - Read-focused: index-level blacklist rejects protected indices at the front end; a `_search` whose body omits `size` is auto-capped at 1000 hits.
1940
+
1941
+ ### Doctor and diagnostics
1942
+
1943
+ `dbcli doctor` runs a dedicated Elasticsearch path:
1944
+
1945
+ - Verifies REST connectivity to `GET /`.
1946
+ - Reads `version.number` and runs the standard version freshness check.
1947
+ - Walks every index via `listTables()` + `getTableSchema()` to feed the blacklist completeness check and the large-table heuristic (using `documentCount`).
1948
+ - Standard schema-cache freshness using `schemaLastUpdated`.
1949
+
1950
+ ### Limitations
1951
+
1952
+ - 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.
1953
+ - No `_search/scroll` or PIT pagination at the CLI layer; large pulls need a saved external script.
1954
+ - `check`, `diff`, `migrate`, and `q` are SQL-only and exit with errors (or fall through to a generic "unsupported" path).
1955
+ - Blacklist column rules are applied to flattened hit rows on `query`; table-level blacklist rejects an index up front.