@carllee1983/dbcli 1.57.0 → 2.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.57.0",
3
+ "version": "2.0.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -74,12 +74,14 @@
74
74
  "test:unit": "bun test tests/unit tests/core",
75
75
  "test:integration": "bun test tests/integration",
76
76
  "test:gherkin": "bun test tests/gherkin",
77
- "test:docker": "docker compose -f docker-compose.test.yml up -d --wait && bun test tests/integration/adapters; docker compose -f docker-compose.test.yml down",
77
+ "test:docker": "docker compose -f docker-compose.test.yml up -d --wait && bun test tests/integration; docker compose -f docker-compose.test.yml down",
78
+ "services:check": "bun run scripts/check-test-services.ts",
78
79
  "docs:check": "bun run scripts/check-user-docs.ts",
79
80
  "contract:check": "bun run scripts/check-cli-contract.ts",
80
81
  "skill:check": "bun run scripts/check-skill-parity.ts",
81
82
  "platform:check": "bun run scripts/check-platform-parity.ts",
82
83
  "agent-core:check": "bun run scripts/check-agent-core-purity.ts",
84
+ "core-stdout:check": "bun run scripts/check-core-no-stdout.ts",
83
85
  "typecheck": "tsc --noEmit --pretty false",
84
86
  "test:perf": "bun test ./tests/perf/*.bench.ts",
85
87
  "lint": "eslint src tests scripts --ext .ts --max-warnings=0",
@@ -43,6 +43,18 @@ condition, first `query` / `export` the target rows' primary keys, then run one
43
43
  `update` / `delete --where "id=<pk>"` per key — or escalate to a human. (MongoDB `--where`
44
44
  takes a full JSON filter and is exempt.)
45
45
 
46
+ **Write gate (2.0.0) — the rule that will refuse you.** Every write is classified into two
47
+ tiers. Ordinary writes (`INSERT`, `UPDATE` / `DELETE` with a `WHERE`, `CREATE`, `ALTER`)
48
+ run unattended exactly as before; `--yes` skips the terminal prompt a human would see.
49
+ **Statements that are not limited to specific rows are refused outright when nobody can
50
+ answer a prompt** — `UPDATE` / `DELETE` with no `WHERE`, `DROP`, `TRUNCATE`, a statement
51
+ the SQL parser cannot read, several statements in one string, and `update` / `delete --where` matching on no primary key and
52
+ no unique index. The process exits `1` with `reason=no_where`, `reason=ddl_destruction`,
53
+ `reason=unparseable` or `reason=non_unique_where`, and **nothing reaches the database**.
54
+ **No flag bypasses this** — not `--yes`, not `--force`. To write every row on purpose, put
55
+ the intent in the SQL itself: add `WHERE 1=1` or a `LIMIT`. `DROP` / `TRUNCATE` have no
56
+ unattended route at all; escalate to a human.
57
+
46
58
  > `report` and `guide` already embed an `inspect` snapshot — you do **not** need to run
47
59
  > `dbcli inspect` first. Run `dbcli inspect --for-agent` manually only when you want the
48
60
  > audit-recent context or to diagnose a connection problem.
@@ -449,8 +461,13 @@ changing the default. `--recovery` is honoured by `query`, `q`, `insert`, `updat
449
461
  (no `>=`, `!=`, `LIKE`, `OR`). MongoDB `--where` takes a full JSON filter
450
462
  (`'{"status":"pending"}'`), falling back to `col=val` when it is not valid JSON.
451
463
  - `--dry-run` prints the parameterized SQL (with `$1` / `?` placeholders, not real values)
452
- and `rows_affected: 0`; proceed once `status:"success"` and the SQL shape matches the
453
- intended `--where` / `--set`. MongoDB prints a shell-style preview.
464
+ and `status:"dry_run"` with `rows_affected: 0` never `success`, which now means the
465
+ write really ran. Proceed once the SQL shape matches the intended `--where` / `--set`.
466
+ Declining at the confirmation prompt reports `status:"cancelled"`, also not `success`.
467
+ MongoDB prints a shell-style preview.
468
+ - `--force` skips the confirmation prompt. Every `insert` / `update` / `delete` asks
469
+ first — SQL, MongoDB and Redis alike — and a non-interactive run cannot answer, so an
470
+ unattended write without `--force` ends as `status:"cancelled"` having changed nothing.
454
471
  - `--recovery` is recommended for automated agent pipelines (enables `dbcli recover --apply`
455
472
  after a failure); optional for one-off manual writes.
456
473
 
@@ -334,9 +334,50 @@ dbcli query "SELECT day, dau FROM dau_daily" --ui # open in brows
334
334
  dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdout
335
335
  ```
