@carllee1983/dbcli 1.39.2 → 1.42.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/dbcli.mdc +21 -12
- package/.cursor/skills/dbcli/reference.md +329 -3
- package/.cursor-plugin/plugin.json +2 -1
- package/.github/skills/dbcli/SKILL.md +21 -12
- package/.github/skills/dbcli/reference.md +329 -3
- package/CHANGELOG.md +56 -0
- package/README.zh-TW.md +1 -1
- package/assets/SKILL.md +21 -12
- package/assets/SKILL.zh-TW.md +12 -7
- package/assets/reference.md +329 -3
- package/assets/tasks/diagnose-slow-query.md +8 -0
- package/assets/tasks/mongo-safe-backfill.md +56 -0
- package/assets/tasks/mongo-schema-drift-review.md +51 -0
- package/assets/tasks/orm-drift-review.md +55 -0
- package/dist/cli.mjs +9247 -4608
- package/dist/core.d.ts +3 -0
- package/dist/core.mjs +215 -59
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/INSTALL.md +19 -10
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +21 -12
- package/plugins/dbcli-agent/skills/dbcli/reference.md +329 -3
- package/skills/dbcli/SKILL.md +21 -12
- package/skills/dbcli/reference.md +329 -3
package/dist/core.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface ColumnSchema {
|
|
|
86
86
|
export interface TableSchema {
|
|
87
87
|
/** Table name */
|
|
88
88
|
name: string;
|
|
89
|
+
/** Exact database schema/catalog namespace, when reliably available */
|
|
90
|
+
schema?: string;
|
|
89
91
|
/** Array of columns in the table */
|
|
90
92
|
columns: ColumnSchema[];
|
|
91
93
|
/** Approximate row count (if available) */
|
|
@@ -98,6 +100,7 @@ export interface TableSchema {
|
|
|
98
100
|
foreignKeys?: Array<{
|
|
99
101
|
name: string;
|
|
100
102
|
columns: string[];
|
|
103
|
+
refSchema?: string;
|
|
101
104
|
refTable: string;
|
|
102
105
|
refColumns: string[];
|
|
103
106
|
}>;
|
package/dist/core.mjs
CHANGED
|
@@ -13434,6 +13434,7 @@ var COMMAND_CAPABILITY_KEYS = Object.freeze([
|
|
|
13434
13434
|
"schemaSingle",
|
|
13435
13435
|
"schemaFullScan",
|
|
13436
13436
|
"query",
|
|
13437
|
+
"lint",
|
|
13437
13438
|
"queryOutput",
|
|
13438
13439
|
"queryLimitGuard",
|
|
13439
13440
|
"q",
|
|
@@ -13482,6 +13483,7 @@ var SQL_BASE = {
|
|
|
13482
13483
|
schemaSingle: cap("supported", "readonly", "Reads a single table schema."),
|
|
13483
13484
|
schemaFullScan: cap("supported", "readonly", "Full scan, refresh, and reset are supported."),
|
|
13484
13485
|
query: cap("supported", "readonly", "Runs SQL through permission and blacklist guards."),
|
|
13486
|
+
lint: cap("supported", "readonly", "Statically analyzes SQL without connecting to the database."),
|
|
13485
13487
|
queryOutput: cap("supported", "readonly", "table/json/csv output is supported."),
|
|
13486
13488
|
queryLimitGuard: cap("supported", "readonly", "Query-only auto-limit and size guard are supported."),
|
|
13487
13489
|
q: cap("supported", "readonly", "Saved SQL snippets allow SELECT/WITH only."),
|
|
@@ -13511,6 +13513,7 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
13511
13513
|
mariadb: Object.freeze(SQL_BASE),
|
|
13512
13514
|
mongodb: Object.freeze({
|
|
13513
13515
|
...SQL_BASE,
|
|
13516
|
+
lint: cap("unsupported", "none", "Static lint accepts SQL connections only."),
|
|
13514
13517
|
schemaSingle: cap("limited", "readonly", "MongoDB schema is sampled from documents."),
|
|
13515
13518
|
schemaFullScan: cap("limited", "readonly", "Full scan is sampled and document-oriented."),
|
|
13516
13519
|
query: cap("limited", "readonly", "Uses JSON filter or aggregation syntax, not SQL."),
|
|
@@ -13529,6 +13532,7 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
13529
13532
|
}),
|
|
13530
13533
|
redis: Object.freeze({
|
|
13531
13534
|
...SQL_BASE,
|
|
13535
|
+
lint: cap("unsupported", "none", "Static lint accepts SQL connections only."),
|
|
13532
13536
|
schemaSingle: cap("limited", "readonly", "Per-key synthetic schema only."),
|
|
13533
13537
|
schemaFullScan: cap("unsupported", "none", "Redis has no full schema cache scan."),
|
|
13534
13538
|
query: cap("limited", "readonly", "Runs allow-listed Redis commands."),
|
|
@@ -13547,6 +13551,7 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
13547
13551
|
}),
|
|
13548
13552
|
elasticsearch: Object.freeze({
|
|
13549
13553
|
...SQL_BASE,
|
|
13554
|
+
lint: cap("unsupported", "none", "Static lint accepts SQL connections only."),
|
|
13550
13555
|
schemaSingle: cap("limited", "readonly", "Schema flattens index mappings."),
|
|
13551
13556
|
schemaFullScan: cap("supported", "readonly", "Full scan iterates non-system indices."),
|
|
13552
13557
|
query: cap("limited", "readonly", "Uses JSON DSL or Lucene query strings with an index."),
|
|
@@ -14437,6 +14442,10 @@ function fixDoubleEncodedUtf8(str) {
|
|
|
14437
14442
|
}
|
|
14438
14443
|
|
|
14439
14444
|
// src/adapters/postgresql-adapter.ts
|
|
14445
|
+
function quoteIdentifier(identifier) {
|
|
14446
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
14447
|
+
}
|
|
14448
|
+
|
|
14440
14449
|
class PostgreSQLAdapter {
|
|
14441
14450
|
pool = null;
|
|
14442
14451
|
client = null;
|
|
@@ -14522,6 +14531,7 @@ class PostgreSQLAdapter {
|
|
|
14522
14531
|
try {
|
|
14523
14532
|
const query = `
|
|
14524
14533
|
SELECT
|
|
14534
|
+
n.nspname as schema_name,
|
|
14525
14535
|
c.relname as table_name,
|
|
14526
14536
|
c.reltuples::bigint as estimated_rows,
|
|
14527
14537
|
CASE c.relkind
|
|
@@ -14541,6 +14551,7 @@ class PostgreSQLAdapter {
|
|
|
14541
14551
|
const result = await this.execute(query);
|
|
14542
14552
|
return result.rows.map((row) => ({
|
|
14543
14553
|
name: row.table_name,
|
|
14554
|
+
schema: row.schema_name,
|
|
14544
14555
|
columns: [],
|
|
14545
14556
|
columnCount: row.column_count,
|
|
14546
14557
|
rowCount: Math.max(0, row.estimated_rows || 0),
|
|
@@ -14569,9 +14580,15 @@ class PostgreSQLAdapter {
|
|
|
14569
14580
|
SELECT EXISTS(
|
|
14570
14581
|
SELECT 1 FROM information_schema.table_constraints tc
|
|
14571
14582
|
JOIN information_schema.key_column_usage kcu
|
|
14572
|
-
ON tc.
|
|
14573
|
-
|
|
14574
|
-
AND tc.
|
|
14583
|
+
ON tc.constraint_catalog = kcu.constraint_catalog
|
|
14584
|
+
AND tc.constraint_schema = kcu.constraint_schema
|
|
14585
|
+
AND tc.constraint_name = kcu.constraint_name
|
|
14586
|
+
WHERE tc.table_catalog = c.table_catalog
|
|
14587
|
+
AND tc.table_schema = c.table_schema
|
|
14588
|
+
AND tc.table_name = c.table_name
|
|
14589
|
+
AND kcu.table_catalog = c.table_catalog
|
|
14590
|
+
AND kcu.table_schema = c.table_schema
|
|
14591
|
+
AND kcu.table_name = c.table_name
|
|
14575
14592
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
14576
14593
|
AND kcu.column_name = c.column_name
|
|
14577
14594
|
)
|
|
@@ -14594,8 +14611,12 @@ class PostgreSQLAdapter {
|
|
|
14594
14611
|
c.column_name as name,
|
|
14595
14612
|
array_agg(e.enumlabel ORDER BY e.enumsortorder) as enum_values
|
|
14596
14613
|
FROM information_schema.columns c
|
|
14597
|
-
JOIN pg_type
|
|
14598
|
-
|
|
14614
|
+
JOIN pg_catalog.pg_type AS enum_type
|
|
14615
|
+
ON enum_type.typname = c.udt_name
|
|
14616
|
+
JOIN pg_catalog.pg_namespace AS enum_schema
|
|
14617
|
+
ON enum_schema.oid = enum_type.typnamespace
|
|
14618
|
+
AND enum_schema.nspname = c.udt_schema
|
|
14619
|
+
JOIN pg_catalog.pg_enum AS e ON e.enumtypid = enum_type.oid
|
|
14599
14620
|
WHERE c.table_name = $1
|
|
14600
14621
|
AND c.table_schema = 'public'
|
|
14601
14622
|
GROUP BY c.column_name
|
|
@@ -14608,17 +14629,40 @@ class PostgreSQLAdapter {
|
|
|
14608
14629
|
}
|
|
14609
14630
|
const fkQuery = `
|
|
14610
14631
|
SELECT
|
|
14611
|
-
|
|
14612
|
-
array_agg(
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14632
|
+
constraint_info.conname as name,
|
|
14633
|
+
array_agg(source_column.attname ORDER BY source_key.ordinality) as columns,
|
|
14634
|
+
referenced_schema.nspname as ref_schema,
|
|
14635
|
+
referenced_table.relname as ref_table,
|
|
14636
|
+
array_agg(referenced_column.attname ORDER BY source_key.ordinality) as ref_columns
|
|
14637
|
+
FROM pg_catalog.pg_constraint AS constraint_info
|
|
14638
|
+
JOIN pg_catalog.pg_class AS source_table
|
|
14639
|
+
ON source_table.oid = constraint_info.conrelid
|
|
14640
|
+
JOIN pg_catalog.pg_namespace AS source_schema
|
|
14641
|
+
ON source_schema.oid = source_table.relnamespace
|
|
14642
|
+
JOIN pg_catalog.pg_class AS referenced_table
|
|
14643
|
+
ON referenced_table.oid = constraint_info.confrelid
|
|
14644
|
+
JOIN pg_catalog.pg_namespace AS referenced_schema
|
|
14645
|
+
ON referenced_schema.oid = referenced_table.relnamespace
|
|
14646
|
+
JOIN LATERAL unnest(constraint_info.conkey) WITH ORDINALITY
|
|
14647
|
+
AS source_key(attnum, ordinality) ON TRUE
|
|
14648
|
+
JOIN LATERAL unnest(constraint_info.confkey) WITH ORDINALITY
|
|
14649
|
+
AS referenced_key(attnum, ordinality)
|
|
14650
|
+
ON referenced_key.ordinality = source_key.ordinality
|
|
14651
|
+
JOIN pg_catalog.pg_attribute AS source_column
|
|
14652
|
+
ON source_column.attrelid = constraint_info.conrelid
|
|
14653
|
+
AND source_column.attnum = source_key.attnum
|
|
14654
|
+
JOIN pg_catalog.pg_attribute AS referenced_column
|
|
14655
|
+
ON referenced_column.attrelid = constraint_info.confrelid
|
|
14656
|
+
AND referenced_column.attnum = referenced_key.attnum
|
|
14657
|
+
WHERE constraint_info.contype = 'f'
|
|
14658
|
+
AND source_table.relname = $1
|
|
14659
|
+
AND source_schema.nspname = 'public'
|
|
14660
|
+
GROUP BY
|
|
14661
|
+
constraint_info.oid,
|
|
14662
|
+
constraint_info.conname,
|
|
14663
|
+
referenced_schema.nspname,
|
|
14664
|
+
referenced_table.relname
|
|
14665
|
+
ORDER BY constraint_info.oid
|
|
14622
14666
|
`;
|
|
14623
14667
|
const fkResult = await this.execute(fkQuery, [tableName]);
|
|
14624
14668
|
const fkResults = fkResult.rows;
|
|
@@ -14646,36 +14690,49 @@ class PostgreSQLAdapter {
|
|
|
14646
14690
|
const indexResult = await this.execute(indexQuery, [tableName]);
|
|
14647
14691
|
const indexResults = indexResult.rows;
|
|
14648
14692
|
const estimateQuery = `
|
|
14649
|
-
SELECT reltuples::bigint as estimated_rows
|
|
14650
|
-
FROM pg_class
|
|
14651
|
-
|
|
14693
|
+
SELECT source_table.reltuples::bigint as estimated_rows
|
|
14694
|
+
FROM pg_catalog.pg_class AS source_table
|
|
14695
|
+
JOIN pg_catalog.pg_namespace AS source_schema
|
|
14696
|
+
ON source_schema.oid = source_table.relnamespace
|
|
14697
|
+
WHERE source_table.relname = $1
|
|
14698
|
+
AND source_schema.nspname = 'public'
|
|
14652
14699
|
`;
|
|
14653
14700
|
const estimateResult = await this.execute(estimateQuery, [
|
|
14654
14701
|
tableName
|
|
14655
14702
|
]);
|
|
14656
14703
|
const estimateResults = estimateResult.rows;
|
|
14657
14704
|
const pkQuery = `
|
|
14658
|
-
SELECT
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
|
|
14664
|
-
|
|
14665
|
-
)
|
|
14705
|
+
SELECT
|
|
14706
|
+
array_agg(primary_key_column.attname ORDER BY primary_key.ordinality) as columns
|
|
14707
|
+
FROM pg_catalog.pg_index AS index_info
|
|
14708
|
+
JOIN pg_catalog.pg_class AS source_table
|
|
14709
|
+
ON source_table.oid = index_info.indrelid
|
|
14710
|
+
JOIN pg_catalog.pg_namespace AS source_schema
|
|
14711
|
+
ON source_schema.oid = source_table.relnamespace
|
|
14712
|
+
JOIN LATERAL unnest(index_info.indkey) WITH ORDINALITY
|
|
14713
|
+
AS primary_key(attnum, ordinality) ON TRUE
|
|
14714
|
+
JOIN pg_catalog.pg_attribute AS primary_key_column
|
|
14715
|
+
ON primary_key_column.attrelid = source_table.oid
|
|
14716
|
+
AND primary_key_column.attnum = primary_key.attnum
|
|
14717
|
+
WHERE index_info.indisprimary
|
|
14718
|
+
AND source_table.relname = $1
|
|
14719
|
+
AND source_schema.nspname = 'public'
|
|
14666
14720
|
`;
|
|
14667
14721
|
const pkResult = await this.execute(pkQuery, [tableName]);
|
|
14668
14722
|
const pkResults = pkResult.rows;
|
|
14669
|
-
const
|
|
14723
|
+
const qualifiedTableName = `${quoteIdentifier("public")}.${quoteIdentifier(tableName)}`;
|
|
14724
|
+
const countResult = await this.execute(`SELECT COUNT(*) as count FROM ${qualifiedTableName}`);
|
|
14670
14725
|
const primaryKeyArray = Array.isArray(pkResults[0]?.columns) ? pkResults[0].columns : [];
|
|
14671
14726
|
const safeForeignKeys = fkResults.map((fk) => ({
|
|
14672
14727
|
name: fk.name,
|
|
14673
14728
|
columns: Array.isArray(fk.columns) ? fk.columns : [],
|
|
14729
|
+
refSchema: fk.ref_schema,
|
|
14674
14730
|
refTable: fk.ref_table,
|
|
14675
14731
|
refColumns: Array.isArray(fk.ref_columns) ? fk.ref_columns : []
|
|
14676
14732
|
}));
|
|
14677
14733
|
const schema = {
|
|
14678
14734
|
name: tableName,
|
|
14735
|
+
schema: "public",
|
|
14679
14736
|
columns: columns.map((col) => ({
|
|
14680
14737
|
name: col.name,
|
|
14681
14738
|
type: col.type,
|
|
@@ -21757,7 +21814,7 @@ function parseEnvPassword(content) {
|
|
|
21757
21814
|
return match?.[1] != null ? match[1].trim() : null;
|
|
21758
21815
|
}
|
|
21759
21816
|
var configModule = {
|
|
21760
|
-
async read(path, connectionName) {
|
|
21817
|
+
async read(path, connectionName, options = {}) {
|
|
21761
21818
|
const effectiveConnectionName = connectionName ?? _globalConnectionName;
|
|
21762
21819
|
try {
|
|
21763
21820
|
const binding = await readProjectBinding(path);
|
|
@@ -21801,23 +21858,27 @@ var configModule = {
|
|
|
21801
21858
|
resolvedConnection.password = legacyPassword;
|
|
21802
21859
|
}
|
|
21803
21860
|
let schema = (v2Config.schemas ?? {})[resolved.name] ?? v2Config.schema;
|
|
21804
|
-
|
|
21805
|
-
|
|
21806
|
-
|
|
21807
|
-
|
|
21808
|
-
|
|
21809
|
-
|
|
21810
|
-
|
|
21811
|
-
|
|
21812
|
-
|
|
21813
|
-
|
|
21814
|
-
|
|
21815
|
-
|
|
21816
|
-
|
|
21861
|
+
if (options.loadLayeredSchema !== false) {
|
|
21862
|
+
try {
|
|
21863
|
+
const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
|
|
21864
|
+
const loader = new SchemaLayeredLoader2(storagePath, {
|
|
21865
|
+
connectionName: resolved.name
|
|
21866
|
+
});
|
|
21867
|
+
const { cache, index } = await loader.initialize();
|
|
21868
|
+
if (index && Object.keys(index.tables).length > 0) {
|
|
21869
|
+
const layeredSchema = {};
|
|
21870
|
+
for (const tableName of Object.keys(index.tables)) {
|
|
21871
|
+
const s = await cache.getTableSchema(tableName);
|
|
21872
|
+
if (s)
|
|
21873
|
+
layeredSchema[tableName] = s;
|
|
21874
|
+
}
|
|
21875
|
+
if (Object.keys(layeredSchema).length > 0) {
|
|
21876
|
+
schema = layeredSchema;
|
|
21877
|
+
}
|
|
21817
21878
|
}
|
|
21879
|
+
} catch {
|
|
21880
|
+
console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
|
|
21818
21881
|
}
|
|
21819
|
-
} catch {
|
|
21820
|
-
console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
|
|
21821
21882
|
}
|
|
21822
21883
|
return DbcliConfigSchema.parse({
|
|
21823
21884
|
connection: resolvedConnection,
|
|
@@ -21943,7 +22004,7 @@ DBCLI_PASSWORD=${password}
|
|
|
21943
22004
|
};
|
|
21944
22005
|
|
|
21945
22006
|
// src/utils/redaction.ts
|
|
21946
|
-
var SQL_SUBCOMMANDS = new Set(["query", "export"]);
|
|
22007
|
+
var SQL_SUBCOMMANDS = new Set(["query", "export", "lint"]);
|
|
21947
22008
|
var REDACTED_VALUE_FLAGS = new Set([
|
|
21948
22009
|
"--where",
|
|
21949
22010
|
"--set",
|
|
@@ -21954,21 +22015,101 @@ var REDACTED_VALUE_FLAGS = new Set([
|
|
|
21954
22015
|
"--use",
|
|
21955
22016
|
"--password",
|
|
21956
22017
|
"--token",
|
|
21957
|
-
"--secret"
|
|
22018
|
+
"--secret",
|
|
22019
|
+
"--bulk"
|
|
22020
|
+
]);
|
|
22021
|
+
var KEEP_VALUE_FLAGS = new Set([
|
|
22022
|
+
"--format",
|
|
22023
|
+
"--conn-name",
|
|
22024
|
+
"--min-severity",
|
|
22025
|
+
"--output",
|
|
22026
|
+
"--limit",
|
|
22027
|
+
"--collection",
|
|
22028
|
+
"--index"
|
|
21958
22029
|
]);
|
|
21959
|
-
var
|
|
22030
|
+
var LINT_BOOLEAN_FLAGS = new Set(["--no-schema", "--recovery"]);
|
|
22031
|
+
function optionParts(token) {
|
|
22032
|
+
const equals = token.indexOf("=");
|
|
22033
|
+
return equals === -1 ? { name: token, inlineValue: undefined } : { name: token.slice(0, equals), inlineValue: token.slice(equals + 1) };
|
|
22034
|
+
}
|
|
22035
|
+
function findSensitiveSubcommand(argv) {
|
|
22036
|
+
for (let index = 1;index < argv.length; index++) {
|
|
22037
|
+
const token = argv[index];
|
|
22038
|
+
if (token.startsWith("--")) {
|
|
22039
|
+
const { name, inlineValue } = optionParts(token);
|
|
22040
|
+
if (inlineValue === undefined && (REDACTED_VALUE_FLAGS.has(name) || KEEP_VALUE_FLAGS.has(name))) {
|
|
22041
|
+
index++;
|
|
22042
|
+
}
|
|
22043
|
+
continue;
|
|
22044
|
+
}
|
|
22045
|
+
if (SQL_SUBCOMMANDS.has(token))
|
|
22046
|
+
return { index, command: token };
|
|
22047
|
+
}
|
|
22048
|
+
return null;
|
|
22049
|
+
}
|
|
22050
|
+
function sensitiveArgvValues(argv) {
|
|
22051
|
+
const sensitiveCommand = findSensitiveSubcommand(argv);
|
|
22052
|
+
const values = new Set;
|
|
22053
|
+
let capturedSingleSql = false;
|
|
22054
|
+
let afterEndOfOptions = false;
|
|
22055
|
+
for (let index = 0;index < argv.length; index++) {
|
|
22056
|
+
const token = argv[index];
|
|
22057
|
+
if (!afterEndOfOptions && token === "--") {
|
|
22058
|
+
afterEndOfOptions = true;
|
|
22059
|
+
continue;
|
|
22060
|
+
}
|
|
22061
|
+
if (!afterEndOfOptions && token.startsWith("--")) {
|
|
22062
|
+
const { name, inlineValue } = optionParts(token);
|
|
22063
|
+
if (REDACTED_VALUE_FLAGS.has(name)) {
|
|
22064
|
+
const value = inlineValue ?? argv[index + 1];
|
|
22065
|
+
if (value) {
|
|
22066
|
+
values.add(value);
|
|
22067
|
+
if (name === "--bulk") {
|
|
22068
|
+
for (const item of value.split(",")) {
|
|
22069
|
+
values.add(item);
|
|
22070
|
+
if (item.startsWith("@") && item.length > 1) {
|
|
22071
|
+
values.add(item.slice(1));
|
|
22072
|
+
}
|
|
22073
|
+
}
|
|
22074
|
+
}
|
|
22075
|
+
}
|
|
22076
|
+
if (inlineValue === undefined)
|
|
22077
|
+
index++;
|
|
22078
|
+
} else if (inlineValue === undefined && KEEP_VALUE_FLAGS.has(name)) {
|
|
22079
|
+
index++;
|
|
22080
|
+
} else if (sensitiveCommand?.command === "lint" && LINT_BOOLEAN_FLAGS.has(name)) {
|
|
22081
|
+
continue;
|
|
22082
|
+
} else if (!sensitiveCommand || index < sensitiveCommand.index) {
|
|
22083
|
+
continue;
|
|
22084
|
+
} else {
|
|
22085
|
+
values.add(token);
|
|
22086
|
+
capturedSingleSql = true;
|
|
22087
|
+
}
|
|
22088
|
+
continue;
|
|
22089
|
+
}
|
|
22090
|
+
if (sensitiveCommand && index > sensitiveCommand.index && (sensitiveCommand.command === "lint" || !capturedSingleSql)) {
|
|
22091
|
+
values.add(token);
|
|
22092
|
+
capturedSingleSql = true;
|
|
22093
|
+
}
|
|
22094
|
+
}
|
|
22095
|
+
return Array.from(values).sort((left, right) => right.length - left.length);
|
|
22096
|
+
}
|
|
21960
22097
|
function redactArgv(argv) {
|
|
21961
22098
|
if (argv.length === 0)
|
|
21962
22099
|
return "<unknown>";
|
|
21963
|
-
const
|
|
21964
|
-
|
|
21965
|
-
|
|
21966
|
-
|
|
21967
|
-
|
|
21968
|
-
for (let i = 2;i < argv.length; i++) {
|
|
22100
|
+
const sensitiveCommand = findSensitiveSubcommand(argv);
|
|
22101
|
+
const out = [];
|
|
22102
|
+
let redactedSingleSql = false;
|
|
22103
|
+
let afterEndOfOptions = false;
|
|
22104
|
+
for (let i = 0;i < argv.length; i++) {
|
|
21969
22105
|
const tok = argv[i];
|
|
21970
|
-
if (tok
|
|
21971
|
-
|
|
22106
|
+
if (!afterEndOfOptions && tok === "--") {
|
|
22107
|
+
out.push(tok);
|
|
22108
|
+
afterEndOfOptions = true;
|
|
22109
|
+
continue;
|
|
22110
|
+
}
|
|
22111
|
+
if (!afterEndOfOptions && tok.startsWith("--")) {
|
|
22112
|
+
const { name, inlineValue } = optionParts(tok);
|
|
21972
22113
|
if (REDACTED_VALUE_FLAGS.has(name)) {
|
|
21973
22114
|
out.push(`${name} <redacted>`);
|
|
21974
22115
|
if (inlineValue === undefined)
|
|
@@ -21980,23 +22121,37 @@ function redactArgv(argv) {
|
|
|
21980
22121
|
out.push(tok);
|
|
21981
22122
|
} else {
|
|
21982
22123
|
out.push(name);
|
|
21983
|
-
if (i + 1 < argv.length
|
|
22124
|
+
if (i + 1 < argv.length) {
|
|
21984
22125
|
out.push(argv[++i]);
|
|
21985
22126
|
}
|
|
21986
22127
|
}
|
|
21987
22128
|
continue;
|
|
21988
22129
|
}
|
|
21989
|
-
|
|
21990
|
-
|
|
22130
|
+
if (sensitiveCommand?.command === "lint" && LINT_BOOLEAN_FLAGS.has(name)) {
|
|
22131
|
+
out.push(tok);
|
|
22132
|
+
continue;
|
|
22133
|
+
}
|
|
22134
|
+
if (!sensitiveCommand || i < sensitiveCommand.index) {
|
|
22135
|
+
out.push(tok);
|
|
22136
|
+
continue;
|
|
22137
|
+
}
|
|
21991
22138
|
}
|
|
21992
|
-
if (i
|
|
22139
|
+
if (sensitiveCommand && i > sensitiveCommand.index && (sensitiveCommand.command === "lint" || !redactedSingleSql)) {
|
|
21993
22140
|
out.push("<sql>");
|
|
22141
|
+
redactedSingleSql = true;
|
|
21994
22142
|
continue;
|
|
21995
22143
|
}
|
|
21996
22144
|
out.push(tok);
|
|
21997
22145
|
}
|
|
21998
22146
|
return out.join(" ");
|
|
21999
22147
|
}
|
|
22148
|
+
function redactArgvSensitiveText(text, argv) {
|
|
22149
|
+
let redacted = text;
|
|
22150
|
+
for (const value of sensitiveArgvValues(argv)) {
|
|
22151
|
+
redacted = redacted.split(value).join("<redacted>");
|
|
22152
|
+
}
|
|
22153
|
+
return redacted;
|
|
22154
|
+
}
|
|
22000
22155
|
function redactSensitive(text) {
|
|
22001
22156
|
return text.replace(/\b(password|token|apiKey|secret|key|token|auth|credential|pass|pwd|sid)([:=]|\s+)([^\s"';,]+)/gi, "$1$2<redacted>");
|
|
22002
22157
|
}
|
|
@@ -22042,6 +22197,7 @@ async function writeAuditEntry(config, commandName, options, outcome) {
|
|
|
22042
22197
|
if (outcome.error) {
|
|
22043
22198
|
errorMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
22044
22199
|
errorMessage = redactSensitive(errorMessage);
|
|
22200
|
+
errorMessage = redactArgvSensitiveText(errorMessage, process.argv);
|
|
22045
22201
|
}
|
|
22046
22202
|
const entry = {
|
|
22047
22203
|
engine,
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -78,8 +78,13 @@ used by Gemini/Antigravity-style agents.
|
|
|
78
78
|
|
|
79
79
|
## Cursor
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
> **Current status: manual install.** `dbcli-agent` is not yet indexed in
|
|
82
|
+
> Cursor's plugin marketplace, so `/add-plugin dbcli-agent` in Cursor Agent chat
|
|
83
|
+
> will not find it. Use one of the manual fallbacks below. The `/add-plugin`
|
|
84
|
+
> command only works once the plugin has been indexed (see the submission steps
|
|
85
|
+
> that follow).
|
|
86
|
+
|
|
87
|
+
Once the plugin is indexed, Cursor can install it from Cursor Agent chat:
|
|
83
88
|
|
|
84
89
|
```text
|
|
85
90
|
/add-plugin dbcli-agent
|
|
@@ -93,18 +98,22 @@ This repository includes Cursor plugin metadata at:
|
|
|
93
98
|
.cursor-plugin/plugin.json
|
|
94
99
|
```
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
To get `/add-plugin` working, submit the repo for Cursor marketplace
|
|
102
|
+
review/indexing:
|
|
97
103
|
|
|
98
104
|
1. Push this repository to GitHub and keep it public.
|
|
99
105
|
2. Keep the single-plugin layout at the repository root:
|
|
100
106
|
`.cursor-plugin/plugin.json` plus `skills/dbcli/`.
|
|
101
|
-
3. Confirm the manifest name is lowercase kebab-case (`dbcli-agent`)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
3. Confirm the manifest name is lowercase kebab-case (`dbcli-agent`), the skill
|
|
108
|
+
files carry YAML frontmatter, `README.md` exists at the repo root, and all
|
|
109
|
+
manifest paths are relative with no `..` or absolute paths.
|
|
110
|
+
4. Submit the repository at <https://cursor.com/marketplace/publish>. This is the
|
|
111
|
+
current channel — the marketplace launched with Cursor 2.5 (2026-02-17) and
|
|
112
|
+
every plugin is manually reviewed and must be open source. Do **not** email
|
|
113
|
+
`kniparko@anysphere.com`; that address came from an older plugin template and
|
|
114
|
+
is not the submission path.
|
|
115
|
+
5. After the plugin is reviewed and listed, users can install from Cursor Agent
|
|
116
|
+
chat with `/add-plugin dbcli-agent` or by searching the Cursor marketplace.
|
|
108
117
|
|
|
109
118
|
The repo also keeps an instruction-file fallback for projects that want to
|
|
110
119
|
vendor the dbcli rule and reference directly.
|
|
@@ -45,7 +45,7 @@ takes a full JSON filter and is exempt.)
|
|
|
45
45
|
|
|
46
46
|
Slow-query diagnosis has three canonical paths (pick by what you already know):
|
|
47
47
|
|
|
48
|
-
- Known slow SQL → `skill tasks plan diagnose-slow-query --param query="<SQL>"` → `guide missing-index-for "<SQL>"`
|
|
48
|
+
- Known slow SQL → `skill tasks plan diagnose-slow-query --param query="<SQL>"` → `lint "<SQL>"` → `guide missing-index-for "<SQL>"`
|
|
49
49
|
- Known hot table → `skill tasks plan analyze-table-perf --param table=<table>`
|
|
50
50
|
- Whole-environment scan → `report --section perf` → `guide slow-query`
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ afterwards add only the `@diag/*` it does not cover (`missing-indexes`, `locks`,
|
|
|
54
54
|
`table-sizes`). Once you have a specific slow statement, `explain --analyze "<SQL>"` shows its plan.
|
|
55
55
|
|
|
56
56
|
**On failure:** pass `--recovery` to `query` / `q` / `insert` / `update` / `delete` /
|
|
57
|
-
`export` / `schema` / `inspect`. The command emits a `RecoveryEnvelope` to stdout and saves
|
|
57
|
+
`export` / `schema` / `inspect` / `lint` / `diff --against-orm`. The command emits a `RecoveryEnvelope` to stdout and saves
|
|
58
58
|
it to `.dbcli/last-recovery.json`; then `dbcli recover` inspects it and `dbcli recover --apply`
|
|
59
59
|
runs the saved plan under risk gating. Multi-turn `--next`, connection branching, and the
|
|
60
60
|
post-apply verify probe are documented in reference.md §Recovery Cookbook.
|
|
@@ -81,12 +81,16 @@ The plan is an ordered list of dbcli commands with rationale and risk labels. Ex
|
|
|
81
81
|
one at a time — task plans do **not** override blacklist, schema, dry-run, or confirmation
|
|
82
82
|
requirements.
|
|
83
83
|
|
|
84
|
-
Builtin packs: `diagnose-slow-query` (targets a specific SQL),
|
|
85
|
-
a specific table; `dbcli inspect` auto-suggests it for the
|
|
86
|
-
activity), `audit-permissions`, `safe-backfill`,
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
84
|
+
Builtin packs (SQL — postgres/mysql): `diagnose-slow-query` (targets a specific SQL),
|
|
85
|
+
`analyze-table-perf` (targets a specific table; `dbcli inspect` auto-suggests it for the
|
|
86
|
+
hottest table in recent audit activity), `audit-permissions`, `safe-backfill`,
|
|
87
|
+
`schema-drift-review`, `orm-drift-review` (ORM definition vs cached DB schema),
|
|
88
|
+
`connection-health`. Review/verify packs: `pr-database-review`,
|
|
89
|
+
`migration-review`, `safe-backfill-verify`, `slow-endpoint-investigation`. MongoDB packs:
|
|
90
|
+
`mongo-safe-backfill` (dry-run–previewed backfill), `mongo-schema-drift-review` (sampled
|
|
91
|
+
dot-path drift). All are read-only `plan-only` — pick the pack matching the situation, and
|
|
92
|
+
run any index/DDL proposal through `migration-review` before writing. Redis/Elasticsearch
|
|
93
|
+
have no packs yet — lead with `guide` / `report` there.
|
|
90
94
|
|
|
91
95
|
Tasks live under `assets/tasks/` (builtin), `.dbcli-shared/tasks/` (shared), and
|
|
92
96
|
`.dbcli/tasks/` (local override).
|
|
@@ -101,9 +105,9 @@ in **How to use dbcli** still applies.
|
|
|
101
105
|
| DB-backed feature | `blacklist list` → `schema <object>` → `queries suggest <intent>` |
|
|
102
106
|
| DB report / dashboard request | `blacklist list` → `queries search <keywords>` / `queries suggest <intent>` → `queries show @<name>` → `q @<name> --ui` or `--format html` |
|
|
103
107
|
| Application data bug | `audit tail --for-agent --n 10` → `blacklist list` → `schema <object>` → narrow query |
|
|
104
|
-
| ORM or migration work | `schema --format json` → `diff --
|
|
108
|
+
| ORM or migration work | `schema --format json` → `diff --against-orm <orm-schema>` → review error-level drift → proposals via `migrate` (dry-run) → `migration-review` task pack → `diff --against <snapshot>` after applying. |
|
|
105
109
|
| PR database review | Review changed persistence paths, then propose concrete `schema` / `plan` / `dry-run` / `report` / `guide` commands per material claim. |
|
|
106
|
-
| Slow endpoint or query | `report --section perf` → task pack `analyze-table-perf` → `guide missing-index-for "<query>"`; use `proxy analyze` when logs exist. |
|
|
110
|
+
| Slow endpoint or query | `report --section perf` → task pack `analyze-table-perf` → `lint "<query>"` → `guide missing-index-for "<query>"`; use `proxy analyze` when logs exist. |
|
|
107
111
|
| Safe data backfill | `blacklist list` → `schema <object>` → count/scope query → `update … --dry-run` → read-back or snippet `--verify`. |
|
|
108
112
|
| Environment validation | `status --format json` → `doctor --format json` → `inspect --for-agent --no-connect`. |
|
|
109
113
|
|
|
@@ -121,9 +125,13 @@ dbcli q @<name> --param k=v --format html > report.html
|
|
|
121
125
|
dbcli export "<SQL>" --format html --output report.html
|
|
122
126
|
dbcli audit tail --for-agent --n 10
|
|
123
127
|
dbcli diff --snapshot <name>
|
|
128
|
+
dbcli diff --against-orm prisma/schema.prisma --format json
|
|
129
|
+
dbcli diff --against-orm "migrations/*.sql" --format markdown
|
|
130
|
+
dbcli skill tasks plan orm-drift-review --param orm_path=prisma/schema.prisma --format json
|
|
124
131
|
dbcli report --section perf --format json
|
|
125
132
|
dbcli skill tasks plan analyze-table-perf --param table=<table> --format json
|
|
126
133
|
dbcli guide missing-index-for "<query>" --format json
|
|
134
|
+
dbcli lint "<SQL>" --format json
|
|
127
135
|
dbcli update <object> --where "<bounded predicate>" --set '<json>' --dry-run --format json
|
|
128
136
|
dbcli inspect --for-agent --no-connect --format json
|
|
129
137
|
```
|
|
@@ -290,6 +298,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
|
|
|
290
298
|
| `schema` | query-only+ | SQL: per-table or full scan into `.dbcli/schemas/`. MongoDB: sampled. ES: flattened mapping. Redis: per-key only (type/TTL/size). Supports `--recovery`. |
|
|
291
299
|
| `query` | query-only+ | SQL, Mongo JSON (`--collection`), Redis command, or ES DSL/Lucene (`--collection`). `--format table\|json\|csv\|html`, `--ui` to open the interactive dashboard in a browser. Supports `--recovery`. |
|
|
292
300
|
| `explain` | query-only+ | **(v1.23)** Read-only query plan with annotations. SQL only. Single query, `@saved-query`, `@file.sql`, or `--bulk @glob/*`. `--analyze` (EXPLAIN ANALYZE / MariaDB ANALYZE SELECT), `--format markdown\|json\|table`. |
|
|
301
|
+
| `lint` | n/a | Static SQL anti-pattern advisor (no DB connection). 9 rules incl. schema-aware implicit-cast / NOT IN-nullable checks via the layered `.dbcli/schemas/` cache; global `--use <conn>` selects a named cache. Findings carry rewrite drafts + guarded `explain` verify commands (`--analyze` only for proven read-only SQL) — report-only, never executes. `--format text\|json\|markdown`, `--min-severity`, `--no-schema`, `--bulk`. Supports `--recovery`. |
|
|
293
302
|
| `plan` | n/a | Static SQL risk analyzer (`--format text\|json`); classifies a statement without connecting to the database. |
|
|
294
303
|
| `q` | query-only+ | Run a saved snippet by `@name` with `--param k=v`. Supports `--verify` to run assertions. |
|
|
295
304
|
| `queries` | n/a | Manage saved snippets: `list` / `show` / `search` / `suggest` / `new` / `edit` / `check` / `delete` / `rename` / `copy` / `import` / `export`. |
|
|
@@ -298,7 +307,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
|
|
|
298
307
|
| `export` | query-only+ | SQL, MongoDB, or **(v1.22)** Elasticsearch (DSL `--index` or whole-index scroll). Query → `--format json\|jsonl\|csv\|html` file or stdout. `html` emits a standalone interactive dashboard. Supports `--recovery`. |
|
|
299
308
|
| `blacklist` | n/a | `list` / `table` / `column` subcommands redact sensitive data from query results. |
|
|
300
309
|
| `check` | query-only+ | SQL only (best on MySQL/MariaDB). |
|
|
301
|
-
| `diff` | query-only+ | SQL only. Save/compare schema snapshots. |
|
|
310
|
+
| `diff` | query-only+ | SQL only. Save/compare schema snapshots. **(P1b)** `--against-orm <path>` compares a Prisma schema / DDL file / normalized JSON against the local schema cache (no DB connection): categorized drift (`missing_in_db` = error, `missing_in_orm` = warn, `mismatch` per tolerance table, `unmanaged`) with dry-run `migrate` proposals; exit 1 on error-level drift. `--orm-format prisma\|ddl\|json\|drizzle\|typeorm\|sequelize`, `--ignore <globs>`, `--format json\|table\|markdown`. Drizzle: point at `drizzle/meta/<NNNN>_snapshot.json` (run `drizzle-kit generate` first; `.ts` sources are rejected with a hint). TypeORM/Sequelize: feed tool-generated DDL (`schema:log` / a schema-only dump); source files are rejected with the exact generation command to run. |
|
|
302
311
|
| `snapshot` | query-only+ | **(v1.25)** SQL only. Capture a result fingerprint (`rowCount` + per-column null/distinct/min/max/sum + order-independent checksum). `--out` (default `.dbcli/snapshots/snap-<ts>.json`), `--rows`, `--stdout`, `--format`, `--no-limit`. Baseline for `assert --against`. |
|
|
303
312
|
| `assert` | query-only+ | **(v1.25)** SQL only. Verify an invariant; exit 1 on failure unless `--no-fail`. `--expect "rows>0\|value==X\|col:c not null\|unique\|between a and b\|>= n"`, `--vs <query> --compare rows\|value` (reconcile), `--against <snapshot> --tolerance <pct>`. |
|
|
304
313
|
| `verification` | n/a | Inspect and manage local verification artifacts. `list` / `show <id-or-path>` / `summary` are read-only; `prune` is dry-run by default and deletes only with `--execute --force`. Reads `<cwd>/.dbcli/verification/`; no DB connection, no audit writes. |
|
|
@@ -318,7 +327,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
|
|
|
318
327
|
|
|
319
328
|
`--use <name>` on any subcommand (including `status` / `doctor`) targets a v2 connection
|
|
320
329
|
without changing the default. `--recovery` is honoured by `query`, `q`, `insert`, `update`,
|
|
321
|
-
`delete`, `export`, `schema`, and `
|
|
330
|
+
`delete`, `export`, `schema`, `inspect`, `lint`, and `diff --against-orm` (see **On failure** above).
|
|
322
331
|
|
|
323
332
|
**Write & query flag semantics** (SQL/Mongo `insert`/`update`):
|
|
324
333
|
|