@carllee1983/dbcli 1.39.1 → 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,
@@ -17423,6 +17480,7 @@ class SessionIdService {
17423
17480
 
17424
17481
  // src/core/config-binding.ts
17425
17482
  import { createHash } from "crypto";
17483
+ import { mkdir as mkdir4, unlink } from "fs/promises";
17426
17484
  import { homedir } from "os";
17427
17485
  import { basename, join as join3, resolve } from "path";
17428
17486
  var BINDING_FILE_NAME = "config.json";
@@ -17467,8 +17525,8 @@ async function writeProjectBinding(projectPath, storagePath = getProjectStorageP
17467
17525
  createdAt: new Date().toISOString()
17468
17526
  }
17469
17527
  };
17470
- await Bun.$`mkdir -p ${projectPath}`;
17471
- await Bun.$`mkdir -p ${storagePath}`;
17528
+ await mkdir4(projectPath, { recursive: true });
17529
+ await mkdir4(storagePath, { recursive: true });
17472
17530
  await Bun.file(join3(projectPath, BINDING_FILE_NAME)).write(JSON.stringify(binding, null, 2));
17473
17531
  return binding;
17474
17532
  }
@@ -21624,6 +21682,7 @@ async function loadEnvFile(filePath) {
21624
21682
 
21625
21683
  // src/core/config-v2.ts
21626
21684
  import { join as join4 } from "path";
21685
+ import { mkdir as mkdir5, rename as rename3 } from "fs/promises";
21627
21686
  function detectConfigVersion(raw) {
21628
21687
  if (typeof raw === "object" && raw !== null && "version" in raw && raw.version === 2 && "connections" in raw) {
21629
21688
  return 2;
@@ -21667,10 +21726,10 @@ async function writeV2Config(path, config) {
21667
21726
  const storagePath = await resolveConfigStoragePath(path);
21668
21727
  const configPath = join4(storagePath, "config.json");
21669
21728
  const tmpPath = `${configPath}.tmp`;
21670
- await Bun.$`mkdir -p ${storagePath}`;
21729
+ await mkdir5(storagePath, { recursive: true });
21671
21730
  const json = JSON.stringify(config, null, 2);
21672
21731
  await Bun.write(tmpPath, json);
21673
- await Bun.$`mv -f ${tmpPath} ${configPath}`;
21732
+ await rename3(tmpPath, configPath);
21674
21733
  }
21675
21734
  function listConnections(config) {
21676
21735
  return Object.entries(config.connections).map(([name, conn]) => {
@@ -21689,6 +21748,7 @@ function listConnections(config) {
21689
21748
 
21690
21749
  // src/core/config.ts
21691
21750
  import { join as join9 } from "path";
21751
+ import { mkdir as mkdir6 } from "fs/promises";
21692
21752
  var _globalConnectionName;
21693
21753
  function getGlobalConnectionName() {
21694
21754
  return _globalConnectionName;
@@ -21754,7 +21814,7 @@ function parseEnvPassword(content) {
21754
21814
  return match?.[1] != null ? match[1].trim() : null;
21755
21815
  }
21756
21816
  var configModule = {
21757
- async read(path, connectionName) {
21817
+ async read(path, connectionName, options = {}) {
21758
21818
  const effectiveConnectionName = connectionName ?? _globalConnectionName;
21759
21819
  try {
21760
21820
  const binding = await readProjectBinding(path);
@@ -21798,23 +21858,27 @@ var configModule = {
21798
21858
  resolvedConnection.password = legacyPassword;
21799
21859
  }
21800
21860
  let schema = (v2Config.schemas ?? {})[resolved.name] ?? v2Config.schema;
21801
- try {
21802
- const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
21803
- const loader = new SchemaLayeredLoader2(storagePath, { connectionName: resolved.name });
21804
- const { cache, index } = await loader.initialize();
21805
- if (index && Object.keys(index.tables).length > 0) {
21806
- const layeredSchema = {};
21807
- for (const tableName of Object.keys(index.tables)) {
21808
- const s = await cache.getTableSchema(tableName);
21809
- if (s)
21810
- layeredSchema[tableName] = s;
21811
- }
21812
- if (Object.keys(layeredSchema).length > 0) {
21813
- 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
+ }
21814
21878
  }
21879
+ } catch {
21880
+ console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
21815
21881
  }
21816
- } catch {
21817
- console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
21818
21882
  }
21819
21883
  return DbcliConfigSchema.parse({
21820
21884
  connection: resolvedConnection,
@@ -21898,7 +21962,7 @@ var configModule = {
21898
21962
  isDirectory = false;
21899
21963
  }
21900
21964
  if (isDirectory || path.endsWith(".dbcli") || path === storagePath && isDirectory) {
21901
- await Bun.$`mkdir -p ${storagePath}`;
21965
+ await mkdir6(storagePath, { recursive: true });
21902
21966
  const hasEnvReferences = isEnvReference(config.connection.password);
21903
21967
  if (hasEnvReferences) {
21904
21968
  const configPath = join9(storagePath, "config.json");
@@ -21940,7 +22004,7 @@ DBCLI_PASSWORD=${password}
21940
22004
  };
21941
22005
 
21942
22006
  // src/utils/redaction.ts
21943
- var SQL_SUBCOMMANDS = new Set(["query", "export"]);
22007
+ var SQL_SUBCOMMANDS = new Set(["query", "export", "lint"]);
21944
22008
  var REDACTED_VALUE_FLAGS = new Set([
21945
22009
  "--where",
21946
22010
  "--set",
@@ -21951,21 +22015,101 @@ var REDACTED_VALUE_FLAGS = new Set([
21951
22015
  "--use",
21952
22016
  "--password",
21953
22017
  "--token",
21954
- "--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"
21955
22029
  ]);
21956
- 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
+ }
21957
22097
  function redactArgv(argv) {
21958
22098
  if (argv.length === 0)
21959
22099
  return "<unknown>";
21960
- const out = [argv[0]];
21961
- if (argv.length === 1)
21962
- return out.join(" ");
21963
- const sub = argv[1];
21964
- out.push(sub);
21965
- 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++) {
21966
22105
  const tok = argv[i];
21967
- if (tok.startsWith("--")) {
21968
- 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);
21969
22113
  if (REDACTED_VALUE_FLAGS.has(name)) {
21970
22114
  out.push(`${name} <redacted>`);
21971
22115
  if (inlineValue === undefined)
@@ -21977,23 +22121,37 @@ function redactArgv(argv) {
21977
22121
  out.push(tok);
21978
22122
  } else {
21979
22123
  out.push(name);
21980
- if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
22124
+ if (i + 1 < argv.length) {
21981
22125
  out.push(argv[++i]);
21982
22126
  }
21983
22127
  }
21984
22128
  continue;
21985
22129
  }
21986
- out.push(tok);
21987
- 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
+ }
21988
22138
  }
21989
- if (i === 2 && SQL_SUBCOMMANDS.has(sub)) {
22139
+ if (sensitiveCommand && i > sensitiveCommand.index && (sensitiveCommand.command === "lint" || !redactedSingleSql)) {
21990
22140
  out.push("<sql>");
22141
+ redactedSingleSql = true;
21991
22142
  continue;
21992
22143
  }
21993
22144
  out.push(tok);
21994
22145
  }
21995
22146
  return out.join(" ");
21996
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
+ }
21997
22155
  function redactSensitive(text) {
21998
22156
  return text.replace(/\b(password|token|apiKey|secret|key|token|auth|credential|pass|pwd|sid)([:=]|\s+)([^\s"';,]+)/gi, "$1$2<redacted>");
21999
22157
  }
@@ -22039,6 +22197,7 @@ async function writeAuditEntry(config, commandName, options, outcome) {
22039
22197
  if (outcome.error) {
22040
22198
  errorMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
22041
22199
  errorMessage = redactSensitive(errorMessage);
22200
+ errorMessage = redactArgvSensitiveText(errorMessage, process.argv);
22042
22201
  }
22043
22202
  const entry = {
22044
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.1",
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.31.0",
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.