336
336
 
337
- **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`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--recovery`
337
+ **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`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--yes`, `--recovery`
338
338
  **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
339
339
 
340
+ #### Write confirmation gate (2.0.0)
341
+
342
+ A SQL write passes through a two-tier gate before the connection is opened. The gate is
343
+ separate from the permission axis: permission says what the connection may do, the gate
344
+ says whether this particular statement may run right now.
345
+
346
+ | Tier | Statements | Interactive terminal | Non-interactive (or `--format json`) |
347
+ | :--- | :--- | :--- | :--- |
348
+ | One | `INSERT`, `UPDATE` / `DELETE` **with** a `WHERE` or `LIMIT`, `CREATE`, `ALTER` | Summary + `y/N`; `--yes` skips it | Runs, exactly as before |
349
+ | Two | `UPDATE` / `DELETE` with **no** `WHERE`, `DROP`, `TRUNCATE`, unparseable statements, several statements in one string | Type the target table name; **no flag skips it** | **Refused**: exit `1`, nothing sent to the database |
350
+
351
+ A refusal message names a machine-readable reason — `reason=no_where`,
352
+ `reason=ddl_destruction`, `reason=unparseable`, `reason=multiple_statements` — so a caller can tell it apart from a
353
+ connection failure or a permission denial.
354
+
355
+ **Escape routes.** For a statement that accepts a `WHERE`, put the intent in the SQL:
356
+
357
+ ```bash
358
+ dbcli query "UPDATE users SET banned = 1" # refused, reason=no_where
359
+ dbcli query "UPDATE users SET banned = 1 WHERE 1=1" # runs — intent is explicit
360
+ dbcli query "DELETE FROM sessions LIMIT 1000" # runs — damage is bounded
361
+ dbcli query "UPDATE users SET banned = 1 WHERE id = 3" --yes # tier one, question skipped
362
+ ```
363
+
364
+ This is deliberately not a flag. `WHERE 1=1` appended to a statement that already has a
365
+ `WHERE` is a syntax error, so a blanket "always add it" habit breaks on the first ordinary
366
+ statement; a flag would be harmless everywhere and therefore added everywhere. For `DROP`
367
+ and `TRUNCATE` there is no clause to add — the connection's `permission` level decides
368
+ whether they are possible at all, and the typed confirmation must come from a person.
369
+
370
+ Every tier-two evaluation is written to the audit log with
371
+ `metadata.write_gate_outcome` (`allowed` / `declined` / `refused`) and
372
+ `metadata.write_gate_reason`.
373
+
374
+ > **Elasticsearch tiers by scope.** A read (`GET` / `HEAD`, plus `POST _search` and
375
+ > `POST _count`) is query-only. Indexing or updating a document is read-write.
376
+ > `DELETE /<index>/_doc/<id>` — one document — is data-admin. Everything that removes or
377
+ > reshapes a container is **admin**: `DELETE /<index>`, a wildcard or `_all` delete,
378
+ > templates and aliases, and `PUT` against `_mapping` or `_settings`. A request whose
379
+ > scope cannot be established is treated as admin rather than guessed downward.
380
+
340
381
  > **Server-side scripts are rejected on every path.** MongoDB `$where`,
341
382
  > `$function`, and `$accumulator`, and Elasticsearch `script` / `script_fields`,
342
383
  > execute code on the database server. The adapters reject them anywhere in a
@@ -925,7 +966,7 @@ dbcli insert users --data '{"name":"Alice"}' --force
925
966
  dbcli insert users --data '{"name":"Alice"}' --plan --format json # risk analysis only; no DB connection
926
967
  ```
927
968
 
928
- **Options:** `--data <json>`, `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output), `--recovery`
969
+ **Options:** `--data <json>`, `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output; `json` also keeps the result envelope instead of prose in a terminal), `--recovery`
929
970
  **Permission:** read-write+
930
971
 
931
972
  ### update
@@ -938,7 +979,7 @@ dbcli update users --where "id=1" --set '{"name":"Bob"}' --dry-run
938
979
  dbcli update users --where "id=1" --set '{"name":"Bob"}' --plan --format json # risk analysis only; no DB connection
939
980
  ```
