@carllee1983/dbcli 1.46.0 → 1.47.1
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 +9 -3
- package/.cursor/skills/dbcli/reference.md +45 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +9 -3
- package/.github/skills/dbcli/reference.md +45 -3
- package/CHANGELOG.md +44 -0
- package/assets/SKILL.md +9 -3
- package/assets/SKILL.zh-TW.md +4 -2
- package/assets/reference.md +45 -3
- package/dist/cli.mjs +10437 -10102
- package/dist/core.d.ts +76 -3
- package/dist/core.mjs +203 -17
- 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/skills/dbcli/SKILL.md +9 -3
- package/plugins/dbcli-agent/skills/dbcli/reference.md +45 -3
- package/skills/dbcli/SKILL.md +9 -3
- package/skills/dbcli/reference.md +45 -3
package/dist/core.mjs
CHANGED
|
@@ -16725,9 +16725,22 @@ class ElasticsearchAdapter {
|
|
|
16725
16725
|
}
|
|
16726
16726
|
}
|
|
16727
16727
|
|
|
16728
|
+
// src/utils/connection-timeout.ts
|
|
16729
|
+
var _globalConnectionTimeout;
|
|
16730
|
+
function resolveConnectionTimeout(configured) {
|
|
16731
|
+
return _globalConnectionTimeout ?? configured;
|
|
16732
|
+
}
|
|
16733
|
+
function withResolvedTimeout(options) {
|
|
16734
|
+
const timeout = resolveConnectionTimeout(options.timeout);
|
|
16735
|
+
if (timeout === options.timeout)
|
|
16736
|
+
return options;
|
|
16737
|
+
return { ...options, timeout };
|
|
16738
|
+
}
|
|
16739
|
+
|
|
16728
16740
|
// src/adapters/factory.ts
|
|
16729
16741
|
class AdapterFactory {
|
|
16730
|
-
static createSqlAdapter(
|
|
16742
|
+
static createSqlAdapter(rawOptions) {
|
|
16743
|
+
const options = withResolvedTimeout(rawOptions);
|
|
16731
16744
|
switch (options.system) {
|
|
16732
16745
|
case "postgresql":
|
|
16733
16746
|
return new PostgreSQLAdapter(options);
|
|
@@ -16738,7 +16751,8 @@ class AdapterFactory {
|
|
|
16738
16751
|
throw new Error(`createSqlAdapter requires a SQL system, got: ${options.system}`);
|
|
16739
16752
|
}
|
|
16740
16753
|
}
|
|
16741
|
-
static createQueryableAdapter(
|
|
16754
|
+
static createQueryableAdapter(rawOptions) {
|
|
16755
|
+
const options = withResolvedTimeout(rawOptions);
|
|
16742
16756
|
switch (options.system) {
|
|
16743
16757
|
case "mongodb":
|
|
16744
16758
|
return new MongoDBAdapter(options);
|
|
@@ -16770,10 +16784,11 @@ class AdapterFactory {
|
|
|
16770
16784
|
}
|
|
16771
16785
|
return AdapterFactory.createQueryableAdapter(options);
|
|
16772
16786
|
}
|
|
16773
|
-
static createRedisAdapter(
|
|
16774
|
-
if (
|
|
16787
|
+
static createRedisAdapter(rawOptions, blacklistRules = [], maskRules = []) {
|
|
16788
|
+
if (rawOptions.system !== "redis") {
|
|
16775
16789
|
throw new Error("createRedisAdapter requires system: redis");
|
|
16776
16790
|
}
|
|
16791
|
+
const options = withResolvedTimeout(rawOptions);
|
|
16777
16792
|
const adapter = new RedisAdapter(options);
|
|
16778
16793
|
adapter.setBlacklistRules(blacklistRules);
|
|
16779
16794
|
adapter.setMaskRules(maskRules);
|
|
@@ -16802,6 +16817,7 @@ function normalizeSQL(sql) {
|
|
|
16802
16817
|
return sql.replace(/--[^\n]*\n/g, `
|
|
16803
16818
|
`).replace(/\/\*[\s\S]*?\*\//g, " ").trim().replace(/\s+/g, " ");
|
|
16804
16819
|
}
|
|
16820
|
+
var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
|
|
16805
16821
|
function stripCommentsAndStrings(sql, options = {}) {
|
|
16806
16822
|
let result = "";
|
|
16807
16823
|
let i = 0;
|
|
@@ -16844,11 +16860,19 @@ function stripCommentsAndStrings(sql, options = {}) {
|
|
|
16844
16860
|
i = closingIndex === -1 ? sql.length : closingIndex + 2;
|
|
16845
16861
|
continue;
|
|
16846
16862
|
}
|
|
16863
|
+
const nests = options.dialect === "postgresql";
|
|
16864
|
+
let depth = 1;
|
|
16847
16865
|
i += 2;
|
|
16848
|
-
while (i < sql.length) {
|
|
16866
|
+
while (i < sql.length && depth > 0) {
|
|
16867
|
+
if (nests && sql[i] === "/" && sql[i + 1] === "*") {
|
|
16868
|
+
depth++;
|
|
16869
|
+
i += 2;
|
|
16870
|
+
continue;
|
|
16871
|
+
}
|
|
16849
16872
|
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
16873
|
+
depth--;
|
|
16850
16874
|
i += 2;
|
|
16851
|
-
|
|
16875
|
+
continue;
|
|
16852
16876
|
}
|
|
16853
16877
|
i++;
|
|
16854
16878
|
}
|
|
@@ -16856,7 +16880,8 @@ function stripCommentsAndStrings(sql, options = {}) {
|
|
|
16856
16880
|
continue;
|
|
16857
16881
|
}
|
|
16858
16882
|
if (options.dialect === "postgresql" && char === "$") {
|
|
16859
|
-
const
|
|
16883
|
+
const opensToken = !IDENTIFIER_CONTINUATION.test(sql[i - 1] ?? "");
|
|
16884
|
+
const delimiter = opensToken ? sql.slice(i).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)?.[0] : undefined;
|
|
16860
16885
|
if (delimiter) {
|
|
16861
16886
|
i += delimiter.length;
|
|
16862
16887
|
const closingIndex = sql.indexOf(delimiter, i);
|
|
@@ -16884,7 +16909,7 @@ function stripCommentsAndStrings(sql, options = {}) {
|
|
|
16884
16909
|
if (char === "'" || char === '"') {
|
|
16885
16910
|
const quote = char;
|
|
16886
16911
|
const quoteIndex = i;
|
|
16887
|
-
const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") &&
|
|
16912
|
+
const postgresEscapeString = options.dialect === "postgresql" && quote === "'" && /[eE]/.test(sql[quoteIndex - 1] ?? "") && !IDENTIFIER_CONTINUATION.test(sql[quoteIndex - 2] ?? "");
|
|
16888
16913
|
const backslashEscapes = options.dialect === undefined || postgresEscapeString;
|
|
16889
16914
|
i++;
|
|
16890
16915
|
while (i < sql.length) {
|
|
@@ -17060,8 +17085,59 @@ function classifyStatement(sql) {
|
|
|
17060
17085
|
confidence: determineConfidence(type, firstKeyword, upper)
|
|
17061
17086
|
};
|
|
17062
17087
|
}
|
|
17063
|
-
|
|
17064
|
-
|
|
17088
|
+
var SQL_DIALECTS = ["postgresql", "mysql", "mariadb"];
|
|
17089
|
+
var SQL_WRITE_OR_DDL_KEYWORDS = /(?<![.\w])(INSERT|UPDATE|DELETE|MERGE|UPSERT|REPLACE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|RENAME|INTO)\b(?!\s*\()/i;
|
|
17090
|
+
var SQL_LOCK_CLAUSE = /\bFOR\s+(?:NO\s+KEY\s+)?UPDATE\b|\bFOR\s+(?:KEY\s+)?SHARE\b/gi;
|
|
17091
|
+
function findWriteKeyword(sql, dialects) {
|
|
17092
|
+
const candidates = dialects && dialects.length > 0 ? dialects : SQL_DIALECTS;
|
|
17093
|
+
for (const dialect of candidates) {
|
|
17094
|
+
const executable = stripCommentsAndStrings(sql, { dialect }).replace(SQL_LOCK_CLAUSE, " ");
|
|
17095
|
+
const match = executable.match(SQL_WRITE_OR_DDL_KEYWORDS);
|
|
17096
|
+
if (match?.[1])
|
|
17097
|
+
return match[1].toUpperCase();
|
|
17098
|
+
}
|
|
17099
|
+
return;
|
|
17100
|
+
}
|
|
17101
|
+
function containsMultipleStatements(sql, dialect) {
|
|
17102
|
+
const statementCount = (candidate) => stripCommentsAndStrings(sql, { dialect: candidate }).split(";").filter((part) => part.trim().length > 0).length;
|
|
17103
|
+
if (dialect)
|
|
17104
|
+
return statementCount(dialect) > 1;
|
|
17105
|
+
return SQL_DIALECTS.some((candidate) => statementCount(candidate) > 1);
|
|
17106
|
+
}
|
|
17107
|
+
var ESCALATABLE_READ_TYPES = new Set(["SELECT", "EXPLAIN", "DESCRIBE"]);
|
|
17108
|
+
function escalateHiddenWrite(sql, classification, dialect) {
|
|
17109
|
+
if (!ESCALATABLE_READ_TYPES.has(classification.type))
|
|
17110
|
+
return classification;
|
|
17111
|
+
const plansOnly = classification.type === "EXPLAIN" || classification.type === "DESCRIBE";
|
|
17112
|
+
if (plansOnly && !/\bANALYZE\b/i.test(sql))
|
|
17113
|
+
return classification;
|
|
17114
|
+
const hidden = findWriteKeyword(sql, dialect ? [dialect] : undefined);
|
|
17115
|
+
if (!hidden)
|
|
17116
|
+
return classification;
|
|
17117
|
+
return {
|
|
17118
|
+
...classification,
|
|
17119
|
+
type: "UNKNOWN",
|
|
17120
|
+
isDangerous: true,
|
|
17121
|
+
confidence: "HIGH",
|
|
17122
|
+
escalatedFrom: hidden
|
|
17123
|
+
};
|
|
17124
|
+
}
|
|
17125
|
+
function checkPermission(sql, permission, dialect) {
|
|
17126
|
+
const classification = escalateHiddenWrite(sql, classifyStatement(sql), dialect);
|
|
17127
|
+
if (permission !== "admin" && containsMultipleStatements(sql, dialect)) {
|
|
17128
|
+
return {
|
|
17129
|
+
allowed: false,
|
|
17130
|
+
reason: "SQL containing multiple statements is refused below admin permission, because only " + "the first statement determines the permission check. Run each statement separately.",
|
|
17131
|
+
classification
|
|
17132
|
+
};
|
|
17133
|
+
}
|
|
17134
|
+
if (permission !== "admin" && classification.escalatedFrom) {
|
|
17135
|
+
return {
|
|
17136
|
+
allowed: false,
|
|
17137
|
+
reason: `This statement opens as a read but contains an executable ` + `${classification.escalatedFrom}. A write hidden inside a read statement ` + `requires admin permission (current level: ${permission}).`,
|
|
17138
|
+
classification
|
|
17139
|
+
};
|
|
17140
|
+
}
|
|
17065
17141
|
if (permission === "admin") {
|
|
17066
17142
|
return {
|
|
17067
17143
|
allowed: true,
|
|
@@ -17121,8 +17197,8 @@ function checkPermission(sql, permission) {
|
|
|
17121
17197
|
classification
|
|
17122
17198
|
};
|
|
17123
17199
|
}
|
|
17124
|
-
function enforcePermission(sql, permission) {
|
|
17125
|
-
const result = checkPermission(sql, permission);
|
|
17200
|
+
function enforcePermission(sql, permission, dialect) {
|
|
17201
|
+
const result = checkPermission(sql, permission, dialect);
|
|
17126
17202
|
if (!result.allowed) {
|
|
17127
17203
|
throw new PermissionError(result.reason, result.classification, permission);
|
|
17128
17204
|
}
|
|
@@ -21851,6 +21927,11 @@ var StringOrEnvRef = exports_external.union([exports_external.string().min(1), E
|
|
|
21851
21927
|
var NumberOrEnvRef = exports_external.union([exports_external.number().int().min(1).max(65535), EnvRefSchema]);
|
|
21852
21928
|
var OptStringOrEnvRef = exports_external.union([exports_external.string(), EnvRefSchema]).optional().default("");
|
|
21853
21929
|
var OptNumberOrEnvRef = exports_external.union([exports_external.number().int(), EnvRefSchema]).optional().default(27017);
|
|
21930
|
+
var MIN_CONNECTION_TIMEOUT_MS = 100;
|
|
21931
|
+
var MAX_CONNECTION_TIMEOUT_MS = 600000;
|
|
21932
|
+
var TimeoutField = {
|
|
21933
|
+
timeout: exports_external.number().int().min(MIN_CONNECTION_TIMEOUT_MS).max(MAX_CONNECTION_TIMEOUT_MS).optional()
|
|
21934
|
+
};
|
|
21854
21935
|
var MongoDBConnectionConfigSchema = exports_external.object({
|
|
21855
21936
|
system: exports_external.literal("mongodb"),
|
|
21856
21937
|
uri: exports_external.union([exports_external.string(), EnvRefSchema]).optional(),
|
|
@@ -21862,7 +21943,8 @@ var MongoDBConnectionConfigSchema = exports_external.object({
|
|
|
21862
21943
|
authSource: exports_external.union([exports_external.string(), EnvRefSchema]).optional(),
|
|
21863
21944
|
replicaSet: exports_external.union([exports_external.string(), EnvRefSchema]).optional(),
|
|
21864
21945
|
tls: exports_external.boolean().optional(),
|
|
21865
|
-
srv: exports_external.boolean().optional().default(false)
|
|
21946
|
+
srv: exports_external.boolean().optional().default(false),
|
|
21947
|
+
...TimeoutField
|
|
21866
21948
|
});
|
|
21867
21949
|
var SqlConnectionConfigSchema = exports_external.object({
|
|
21868
21950
|
system: exports_external.enum(["postgresql", "mysql", "mariadb"]),
|
|
@@ -21870,7 +21952,8 @@ var SqlConnectionConfigSchema = exports_external.object({
|
|
|
21870
21952
|
port: NumberOrEnvRef,
|
|
21871
21953
|
user: StringOrEnvRef,
|
|
21872
21954
|
password: exports_external.union([exports_external.string(), EnvRefSchema]).default(""),
|
|
21873
|
-
database: StringOrEnvRef
|
|
21955
|
+
database: StringOrEnvRef,
|
|
21956
|
+
...TimeoutField
|
|
21874
21957
|
});
|
|
21875
21958
|
var RedisConnectionConfigSchema = exports_external.object({
|
|
21876
21959
|
system: exports_external.literal("redis"),
|
|
@@ -21878,7 +21961,8 @@ var RedisConnectionConfigSchema = exports_external.object({
|
|
|
21878
21961
|
port: NumberOrEnvRef,
|
|
21879
21962
|
user: OptStringOrEnvRef,
|
|
21880
21963
|
password: exports_external.union([exports_external.string(), EnvRefSchema]).optional().default(""),
|
|
21881
|
-
database: OptStringOrEnvRef
|
|
21964
|
+
database: OptStringOrEnvRef,
|
|
21965
|
+
...TimeoutField
|
|
21882
21966
|
});
|
|
21883
21967
|
var ElasticsearchConnectionConfigSchema = exports_external.object({
|
|
21884
21968
|
system: exports_external.literal("elasticsearch"),
|
|
@@ -21892,7 +21976,8 @@ var ElasticsearchConnectionConfigSchema = exports_external.object({
|
|
|
21892
21976
|
cloudId: exports_external.union([exports_external.string(), EnvRefSchema]).optional(),
|
|
21893
21977
|
apiKey: exports_external.union([exports_external.string(), EnvRefSchema]).optional(),
|
|
21894
21978
|
caPath: exports_external.string().optional(),
|
|
21895
|
-
rejectUnauthorized: exports_external.boolean().optional().default(true)
|
|
21979
|
+
rejectUnauthorized: exports_external.boolean().optional().default(true),
|
|
21980
|
+
...TimeoutField
|
|
21896
21981
|
});
|
|
21897
21982
|
var ConnectionConfigSchema = exports_external.union([
|
|
21898
21983
|
SqlConnectionConfigSchema,
|
|
@@ -21982,6 +22067,89 @@ var DbcliConfigV2Schema = exports_external.object({
|
|
|
21982
22067
|
path: ["default"]
|
|
21983
22068
|
});
|
|
21984
22069
|
|
|
22070
|
+
// src/utils/config-error-format.ts
|
|
22071
|
+
var SUPPORTED_SYSTEMS = [
|
|
22072
|
+
"postgresql",
|
|
22073
|
+
"mysql",
|
|
22074
|
+
"mariadb",
|
|
22075
|
+
"mongodb",
|
|
22076
|
+
"redis",
|
|
22077
|
+
"elasticsearch"
|
|
22078
|
+
];
|
|
22079
|
+
function isConnectionNode(raw, path) {
|
|
22080
|
+
const last = path[path.length - 1];
|
|
22081
|
+
const parent = path[path.length - 2];
|
|
22082
|
+
if (last !== "connection" && parent !== "connections")
|
|
22083
|
+
return false;
|
|
22084
|
+
const node = valueAtPath(raw, path);
|
|
22085
|
+
return typeof node === "object" && node !== null && !Array.isArray(node);
|
|
22086
|
+
}
|
|
22087
|
+
function valueAtPath(raw, path) {
|
|
22088
|
+
let current = raw;
|
|
22089
|
+
for (const key of path) {
|
|
22090
|
+
if (current == null || typeof current !== "object")
|
|
22091
|
+
return;
|
|
22092
|
+
current = current[key];
|
|
22093
|
+
}
|
|
22094
|
+
return current;
|
|
22095
|
+
}
|
|
22096
|
+
function declaredSystem(raw, path) {
|
|
22097
|
+
const node = valueAtPath(raw, path);
|
|
22098
|
+
if (node == null || typeof node !== "object")
|
|
22099
|
+
return;
|
|
22100
|
+
const system = node.system;
|
|
22101
|
+
return typeof system === "string" ? system : undefined;
|
|
22102
|
+
}
|
|
22103
|
+
function branchMatchesSystem(issues) {
|
|
22104
|
+
return !issues.some((issue) => issue.path[issue.path.length - 1] === "system" && (issue.code === "invalid_literal" || issue.code === "invalid_enum_value" || issue.code === "invalid_type"));
|
|
22105
|
+
}
|
|
22106
|
+
function formatPath(path) {
|
|
22107
|
+
return path.length > 0 ? path.join(".") : "(root)";
|
|
22108
|
+
}
|
|
22109
|
+
function flattenIssues(issues, raw, basePath = []) {
|
|
22110
|
+
const flat = [];
|
|
22111
|
+
for (const issue of issues) {
|
|
22112
|
+
const fullPath = [...basePath, ...issue.path];
|
|
22113
|
+
if (issue.code === "invalid_union") {
|
|
22114
|
+
const system = declaredSystem(raw, fullPath);
|
|
22115
|
+
const branches = issue.unionErrors.map((error) => error.issues);
|
|
22116
|
+
const candidates = system ? branches.filter((branchIssues) => branchMatchesSystem(branchIssues)) : [];
|
|
22117
|
+
if (isConnectionNode(raw, fullPath) && candidates.length === 0) {
|
|
22118
|
+
flat.push({
|
|
22119
|
+
path: formatPath([...fullPath, "system"]),
|
|
22120
|
+
message: `must be one of ${SUPPORTED_SYSTEMS.join(" | ")}` + (system === undefined ? " (missing)" : ` (received '${system}')`)
|
|
22121
|
+
});
|
|
22122
|
+
continue;
|
|
22123
|
+
}
|
|
22124
|
+
const chosen = candidates[0] ?? branches.reduce((best, current) => current.length < best.length ? current : best, branches[0] ?? []);
|
|
22125
|
+
flat.push(...flattenIssues(chosen, raw, basePath));
|
|
22126
|
+
continue;
|
|
22127
|
+
}
|
|
22128
|
+
flat.push({ path: formatPath(fullPath), message: issue.message });
|
|
22129
|
+
}
|
|
22130
|
+
return flat;
|
|
22131
|
+
}
|
|
22132
|
+
function dedupe(issues) {
|
|
22133
|
+
const seen = new Set;
|
|
22134
|
+
return issues.filter((issue) => {
|
|
22135
|
+
const key = `${issue.path}\x00${issue.message}`;
|
|
22136
|
+
if (seen.has(key))
|
|
22137
|
+
return false;
|
|
22138
|
+
seen.add(key);
|
|
22139
|
+
return true;
|
|
22140
|
+
});
|
|
22141
|
+
}
|
|
22142
|
+
function formatConfigValidationError(error, raw) {
|
|
22143
|
+
const issues = dedupe(flattenIssues(error.issues, raw));
|
|
22144
|
+
if (issues.length === 0)
|
|
22145
|
+
return error.message;
|
|
22146
|
+
return issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join(`
|
|
22147
|
+
`);
|
|
22148
|
+
}
|
|
22149
|
+
function isZodError(error) {
|
|
22150
|
+
return error instanceof Error && error.name === "ZodError" && Array.isArray(error.issues);
|
|
22151
|
+
}
|
|
22152
|
+
|
|
21985
22153
|
// src/agent-core/env-loader.ts
|
|
21986
22154
|
import { readFile as readFile3 } from "fs/promises";
|
|
21987
22155
|
function parseEnvContent(content) {
|
|
@@ -22188,6 +22356,7 @@ function parseEnvPassword(content) {
|
|
|
22188
22356
|
var configModule = {
|
|
22189
22357
|
async read(path, connectionName, options = {}) {
|
|
22190
22358
|
const effectiveConnectionName = connectionName ?? _globalConnectionName;
|
|
22359
|
+
let rawForDiagnostics;
|
|
22191
22360
|
try {
|
|
22192
22361
|
const binding = await readProjectBinding(path);
|
|
22193
22362
|
const storagePath = await resolveConfigStoragePath(path);
|
|
@@ -22213,6 +22382,7 @@ var configModule = {
|
|
|
22213
22382
|
const content = await configFile.text();
|
|
22214
22383
|
await assertConfigIntegrity(storagePath, content, { requireRecord: true });
|
|
22215
22384
|
const config = JSON.parse(content);
|
|
22385
|
+
rawForDiagnostics = config;
|
|
22216
22386
|
if (detectConfigVersion(config) === 2) {
|
|
22217
22387
|
const v2Config = DbcliConfigV2Schema.parse(config);
|
|
22218
22388
|
const resolved = resolveConnection(v2Config, effectiveConnectionName);
|
|
@@ -22293,6 +22463,7 @@ var configModule = {
|
|
|
22293
22463
|
assertNoConnectionSelectorOnV1(effectiveConnectionName);
|
|
22294
22464
|
const content = await file.text();
|
|
22295
22465
|
const raw = JSON.parse(content);
|
|
22466
|
+
rawForDiagnostics = raw;
|
|
22296
22467
|
const resolved = resolveEnvReferences(raw, process.env);
|
|
22297
22468
|
return DbcliConfigSchema.parse(resolved);
|
|
22298
22469
|
}
|
|
@@ -22303,6 +22474,10 @@ var configModule = {
|
|
|
22303
22474
|
} catch (error) {
|
|
22304
22475
|
if (error instanceof ConfigError)
|
|
22305
22476
|
throw error;
|
|
22477
|
+
if (isZodError(error)) {
|
|
22478
|
+
throw new ConfigError(`Failed to read .dbcli config: \u8A2D\u5B9A\u5167\u5BB9\u4E0D\u7B26\u5408\u7D50\u69CB
|
|
22479
|
+
${formatConfigValidationError(error, rawForDiagnostics)}`);
|
|
22480
|
+
}
|
|
22306
22481
|
if (error instanceof Error && error.message.includes("JSON")) {
|
|
22307
22482
|
throw new ConfigError(`Failed to parse .dbcli file: ${error.message}`);
|
|
22308
22483
|
}
|
|
@@ -22313,6 +22488,10 @@ var configModule = {
|
|
|
22313
22488
|
try {
|
|
22314
22489
|
return DbcliConfigSchema.parse(raw);
|
|
22315
22490
|
} catch (error) {
|
|
22491
|
+
if (isZodError(error)) {
|
|
22492
|
+
throw new ConfigError(`Invalid .dbcli config structure:
|
|
22493
|
+
${formatConfigValidationError(error, raw)}`);
|
|
22494
|
+
}
|
|
22316
22495
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
22317
22496
|
throw new ConfigError(`Invalid .dbcli config structure: ${errorMessage}`);
|
|
22318
22497
|
}
|
|
@@ -22413,6 +22592,7 @@ var KEEP_VALUE_FLAGS = new Set([
|
|
|
22413
22592
|
"--min-severity",
|
|
22414
22593
|
"--output",
|
|
22415
22594
|
"--limit",
|
|
22595
|
+
"--timeout",
|
|
22416
22596
|
"--collection",
|
|
22417
22597
|
"--index",
|
|
22418
22598
|
"--fields",
|
|
@@ -22766,6 +22946,12 @@ class QueryExecutor {
|
|
|
22766
22946
|
this.config = config;
|
|
22767
22947
|
this.options = options;
|
|
22768
22948
|
}
|
|
22949
|
+
resolveDialect() {
|
|
22950
|
+
if (this.options.dialect)
|
|
22951
|
+
return this.options.dialect;
|
|
22952
|
+
const system = this.config?.connection?.system;
|
|
22953
|
+
return SQL_DIALECTS.find((dialect) => dialect === system);
|
|
22954
|
+
}
|
|
22769
22955
|
takeDiagnostics() {
|
|
22770
22956
|
const diagnostics = this.pendingDiagnostics;
|
|
22771
22957
|
this.pendingDiagnostics = [];
|
|
@@ -22775,7 +22961,7 @@ class QueryExecutor {
|
|
|
22775
22961
|
const start = performance.now();
|
|
22776
22962
|
this.pendingDiagnostics = [];
|
|
22777
22963
|
try {
|
|
22778
|
-
const classification = enforcePermission(sql, this.permission);
|
|
22964
|
+
const classification = enforcePermission(sql, this.permission, this.resolveDialect());
|
|
22779
22965
|
const dangerousOperationWarning = classification.isDangerous && this.permission === "admin" ? `\u26A0 Warning: executing ${classification.type} operation (admin mode)` : undefined;
|
|
22780
22966
|
const AUTO_LIMIT_TYPES = new Set(["SELECT"]);
|
|
22781
22967
|
let executeSql = sql;
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -77,7 +77,10 @@ When reporting a check's outcome use the vocabulary `verified` (evidence matched
|
|
|
77
77
|
`not_verified` (check ran and contradicted) / `indeterminate` (ran but ambiguous) /
|
|
78
78
|
`blocked` (could not run due to config, permission, schema, placeholder, or safety gate).
|
|
79
79
|
|
|
80
|
-
Prefer `--format json` for agent-friendly output.
|
|
80
|
+
Prefer `--format json` for agent-friendly output. Diagnostics (auto-limit notices,
|
|
81
|
+
warnings) go to stderr so stdout stays parseable — when piping JSON into a parser,
|
|
82
|
+
use `2>/dev/null` or leave stderr alone. **Never `2>&1`**: it merges those lines back
|
|
83
|
+
into stdout and the parse fails.
|
|
81
84
|
|
|
82
85
|
## Agent Task Packs
|
|
83
86
|
|
|
@@ -214,7 +217,10 @@ or `doctor` / `status` reports a missing or invalid config, follow this flow.
|
|
|
214
217
|
`--password` / `--name` (and `--system`).
|
|
215
218
|
3. **What permission tier?** Default to the **lowest** that satisfies the task:
|
|
216
219
|
`query-only` → `read-write` → `data-admin` → `admin`. Set with `--permission`
|
|
217
|
-
(defaults to `query-only`).
|
|
220
|
+
(defaults to `query-only`). Tiers judge what a statement does, not how it
|
|
221
|
+
opens: below `admin`, multi-statement SQL is rejected; snippets must be free
|
|
222
|
+
of write and DDL keywords; MongoDB `$out` / `$merge` need `data-admin` and are
|
|
223
|
+
refused entirely in snippets and `export`.
|
|
218
224
|
4. **Verify, never assume.** After init: `dbcli status` (system + permission +
|
|
219
225
|
blacklist summary, no creds) and `dbcli doctor --format json` (env, config
|
|
220
226
|
shape, connectivity, schema-cache age, Mongo SRV path).
|
|
@@ -553,4 +559,4 @@ schema. Raw `query` / `export` invocations render a sortable table only.
|
|
|
553
559
|
- Blacklisted tables and columns are redacted from query output.
|
|
554
560
|
- `schema` reports `estimatedRowCount` and `sizeCategory` (small / medium / large / huge). For large/huge tables add `WHERE` or `LIMIT` — bands in reference.md.
|
|
555
561
|
- `doctor` on `mongodb+srv://` reports whether SRV resolves natively or through the DoH fallback — useful when the runtime restricts DNS.
|
|
556
|
-
- **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
|
|
562
|
+
- **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
|
|
@@ -19,6 +19,28 @@ command-level option is only valid after the command that declares it.
|
|
|
19
19
|
| `--config <path>` | Select the `.dbcli` configuration path. |
|
|
20
20
|
| `--global` | Select the user-global registry at `~/.config/dbcli/config.json` instead of the current project's `.dbcli` config. Place it before the command path. |
|
|
21
21
|
| `--use <connection>` | Select a named connection for this invocation; place it before the command path unless that command explicitly lists a command-level `--use`. |
|
|
22
|
+
| `--timeout <ms>` | Connection timeout in milliseconds (integer, 100–600000), overriding the connection config's `timeout` field for this invocation. Applies to every engine adapter. Without either the flag or the config field, adapters fall back to their built-in 5000ms default. |
|
|
23
|
+
|
|
24
|
+
`--timeout` is applied only when the adapter is constructed for this invocation — it is
|
|
25
|
+
never written back to `config.json`. Set the connection's `timeout` field instead for a
|
|
26
|
+
value that persists across runs. On PostgreSQL, the same value is also used as the
|
|
27
|
+
session's `statement_timeout` (not just the connection timeout), so a low value can cut
|
|
28
|
+
off a long-running query with an error that looks like a connection timeout; the 100ms
|
|
29
|
+
floor exists specifically to keep that failure mode from being too easy to trigger.
|
|
30
|
+
Elasticsearch applies its timeout per request rather than once for the whole connection.
|
|
31
|
+
The `timeout` field itself always takes a literal number — unlike other connection
|
|
32
|
+
fields, it does not accept an `{"$env": "..."}` reference.
|
|
33
|
+
|
|
34
|
+
### Redirecting output
|
|
35
|
+
|
|
36
|
+
Results go to stdout; diagnostics (auto-limit notices, warnings, update hints) go to
|
|
37
|
+
stderr. That split is what keeps `--format json` machine-parseable, so do not collapse
|
|
38
|
+
it with `2>&1` — the diagnostic lines land in front of the JSON document and the parse
|
|
39
|
+
fails. Pipe stdout alone, or add `2>/dev/null` when the diagnostics are not wanted:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dbcli query '{}' --collection events --format json 2>/dev/null | jq '.rows | length'
|
|
43
|
+
```
|
|
22
44
|
|
|
23
45
|
## Commands
|
|
24
46
|
|
|
@@ -118,6 +140,10 @@ agent mode disabled. A host that needs protection from a same-user hostile
|
|
|
118
140
|
process can set `DBCLI_CONFIG_INTEGRITY_ANCHOR_DIR` to a protected or read-only
|
|
119
141
|
directory; trusted writes publish detached digests there.
|
|
120
142
|
|
|
143
|
+
When a connection's config fails schema validation, dbcli reports the specific field
|
|
144
|
+
path(s) that are wrong for that connection's declared `system` — not the raw Zod union
|
|
145
|
+
error tree — so a broken `.dbcli` can be fixed without guessing which branch applies.
|
|
146
|
+
|
|
121
147
|
### list
|
|
122
148
|
|
|
123
149
|
List all tables (SQL), collections (MongoDB), keys (Redis), or indices (Elasticsearch).
|
|
@@ -197,6 +223,13 @@ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdou
|
|
|
197
223
|
**Options:** `--format <table|json|csv|html>`, `--ui` (open the dashboard in the system browser; implies `--format html`), `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`), `--fields <list>`, `--truncate <number>` / `--no-truncate`, `-f, --query-file <path>`, `--use <name[,name]>`, `--recovery`
|
|
198
224
|
**Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
|
|
199
225
|
|
|
226
|
+
Below `admin`, SQL holding more than one statement is rejected, because only the
|
|
227
|
+
first statement would decide the permission check while a driver on the simple
|
|
228
|
+
query protocol executes them all. Semicolons inside string literals, backtick
|
|
229
|
+
identifiers, and `#` comments are not separators. A MongoDB pipeline containing
|
|
230
|
+
`$out` or `$merge` requires `data-admin`, and is rejected outright on `export`,
|
|
231
|
+
in snippets, and in multi-connection fan-out.
|
|
232
|
+
|
|
200
233
|
#### Field projection (`--fields`)
|
|
201
234
|
|
|
202
235
|
```bash
|
|
@@ -253,7 +286,7 @@ instead of silently running its only connection.
|
|
|
253
286
|
An explicit comma-separated `--use primary,staging` fans one query out to several named
|
|
254
287
|
connections. `DBCLI_CONNECTION` always names one literal connection and never enables
|
|
255
288
|
fan-out. SQL permits `SELECT`, `SHOW`, `DESCRIBE`, and `EXPLAIN`; MongoDB permits filters and
|
|
256
|
-
read-only pipelines without
|
|
289
|
+
read-only pipelines without `$out` / `$merge`; Elasticsearch permits searches.
|
|
257
290
|
Redis, writes, `--recovery`, `--ui`, CSV, and HTML are rejected before execution. Each
|
|
258
291
|
connection keeps its own blacklist, limit metadata, audit entry, and error. Aggregate exit
|
|
259
292
|
codes are `0` when all succeed, `2` for mixed outcomes, and `1` when all fail or preflight
|
|
@@ -505,6 +538,13 @@ dbcli q @analytics/revenue --param days=30 --format html > report.html
|
|
|
505
538
|
|
|
506
539
|
Each `.sql` file is plain SQL with optional YAML frontmatter inside a leading `-- ---` block. Lines outside frontmatter form the SQL body.
|
|
507
540
|
|
|
541
|
+
Snippets are read-only by contract, at every permission level including `admin`.
|
|
542
|
+
A body must be a single statement opening with `SELECT` or `WITH` **and** free of
|
|
543
|
+
write or DDL keywords, so a data-modifying CTE (`WITH x AS (DELETE … RETURNING *)
|
|
544
|
+
SELECT * FROM x`) and `SELECT … INTO` are rejected at parse time rather than at
|
|
545
|
+
execution. A MongoDB body may not contain `$out` or `$merge`. The same rule
|
|
546
|
+
applies to `verify.query` in frontmatter, which `q --verify` executes verbatim.
|
|
547
|
+
|
|
508
548
|
```sql
|
|
509
549
|
-- ---
|
|
510
550
|
-- name: DAU
|
|
@@ -2605,6 +2645,8 @@ MongoDB connections use a JSON-based query model instead of SQL. Treat MongoDB s
|
|
|
2605
2645
|
|
|
2606
2646
|
`init --system mongodb` defaults to a field-by-field wizard (`host`, `srv`, `port`, `user`, `password` + `authSource`, then optional `replicaSet` / `tls`); a full `uri` is an explicit advanced choice in the interactive flow and the unchanged non-interactive path via `--uri`. Optional fields `authSource`, `replicaSet`, `tls`, and `srv` express what previously required embedding options in the `uri` query string. Atlas-style `mongodb+srv://` URIs are supported both as a full `uri` and via the per-field `srv: true` option. `list` and `query` run against the database configured for the connection, and `query` always requires `--collection <name>`.
|
|
2607
2647
|
|
|
2648
|
+
The 5000ms default server-selection timeout is often too tight for a connection over a VPN or to Atlas. Set a `timeout` field (ms) in the connection config, or override it per invocation with root-level `--timeout`, e.g. `dbcli --timeout 20000 --use <conn> list`.
|
|
2649
|
+
|
|
2608
2650
|
**Supported commands:** `init`, `use`, `list`, `schema`, `query`, `q`, `insert`, `update`, `delete`, `export`, `status`, `shell`, `doctor`, `upgrade`, `completion`
|
|
2609
2651
|
|
|
2610
2652
|
**Limited support:**
|
|
@@ -2669,7 +2711,7 @@ Redis connections speak Redis commands rather than SQL. The adapter uses Bun's n
|
|
|
2669
2711
|
|
|
2670
2712
|
- Required fields: `system: redis`, `host`, `port`. `password` and `database` are optional.
|
|
2671
2713
|
- `database` is the **logical DB index** (`"0"` … `"15"`), kept as a string to play nicely with env-ref bindings. `list` and the connection metadata both label it as the active DB.
|
|
2672
|
-
- `connection.timeout` (ms, default 5000) maps to the client's `connectionTimeout
|
|
2714
|
+
- `connection.timeout` (ms, default 5000) maps to the client's `connectionTimeout`; root-level `--timeout <ms>` overrides it for a single invocation.
|
|
2673
2715
|
|
|
2674
2716
|
### Permission classification
|
|
2675
2717
|
|
|
@@ -2799,7 +2841,7 @@ Elasticsearch connections speak the REST API. The adapter is fetch-based (no SDK
|
|
|
2799
2841
|
- Either `host` + `port` (default `https://localhost:9200`) or `nodes: [...]` (first node is used) or `cloudId`.
|
|
2800
2842
|
- Auth precedence: `apiKey` → `user`/`password` (HTTP Basic). Leave both unset for an open cluster.
|
|
2801
2843
|
- `protocol` defaults to `https`. For TLS quirks: `caPath` (path to a PEM bundle) and `rejectUnauthorized: false` (last resort).
|
|
2802
|
-
- `connection.timeout` (ms, default 5000) is wired to `AbortController` on every request.
|
|
2844
|
+
- `connection.timeout` (ms, default 5000) is wired to `AbortController` on every request; root-level `--timeout <ms>` overrides it for a single invocation.
|
|
2803
2845
|
|
|
2804
2846
|
### Permission classification
|
|
2805
2847
|
|
package/skills/dbcli/SKILL.md
CHANGED
|
@@ -77,7 +77,10 @@ When reporting a check's outcome use the vocabulary `verified` (evidence matched
|
|
|
77
77
|
`not_verified` (check ran and contradicted) / `indeterminate` (ran but ambiguous) /
|
|
78
78
|
`blocked` (could not run due to config, permission, schema, placeholder, or safety gate).
|
|
79
79
|
|
|
80
|
-
Prefer `--format json` for agent-friendly output.
|
|
80
|
+
Prefer `--format json` for agent-friendly output. Diagnostics (auto-limit notices,
|
|
81
|
+
warnings) go to stderr so stdout stays parseable — when piping JSON into a parser,
|
|
82
|
+
use `2>/dev/null` or leave stderr alone. **Never `2>&1`**: it merges those lines back
|
|
83
|
+
into stdout and the parse fails.
|
|
81
84
|
|
|
82
85
|
## Agent Task Packs
|
|
83
86
|
|
|
@@ -214,7 +217,10 @@ or `doctor` / `status` reports a missing or invalid config, follow this flow.
|
|
|
214
217
|
`--password` / `--name` (and `--system`).
|
|
215
218
|
3. **What permission tier?** Default to the **lowest** that satisfies the task:
|
|
216
219
|
`query-only` → `read-write` → `data-admin` → `admin`. Set with `--permission`
|
|
217
|
-
(defaults to `query-only`).
|
|
220
|
+
(defaults to `query-only`). Tiers judge what a statement does, not how it
|
|
221
|
+
opens: below `admin`, multi-statement SQL is rejected; snippets must be free
|
|
222
|
+
of write and DDL keywords; MongoDB `$out` / `$merge` need `data-admin` and are
|
|
223
|
+
refused entirely in snippets and `export`.
|
|
218
224
|
4. **Verify, never assume.** After init: `dbcli status` (system + permission +
|
|
219
225
|
blacklist summary, no creds) and `dbcli doctor --format json` (env, config
|
|
220
226
|
shape, connectivity, schema-cache age, Mongo SRV path).
|
|
@@ -553,4 +559,4 @@ schema. Raw `query` / `export` invocations render a sortable table only.
|
|
|
553
559
|
- Blacklisted tables and columns are redacted from query output.
|
|
554
560
|
- `schema` reports `estimatedRowCount` and `sizeCategory` (small / medium / large / huge). For large/huge tables add `WHERE` or `LIMIT` — bands in reference.md.
|
|
555
561
|
- `doctor` on `mongodb+srv://` reports whether SRV resolves natively or through the DoH fallback — useful when the runtime restricts DNS.
|
|
556
|
-
- **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
|
|
562
|
+
- **Global flags:** `--version`, `--config <path>`, `--global`, `--use <name>`, `--timeout <ms>`, `-v` / `--verbose` / `-vv`, `-q` / `--quiet`, `--no-color` (also honours `NO_COLOR`). Root-level flags must precede the command unless the command explicitly declares a command-level option.
|