@carllee1983/dbcli 1.39.2 → 1.41.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/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.constraint_name = kcu.constraint_name
14573
- WHERE tc.table_name = $1
14574
- AND tc.table_schema = 'public'
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 t ON t.typname = c.udt_name
14598
- JOIN pg_enum e ON e.enumtypid = t.oid
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
- tc.constraint_name as name,
14612
- array_agg(kcu.column_name) as columns,
14613
- ccu.table_name as ref_table,
14614
- array_agg(ccu.column_name) as ref_columns
14615
- FROM information_schema.table_constraints AS tc
14616
- JOIN information_schema.key_column_usage AS kcu
14617
- ON tc.table_name = kcu.table_name AND tc.constraint_name = kcu.constraint_name
14618
- JOIN information_schema.constraint_column_usage AS ccu
14619
- ON ccu.constraint_name = tc.constraint_name
14620
- WHERE tc.table_name = $1 AND tc.constraint_type = 'FOREIGN KEY'
14621
- GROUP BY tc.constraint_name, ccu.table_name
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
- WHERE relname = $1
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 array_agg(a.attname) as columns
14659
- FROM (
14660
- SELECT a.attname
14661
- FROM pg_index i
14662
- JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
14663
- WHERE i.indisprimary AND i.indrelid = $1::regclass
14664
- ORDER BY a.attnum
14665
- ) a
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 countResult = await this.execute(`SELECT COUNT(*) as count FROM "${tableName}"`);
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
- try {
21805
- const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
21806
- const loader = new SchemaLayeredLoader2(storagePath, { connectionName: resolved.name });
21807
- const { cache, index } = await loader.initialize();
21808
- if (index && Object.keys(index.tables).length > 0) {
21809
- const layeredSchema = {};
21810
- for (const tableName of Object.keys(index.tables)) {
21811
- const s = await cache.getTableSchema(tableName);
21812
- if (s)
21813
- layeredSchema[tableName] = s;
21814
- }
21815
- if (Object.keys(layeredSchema).length > 0) {
21816
- schema = layeredSchema;
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 KEEP_VALUE_FLAGS = new Set(["--format", "--conn-name"]);
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 out = [argv[0]];
21964
- if (argv.length === 1)
21965
- return out.join(" ");
21966
- const sub = argv[1];
21967
- out.push(sub);
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.startsWith("--")) {
21971
- const [name, inlineValue] = tok.split("=");
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 && !argv[i + 1].startsWith("--")) {
22124
+ if (i + 1 < argv.length) {
21984
22125
  out.push(argv[++i]);
21985
22126
  }
21986
22127
  }
21987
22128
  continue;
21988
22129
  }
21989
- out.push(tok);
21990
- continue;
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 === 2 && SQL_SUBCOMMANDS.has(sub)) {
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "1.37.1",
3
+ "version": "1.41.0",
4
4
  "description": "Database CLI skill and command reference for AI agents.",
5
5
  "contextFileName": "AGENTS.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.39.2",
3
+ "version": "1.41.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "1.39.2",
3
+ "version": "1.41.0",
4
4
  "description": "Database CLI skill and command reference for AI agents",
5
5
  "author": {
6
6
  "name": "Carl Lee",
@@ -78,8 +78,13 @@ used by Gemini/Antigravity-style agents.
78
78
 
79
79
  ## Cursor
80
80
 
81
- Cursor can install plugins from Cursor Agent chat when the plugin is available
82
- in Cursor's plugin marketplace:
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
- For Cursor marketplace review/indexing:
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`) and that
102
- `category`, `tags`, `homepage`, `repository`, and `skills` are present.
103
- 4. Submit the repository URL to the Cursor plugin team for review/indexing.
104
- The Cursor plugin template currently points submitters to the Cursor
105
- community Slack or `kniparko@anysphere.com`.
106
- 5. After indexing, users can install from Cursor Agent chat with
107
- `/add-plugin dbcli-agent` or by searching the Cursor plugin marketplace.
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), `analyze-table-perf` (targets
85
- a specific table; `dbcli inspect` auto-suggests it for the hottest table in recent audit
86
- activity), `audit-permissions`, `safe-backfill`, `schema-drift-review`, `connection-health`.
87
- Review/verify packs: `pr-database-review`, `migration-review`, `safe-backfill-verify`,
88
- `slow-endpoint-investigation`. All are read-only `plan-only` — pick the pack matching the
89
- situation, and run any index/DDL proposal through `migration-review` before writing.
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 --snapshot <name>` → `migrate add-index`/`add-column` (preview SQL) → `diff --against <snapshot>` |
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`, `--ignore <globs>`, `--format json\|table\|markdown`. |
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 `inspect` (see **On failure** above).
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