940
981
 
941
- **Options:** `--where <condition>` (required), `--set <json>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output), `--recovery`
982
+ **Options:** `--where <condition>` (required), `--set <json>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output; `json` also keeps the result envelope instead of prose in a terminal), `--recovery`
942
983
  **Permission:** read-write+
943
984
 
944
985
  > **`--where` grammar (SQL `update` / `delete`)** — equality only: `col=val` or
@@ -948,6 +989,14 @@ dbcli update users --where "id=1" --set '{"name":"Bob"}' --plan --format json
948
989
  > target primary keys first, then issue one `update` / `delete --where "id=<pk>"` per key.
949
990
  > (MongoDB `--where` accepts a full JSON filter and is exempt.)
950
991
 
992
+ > **Tier two for structured writes (2.0.0)** — a `--where` that matches on no primary key
993
+ > and no unique index selects an unknown number of rows, so it is treated the same as a
994
+ > raw statement with no `WHERE`: the target table name must be typed at an interactive
995
+ > terminal, and a non-interactive run is refused with exit `1` and
996
+ > `reason=non_unique_where`. `--force` and `--dry-run` do not affect this; `--force` skips
997
+ > the ordinary confirmation only. Select the primary keys first and write one row at a
998
+ > time, or run it where a person can confirm it.
999
+
951
1000
  ### delete
952
1001
 
953
1002
  Delete data from a table.
@@ -959,7 +1008,7 @@ dbcli delete users --where "id=1" --force
959
1008
  dbcli delete users --where "id=1" --plan --format json # risk analysis only; no DB connection
960
1009
  ```
961
1010
 
962
- **Options:** `--where <condition>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output), `--recovery`
1011
+ **Options:** `--where <condition>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output; `json` also keeps the result envelope instead of prose in a terminal), `--recovery`
963
1012
  **Permission:** data-admin+
964
1013
 
965
1014
  ### export
@@ -2540,6 +2589,8 @@ dbcli migrate add-enum status active inactive suspended
2540
2589
  dbcli migrate alter-enum status --add-value archived
2541
2590
  dbcli migrate drop-enum status --execute --force
2542
2591
  ```
2592
+ A destructive `migrate` action (`drop`, `drop-column`, `drop-index`, `drop-enum`) asks for confirmation on stderr before it runs, and reports `status: "cancelled"` if you decline — not `success`, which it used to claim with the cancellation buried in `warnings`. `--force` skips the question; a non-interactive run that omits it cannot answer and therefore cancels.
2593
+
2543
2594
 
2544
2595
  **Column spec format:** `name:type[:modifier[:modifier...]]`
2545
2596
  - Modifiers: `pk`, `not-null`, `unique`, `auto-increment`, `default=<value>`, `references=<table>.<column>`
@@ -43,6 +43,18 @@ condition, first `query` / `export` the target rows' primary keys, then run one
43
43
  `update` / `delete --where "id=<pk>"` per key — or escalate to a human. (MongoDB `--where`
44
44
  takes a full JSON filter and is exempt.)
45
45
 
46
+ **Write gate (2.0.0) — the rule that will refuse you.** Every write is classified into two
47
+ tiers. Ordinary writes (`INSERT`, `UPDATE` / `DELETE` with a `WHERE`, `CREATE`, `ALTER`)
48
+ run unattended exactly as before; `--yes` skips the terminal prompt a human would see.
49
+ **Statements that are not limited to specific rows are refused outright when nobody can
50
+ answer a prompt** — `UPDATE` / `DELETE` with no `WHERE`, `DROP`, `TRUNCATE`, a statement
51
+ the SQL parser cannot read, several statements in one string, and `update` / `delete --where` matching on no primary key and
52
+ no unique index. The process exits `1` with `reason=no_where`, `reason=ddl_destruction`,
53
+ `reason=unparseable` or `reason=non_unique_where`, and **nothing reaches the database**.
54
+ **No flag bypasses this** — not `--yes`, not `--force`. To write every row on purpose, put
55
+ the intent in the SQL itself: add `WHERE 1=1` or a `LIMIT`. `DROP` / `TRUNCATE` have no
56
+ unattended route at all; escalate to a human.
57
+
46
58
  > `report` and `guide` already embed an `inspect` snapshot — you do **not** need to run
