@carllee1983/dbcli 1.58.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/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@carllee1983/dbcli",
6
- version: "1.58.0",
6
+ version: "2.0.0",
7
7
  description: "Database CLI for AI agents",
8
8
  type: "module",
9
9
  publishConfig: {
package/dist/core.mjs CHANGED
@@ -2191,6 +2191,23 @@ var ceremony_default = {
2191
2191
  confirm_destructive_warning_ddl: "\u26A0\uFE0F Warning: this schema change is destructive and cannot be undone!",
2192
2192
  confirm_ddl_prompt: "Are you sure you want to run this {operation}? This cannot be undone.",
2193
2193
  confirm_prompt: "Proceed with this operation?",
2194
+ gate_summary: "This will {operation} rows in {table}.",
2195
+ gate_summary_untargeted: "This will run a {operation} statement.",
2196
+ gate_statement: "Statement:",
2197
+ gate_cancelled: "Cancelled. Nothing was run.",
2198
+ gate_full_table_warning: "\u26A0\uFE0F This affects EVERY row in {table} and cannot be undone.",
2199
+ gate_ddl_warning: "\u26A0\uFE0F This destroys {table} and cannot be undone.",
2200
+ gate_ddl_warning_untargeted: "\u26A0\uFE0F This is a destructive schema change and cannot be undone.",
2201
+ gate_unparseable_warning: "\u26A0\uFE0F dbcli could not parse this statement, so it is treated as affecting everything.",
2202
+ gate_multiple_statements_warning: "\u26A0\uFE0F This is more than one statement, and dbcli will not judge them one at a time.",
2203
+ gate_typed_prompt: "Type {phrase} to run it",
2204
+ gate_typed_mismatch: "That did not match {phrase}. Nothing was run.",
2205
+ gate_refused: "Refused: this statement is not limited to specific rows and nobody is here to confirm it (reason={reason}).",
2206
+ gate_remedy_no_where: "Add a WHERE clause \u2014 WHERE 1=1 if you really do mean every row \u2014 or a LIMIT, then run it again.",
2207
+ gate_remedy_ddl_destruction: "No flag skips this. Run it from an interactive terminal, or apply the schema change through a reviewed migration.",
2208
+ gate_remedy_unparseable: "dbcli could not parse this statement and will not guess. Simplify it, or run it from an interactive terminal.",
2209
+ gate_remedy_multiple_statements: "Run one statement per command.",
2210
+ gate_remedy_non_unique_where: "Match on a primary key or a uniquely indexed column, or run it from an interactive terminal.",
2194
2211
  recovery_none: "This cannot be undone automatically \u2014 restore from a backup to get the previous values back.",
2195
2212
  recovery_retry: "Re-run with --recovery to write a recovery plan, then read it with: dbcli recover",
2196
2213
  blacklist_hint: "Run `dbcli blacklist list` to see what is protected"
@@ -2214,6 +2231,23 @@ var ceremony_default2 = {
2214
2231
  confirm_destructive_warning_ddl: "\u26A0\uFE0F \u8B66\u544A\uFF1A\u9019\u500B schema \u8B8A\u66F4\u5177\u7834\u58DE\u6027\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\uFF01",
2215
2232
  confirm_ddl_prompt: "\u78BA\u5B9A\u8981\u57F7\u884C\u9019\u500B {operation} \u55CE\uFF1F\u6B64\u64CD\u4F5C\u7121\u6CD5\u5FA9\u539F\u3002",
2216
2233
  confirm_prompt: "\u8981\u7E7C\u7E8C\u57F7\u884C\u9019\u500B\u64CD\u4F5C\u55CE\uFF1F",
2234
+ gate_summary: "\u9019\u53E5\u6703\u5C0D {table} \u57F7\u884C {operation}\u3002",
2235
+ gate_summary_untargeted: "\u9019\u53E5\u6703\u57F7\u884C {operation}\u3002",
2236
+ gate_statement: "\u8A9E\u53E5\uFF1A",
2237
+ gate_cancelled: "\u5DF2\u53D6\u6D88\uFF0C\u6C92\u6709\u57F7\u884C\u4EFB\u4F55\u8A9E\u53E5\u3002",
2238
+ gate_full_table_warning: "\u26A0\uFE0F \u9019\u53E5\u6703\u5F71\u97FF {table} \u7684\u6BCF\u4E00\u5217\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
2239
+ gate_ddl_warning: "\u26A0\uFE0F \u9019\u53E5\u6703\u6467\u6BC0 {table}\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
2240
+ gate_ddl_warning_untargeted: "\u26A0\uFE0F \u9019\u662F\u7834\u58DE\u6027\u7684 schema \u8B8A\u66F4\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
2241
+ gate_unparseable_warning: "\u26A0\uFE0F dbcli \u7121\u6CD5\u89E3\u6790\u9019\u53E5\uFF0C\u56E0\u6B64\u4E00\u5F8B\u8996\u70BA\u6703\u5F71\u97FF\u5168\u90E8\u8CC7\u6599\u3002",
2242
+ gate_multiple_statements_warning: "\u26A0\uFE0F \u9019\u88E1\u4E0D\u53EA\u4E00\u53E5\u8A9E\u53E5\uFF0Cdbcli \u4E0D\u6703\u9010\u53E5\u5224\u65B7\u3002",
2243
+ gate_typed_prompt: "\u8F38\u5165 {phrase} \u624D\u6703\u57F7\u884C",
2244
+ gate_typed_mismatch: "\u8F38\u5165\u7684\u5167\u5BB9\u8207 {phrase} \u4E0D\u7B26\uFF0C\u6C92\u6709\u57F7\u884C\u4EFB\u4F55\u8A9E\u53E5\u3002",
2245
+ gate_refused: "\u5DF2\u62D2\u7D55\uFF1A\u9019\u53E5\u6C92\u6709\u9650\u5B9A\u8981\u52D5\u54EA\u4E9B\u5217\uFF0C\u800C\u73FE\u5728\u6C92\u6709\u4EBA\u80FD\u78BA\u8A8D\uFF08reason={reason}\uFF09\u3002",
2246
+ gate_remedy_no_where: "\u88DC\u4E0A WHERE\uFF08\u771F\u7684\u8981\u5168\u8868\u5C31\u5BEB WHERE 1=1\uFF09\u6216 LIMIT\uFF0C\u7136\u5F8C\u91CD\u8DD1\u3002",
2247
+ gate_remedy_ddl_destruction: "\u6C92\u6709\u4EFB\u4F55\u65D7\u6A19\u53EF\u4EE5\u8DF3\u904E\u9019\u4E00\u95DC\u3002\u8ACB\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\uFF0C\u6216\u6539\u8D70\u7D93\u904E\u5BE9\u67E5\u7684 migration\u3002",
2248
+ gate_remedy_unparseable: "dbcli \u7121\u6CD5\u89E3\u6790\u9019\u53E5\uFF0C\u4E5F\u4E0D\u6703\u7528\u731C\u7684\u3002\u8ACB\u7C21\u5316\u8A9E\u53E5\uFF0C\u6216\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\u3002",
2249
+ gate_remedy_multiple_statements: "\u4E00\u6B21\u53EA\u57F7\u884C\u4E00\u53E5\u8A9E\u53E5\u3002",
2250
+ gate_remedy_non_unique_where: "\u6539\u7528\u4E3B\u9375\u6216\u552F\u4E00\u7D22\u5F15\u6B04\u4F4D\u7576\u689D\u4EF6\uFF0C\u6216\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\u3002",
2217
2251
  recovery_none: "\u6B64\u64CD\u4F5C\u7121\u6CD5\u81EA\u52D5\u9084\u539F\uFF0C\u8981\u53D6\u56DE\u5148\u524D\u7684\u8CC7\u6599\u8ACB\u5F9E\u5099\u4EFD\u56DE\u5FA9\u3002",
2218
2252
  recovery_retry: "\u52A0\u4E0A --recovery \u91CD\u8DD1\u53EF\u7522\u751F\u56DE\u5FA9\u8A08\u756B\uFF0C\u518D\u7528 dbcli recover \u8B80\u53D6",
2219
2253
  blacklist_hint: "\u57F7\u884C `dbcli blacklist list` \u67E5\u770B\u53D7\u4FDD\u8B77\u7684\u9805\u76EE"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.58.0",
3
+ "version": "2.0.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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.
@@ -334,9 +334,43 @@ 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
+
340
374
  > **Elasticsearch tiers by scope.** A read (`GET` / `HEAD`, plus `POST _search` and
341
375
  > `POST _count`) is query-only. Indexing or updating a document is read-write.
342
376
  > `DELETE /<index>/_doc/<id>` — one document — is data-admin. Everything that removes or
@@ -955,6 +989,14 @@ dbcli update users --where "id=1" --set '{"name":"Bob"}' --plan --format json
955
989
  > target primary keys first, then issue one `update` / `delete --where "id=<pk>"` per key.
956
990
  > (MongoDB `--where` accepts a full JSON filter and is exempt.)
957
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
+
958
1000
  ### delete
959
1001
 
960
1002
  Delete data from a table.
@@ -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.
@@ -334,9 +334,43 @@ 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
+
340
374
  > **Elasticsearch tiers by scope.** A read (`GET` / `HEAD`, plus `POST _search` and
341
375
  > `POST _count`) is query-only. Indexing or updating a document is read-write.
342
376
  > `DELETE /<index>/_doc/<id>` — one document — is data-admin. Everything that removes or
@@ -955,6 +989,14 @@ dbcli update users --where "id=1" --set '{"name":"Bob"}' --plan --format json
955
989
  > target primary keys first, then issue one `update` / `delete --where "id=<pk>"` per key.
956
990
  > (MongoDB `--where` accepts a full JSON filter and is exempt.)
957
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
+
958
1000
  ### delete
959
1001
 
960
1002
  Delete data from a table.