47
59
  > `dbcli inspect` first. Run `dbcli inspect --for-agent` manually only when you want the
48
60
  > audit-recent context or to diagnose a connection problem.
@@ -449,8 +461,13 @@ changing the default. `--recovery` is honoured by `query`, `q`, `insert`, `updat
449
461
  (no `>=`, `!=`, `LIKE`, `OR`). MongoDB `--where` takes a full JSON filter
450
462
  (`'{"status":"pending"}'`), falling back to `col=val` when it is not valid JSON.
451
463
  - `--dry-run` prints the parameterized SQL (with `$1` / `?` placeholders, not real values)
452
- and `rows_affected: 0`; proceed once `status:"success"` and the SQL shape matches the
453
- intended `--where` / `--set`. MongoDB prints a shell-style preview.
464
+ and `status:"dry_run"` with `rows_affected: 0` never `success`, which now means the
465
+ write really ran. Proceed once the SQL shape matches the intended `--where` / `--set`.
466
+ Declining at the confirmation prompt reports `status:"cancelled"`, also not `success`.
467
+ MongoDB prints a shell-style preview.
468
+ - `--force` skips the confirmation prompt. Every `insert` / `update` / `delete` asks
469
+ first — SQL, MongoDB and Redis alike — and a non-interactive run cannot answer, so an
470
+ unattended write without `--force` ends as `status:"cancelled"` having changed nothing.
454
471
  - `--recovery` is recommended for automated agent pipelines (enables `dbcli recover --apply`
455
472
  after a failure); optional for one-off manual writes.
456
473
 
@@ -334,9 +334,50 @@ dbcli query "SELECT day, dau FROM dau_daily" --ui # open in brows
334
334
  dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdout
335
335
  ```
336
336
 
337
- **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`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--recovery`
337
+ **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`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--slow-ms <number>`, `--yes`, `--recovery`
338
338
  **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
339
339
 
340
+ #### Write confirmation gate (2.0.0)
341
+
342
+ A SQL write passes through a two-tier gate before the connection is opened. The gate is
343
+ separate from the permission axis: permission says what the connection may do, the gate
344
+ says whether this particular statement may run right now.
345
+
346
+ | Tier | Statements | Interactive terminal | Non-interactive (or `--format json`) |
347
+ | :--- | :--- | :--- | :--- |
348
+ | One | `INSERT`, `UPDATE` / `DELETE` **with** a `WHERE` or `LIMIT`, `CREATE`, `ALTER` | Summary + `y/N`; `--yes` skips it | Runs, exactly as before |
349
+ | Two | `UPDATE` / `DELETE` with **no** `WHERE`, `DROP`, `TRUNCATE`, unparseable statements, several statements in one string | Type the target table name; **no flag skips it** | **Refused**: exit `1`, nothing sent to the database |
350
+
351
+ A refusal message names a machine-readable reason — `reason=no_where`,
352
+ `reason=ddl_destruction`, `reason=unparseable`, `reason=multiple_statements` — so a caller can tell it apart from a
353
+ connection failure or a permission denial.
354
+
355
+ **Escape routes.** For a statement that accepts a `WHERE`, put the intent in the SQL:
356
+
357
+ ```bash
358
+ dbcli query "UPDATE users SET banned = 1" # refused, reason=no_where
359
+ dbcli query "UPDATE users SET banned = 1 WHERE 1=1" # runs — intent is explicit
360
+ dbcli query "DELETE FROM sessions LIMIT 1000" # runs — damage is bounded
361
+ dbcli query "UPDATE users SET banned = 1 WHERE id = 3" --yes # tier one, question skipped
362
+ ```
363
+
364
+ This is deliberately not a flag. `WHERE 1=1` appended to a statement that already has a
365
+ `WHERE` is a syntax error, so a blanket "always add it" habit breaks on the first ordinary
366
+ statement; a flag would be harmless everywhere and therefore added everywhere. For `DROP`
367
+ and `TRUNCATE` there is no clause to add — the connection's `permission` level decides
368
+ whether they are possible at all, and the typed confirmation must come from a person.
369
+
370
+ Every tier-two evaluation is written to the audit log with
371
+ `metadata.write_gate_outcome` (`allowed` / `declined` / `refused`) and
372
+ `metadata.write_gate_reason`.
373
+
374
+ > **Elasticsearch tiers by scope.** A read (`GET` / `HEAD`, plus `POST _search` and
375
+ > `POST _count`) is query-only. Indexing or updating a document is read-write.
376
+ > `DELETE /<index>/_doc/<id>` — one document — is data-admin. Everything that removes or
377
+ > reshapes a container is **admin**: `DELETE /<index>`, a wildcard or `_all` delete,
378
+ > templates and aliases, and `PUT` against `_mapping` or `_settings`. A request whose
379
+ > scope cannot be established is treated as admin rather than guessed downward.
380
+
340
381
  > **Server-side scripts are rejected on every path.** MongoDB `$where`,
341
382
  > `$function`, and `$accumulator`, and Elasticsearch `script` / `script_fields`,
342
383
  > execute code on the database server. The adapters reject them anywhere in a
@@ -925,7 +966,7 @@ dbcli insert users --data '{"name":"Alice"}' --force
925
966
  dbcli insert users --data '{"name":"Alice"}' --plan --format json # risk analysis only; no DB connection
926
967
  ```
927
968
 
928
- **Options:** `--data <json>`, `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output), `--recovery`
969
+ **Options:** `--data <json>`, `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output; `json` also keeps the result envelope instead of prose in a terminal), `--recovery`
929
970
  **Permission:** read-write+
930
971
 
931
972
  ### update
@@ -938,7 +979,7 @@ dbcli update users --where "id=1" --set '{"name":"Bob"}' --dry-run
938
979
  dbcli update users --where "id=1" --set '{"name":"Bob"}' --plan --format json # risk analysis only; no DB connection
939
980
  ```
940
981
 
941
- **Options:** `--where <condition>` (required), `--set <json>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output), `--recovery`
982
+ **Options:** `--where <condition>` (required), `--set <json>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output; `json` also keeps the result envelope instead of prose in a terminal), `--recovery`
942
983
  **Permission:** read-write+
943
984
 
944
985
  > **`--where` grammar (SQL `update` / `delete`)** — equality only: `col=val` or
@@ -948,6 +989,14 @@ dbcli update users --where "id=1" --set '{"name":"Bob"}' --plan --format json
948
989
  > target primary keys first, then issue one `update` / `delete --where "id=<pk>"` per key.
949
990
  > (MongoDB `--where` accepts a full JSON filter and is exempt.)
950
991
 
992
+ > **Tier two for structured writes (2.0.0)** — a `--where` that matches on no primary key
993
+ > and no unique index selects an unknown number of rows, so it is treated the same as a
994
+ > raw statement with no `WHERE`: the target table name must be typed at an interactive
995
+ > terminal, and a non-interactive run is refused with exit `1` and
996
+ > `reason=non_unique_where`. `--force` and `--dry-run` do not affect this; `--force` skips
997
+ > the ordinary confirmation only. Select the primary keys first and write one row at a
998
+ > time, or run it where a person can confirm it.
999
+
951
1000
  ### delete
952
1001
 
953
1002
  Delete data from a table.
@@ -959,7 +1008,7 @@ dbcli delete users --where "id=1" --force
959
1008
  dbcli delete users --where "id=1" --plan --format json # risk analysis only; no DB connection
960
1009
  ```
961
1010
 
962
- **Options:** `--where <condition>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output), `--recovery`
1011
+ **Options:** `--where <condition>` (required), `--dry-run`, `--force`, `--plan` (analyze risk without connecting or executing), `--format <text|json>` (`--plan` output; `json` also keeps the result envelope instead of prose in a terminal), `--recovery`
963
1012
  **Permission:** data-admin+
964
1013
 
965
1014
  ### export
@@ -2540,6 +2589,8 @@ dbcli migrate add-enum status active inactive suspended
2540
2589
  dbcli migrate alter-enum status --add-value archived
2541
2590
  dbcli migrate drop-enum status --execute --force
2542
2591
  ```
2592
+ A destructive `migrate` action (`drop`, `drop-column`, `drop-index`, `drop-enum`) asks for confirmation on stderr before it runs, and reports `status: "cancelled"` if you decline — not `success`, which it used to claim with the cancellation buried in `warnings`. `--force` skips the question; a non-interactive run that omits it cannot answer and therefore cancels.
2593
+
2543
2594
 
2544
2595
  **Column spec format:** `name:type[:modifier[:modifier...]]`
2545
2596
  - Modifiers: `pk`, `not-null`, `unique`, `auto-increment`, `default=<value>`, `references=<table>.<column>`