@carllee1983/dbcli 1.48.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -6
- package/.cursor/rules/dbcli.mdc +1 -1
- package/.cursor/skills/dbcli/reference.md +9 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +1 -1
- package/.github/skills/dbcli/reference.md +9 -0
- package/CHANGELOG.md +28 -0
- package/assets/SKILL.md +1 -1
- package/assets/reference.md +9 -0
- package/assets/ui-template.html +10 -10
- package/dist/cli.mjs +156 -30
- package/dist/core.mjs +88 -9
- package/gemini-extension.json +1 -1
- package/package.json +4 -3
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +2 -6
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +1 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +9 -0
- package/skills/dbcli/SKILL.md +1 -1
- package/skills/dbcli/reference.md +9 -0
package/dist/cli.mjs
CHANGED
|
@@ -52,7 +52,7 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "@carllee1983/dbcli",
|
|
55
|
-
version: "1.
|
|
55
|
+
version: "1.49.0",
|
|
56
56
|
description: "Database CLI for AI agents",
|
|
57
57
|
type: "module",
|
|
58
58
|
publishConfig: {
|
|
@@ -133,8 +133,9 @@ var init_package = __esm(() => {
|
|
|
133
133
|
typecheck: "tsc --noEmit --pretty false",
|
|
134
134
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
135
135
|
lint: "eslint src tests scripts --ext .ts --max-warnings=0",
|
|
136
|
-
"
|
|
137
|
-
format:
|
|
136
|
+
"format:check": "prettier --check .",
|
|
137
|
+
format: "prettier --write .",
|
|
138
|
+
"lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0"
|
|
138
139
|
},
|
|
139
140
|
dependencies: {
|
|
140
141
|
"cli-table3": "^0.6.5",
|
|
@@ -30311,16 +30312,36 @@ function projectRows(rows, selection) {
|
|
|
30311
30312
|
const columnNames = collectColumnNames(projectedRows);
|
|
30312
30313
|
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
30313
30314
|
}
|
|
30314
|
-
function hasFieldPath(row,
|
|
30315
|
-
return readPath(row,
|
|
30315
|
+
function hasFieldPath(row, segments) {
|
|
30316
|
+
return readPath(row, segments).found;
|
|
30316
30317
|
}
|
|
30317
30318
|
function omitFieldPaths(rows, paths) {
|
|
30318
|
-
|
|
30319
|
-
|
|
30320
|
-
|
|
30321
|
-
|
|
30319
|
+
const topLevel = new Set(paths);
|
|
30320
|
+
const dotted = paths.filter((path5) => path5.includes(".")).map((path5) => ({ head: path5.slice(0, path5.indexOf(".")), segments: path5.split(".") }));
|
|
30321
|
+
const maskRecord = (row) => {
|
|
30322
|
+
let projected = cloneRecord(row, topLevel);
|
|
30323
|
+
for (const { head, segments } of dotted) {
|
|
30324
|
+
const value = row[head];
|
|
30325
|
+
if (value !== null && typeof value === "object")
|
|
30326
|
+
projected = omitPath(projected, segments);
|
|
30327
|
+
}
|
|
30322
30328
|
return projected;
|
|
30329
|
+
};
|
|
30330
|
+
return rows.map((row) => {
|
|
30331
|
+
if (row === null || typeof row !== "object")
|
|
30332
|
+
return {};
|
|
30333
|
+
if (Array.isArray(row))
|
|
30334
|
+
return maskArrayRow(row, maskRecord);
|
|
30335
|
+
return maskRecord(row);
|
|
30336
|
+
});
|
|
30337
|
+
}
|
|
30338
|
+
function maskArrayRow(row, maskRecord) {
|
|
30339
|
+
const out = {};
|
|
30340
|
+
row.forEach((item, index) => {
|
|
30341
|
+
const masked = Array.isArray(item) ? maskArrayRow(item, maskRecord) : item !== null && typeof item === "object" ? maskRecord(item) : item;
|
|
30342
|
+
defineData(out, String(index), masked);
|
|
30323
30343
|
});
|
|
30344
|
+
return out;
|
|
30324
30345
|
}
|
|
30325
30346
|
function toMongoProjection(selection) {
|
|
30326
30347
|
const projection = {};
|
|
@@ -30384,10 +30405,10 @@ function omitPath(value, segments) {
|
|
|
30384
30405
|
}
|
|
30385
30406
|
return out;
|
|
30386
30407
|
}
|
|
30387
|
-
function cloneRecord(value,
|
|
30408
|
+
function cloneRecord(value, omittedKeys) {
|
|
30388
30409
|
const out = {};
|
|
30389
30410
|
for (const [key, child] of Object.entries(value)) {
|
|
30390
|
-
if (key
|
|
30411
|
+
if (!omittedKeys.has(key))
|
|
30391
30412
|
defineData(out, key, child);
|
|
30392
30413
|
}
|
|
30393
30414
|
return out;
|
|
@@ -30526,6 +30547,16 @@ var MAX_DECODE_PASSES = 4;
|
|
|
30526
30547
|
var init_es_index_target = () => {};
|
|
30527
30548
|
|
|
30528
30549
|
// src/core/blacklist-validator.ts
|
|
30550
|
+
function flattenArrayRow(row) {
|
|
30551
|
+
const records = [];
|
|
30552
|
+
for (const item of row) {
|
|
30553
|
+
if (Array.isArray(item))
|
|
30554
|
+
records.push(...flattenArrayRow(item));
|
|
30555
|
+
else if (item !== null && typeof item === "object")
|
|
30556
|
+
records.push(item);
|
|
30557
|
+
}
|
|
30558
|
+
return records;
|
|
30559
|
+
}
|
|
30529
30560
|
function dedupe2(values) {
|
|
30530
30561
|
const seen = new Set;
|
|
30531
30562
|
const result = [];
|
|
@@ -30626,7 +30657,56 @@ class BlacklistValidator {
|
|
|
30626
30657
|
if (blacklistedColumns.length === 0) {
|
|
30627
30658
|
return { filteredRows: rows, omittedColumns: [] };
|
|
30628
30659
|
}
|
|
30629
|
-
const
|
|
30660
|
+
const probeNested = blacklistedColumns.some((path5) => path5.includes("."));
|
|
30661
|
+
const presentColumns = new Set(columnList);
|
|
30662
|
+
const nestedHeads = new Set;
|
|
30663
|
+
const collect2 = (record) => {
|
|
30664
|
+
for (const key of Object.getOwnPropertyNames(record)) {
|
|
30665
|
+
presentColumns.add(key);
|
|
30666
|
+
if (!probeNested)
|
|
30667
|
+
continue;
|
|
30668
|
+
const value = record[key];
|
|
30669
|
+
if (value !== null && typeof value === "object")
|
|
30670
|
+
nestedHeads.add(key);
|
|
30671
|
+
}
|
|
30672
|
+
};
|
|
30673
|
+
for (const row of rows) {
|
|
30674
|
+
if (row === null || typeof row !== "object")
|
|
30675
|
+
continue;
|
|
30676
|
+
if (Array.isArray(row)) {
|
|
30677
|
+
for (const item of flattenArrayRow(row))
|
|
30678
|
+
collect2(item);
|
|
30679
|
+
continue;
|
|
30680
|
+
}
|
|
30681
|
+
collect2(row);
|
|
30682
|
+
}
|
|
30683
|
+
const protectedPaths = new Set(blacklistedColumns);
|
|
30684
|
+
const omitted = new Set;
|
|
30685
|
+
for (const path5 of blacklistedColumns) {
|
|
30686
|
+
if (presentColumns.has(path5)) {
|
|
30687
|
+
omitted.add(path5);
|
|
30688
|
+
continue;
|
|
30689
|
+
}
|
|
30690
|
+
const dot = path5.indexOf(".");
|
|
30691
|
+
if (dot < 0)
|
|
30692
|
+
continue;
|
|
30693
|
+
if (!nestedHeads.has(path5.slice(0, dot)))
|
|
30694
|
+
continue;
|
|
30695
|
+
const segments = path5.split(".");
|
|
30696
|
+
if (rows.some((row) => hasFieldPath(row, segments)))
|
|
30697
|
+
omitted.add(path5);
|
|
30698
|
+
}
|
|
30699
|
+
for (const column of presentColumns) {
|
|
30700
|
+
let dot = column.indexOf(".");
|
|
30701
|
+
while (dot >= 0) {
|
|
30702
|
+
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
30703
|
+
omitted.add(column);
|
|
30704
|
+
break;
|
|
30705
|
+
}
|
|
30706
|
+
dot = column.indexOf(".", dot + 1);
|
|
30707
|
+
}
|
|
30708
|
+
}
|
|
30709
|
+
const omittedColumns = Array.from(omitted);
|
|
30630
30710
|
if (omittedColumns.length === 0) {
|
|
30631
30711
|
return { filteredRows: rows, omittedColumns: [] };
|
|
30632
30712
|
}
|
|
@@ -106230,6 +106310,10 @@ function fingerprintSql(sql) {
|
|
|
106230
106310
|
function shellEscapeDq(s) {
|
|
106231
106311
|
return s.replace(/\\/g, "\\\\").replace(/\$/g, "\\$").replace(/`/g, "\\`").replace(/"/g, "\\\"");
|
|
106232
106312
|
}
|
|
106313
|
+
function explainSuggestions(sql) {
|
|
106314
|
+
const esc = shellEscapeDq(sql);
|
|
106315
|
+
return [`dbcli explain "${esc}"`, `dbcli guide missing-index-for "${esc}"`];
|
|
106316
|
+
}
|
|
106233
106317
|
function buildByFingerprint(events, slowMs, top) {
|
|
106234
106318
|
const errorByFp = new Map;
|
|
106235
106319
|
for (const e of events.filter(isErrored)) {
|
|
@@ -106298,11 +106382,7 @@ function buildByFingerprint(events, slowMs, top) {
|
|
|
106298
106382
|
stats.sort((a, b) => b.durationMs.total - a.durationMs.total);
|
|
106299
106383
|
return stats.map((s, i) => {
|
|
106300
106384
|
if (i < top && s.statement === "SELECT") {
|
|
106301
|
-
|
|
106302
|
-
return {
|
|
106303
|
-
...s,
|
|
106304
|
-
suggestedCommands: [`dbcli explain "${sql}"`, `dbcli guide missing-index-for "${sql}"`]
|
|
106305
|
-
};
|
|
106385
|
+
return { ...s, suggestedCommands: explainSuggestions(s.exampleSql) };
|
|
106306
106386
|
}
|
|
106307
106387
|
return s;
|
|
106308
106388
|
});
|
|
@@ -106329,13 +106409,27 @@ function buildErrors(events) {
|
|
|
106329
106409
|
message: e.error.message,
|
|
106330
106410
|
count: 0,
|
|
106331
106411
|
fingerprint: fingerprintSql(e.sql),
|
|
106332
|
-
exampleSql: e.sql
|
|
106412
|
+
exampleSql: e.sql,
|
|
106413
|
+
tables: e.tables
|
|
106333
106414
|
};
|
|
106334
106415
|
groups.set(key, g);
|
|
106335
106416
|
}
|
|
106336
106417
|
g.count += 1;
|
|
106337
106418
|
}
|
|
106338
|
-
return [...groups.values()].sort((a, b) => b.count - a.count)
|
|
106419
|
+
return [...groups.values()].sort((a, b) => b.count - a.count).map((g) => {
|
|
106420
|
+
const schemaCmds = g.tables.slice(0, MAX_ERROR_SCHEMA_TABLES).map((t2) => `dbcli schema ${t2}`);
|
|
106421
|
+
const hint = `failed ${g.count}\xD7: verify the involved table/column names against the live schema ` + `before fixing the SQL \u2014 never guess column names`;
|
|
106422
|
+
return {
|
|
106423
|
+
code: g.code,
|
|
106424
|
+
message: g.message,
|
|
106425
|
+
count: g.count,
|
|
106426
|
+
fingerprint: g.fingerprint,
|
|
106427
|
+
exampleSql: g.exampleSql,
|
|
106428
|
+
tables: g.tables,
|
|
106429
|
+
...schemaCmds.length ? { suggestedCommands: schemaCmds } : {},
|
|
106430
|
+
hints: [hint]
|
|
106431
|
+
};
|
|
106432
|
+
});
|
|
106339
106433
|
}
|
|
106340
106434
|
function buildHotTables(events) {
|
|
106341
106435
|
const map = new Map;
|
|
@@ -106364,10 +106458,13 @@ function buildRepetition(events, threshold) {
|
|
|
106364
106458
|
fingerprint: fp,
|
|
106365
106459
|
sessionId: e.sessionId,
|
|
106366
106460
|
tables: e.tables,
|
|
106461
|
+
statement: e.statement,
|
|
106367
106462
|
count: 0,
|
|
106368
106463
|
totalDurationMs: 0,
|
|
106369
106464
|
minTs: ts,
|
|
106370
|
-
maxTs: ts
|
|
106465
|
+
maxTs: ts,
|
|
106466
|
+
exampleSql: e.sql,
|
|
106467
|
+
exampleDuration: e.durationMs
|
|
106371
106468
|
};
|
|
106372
106469
|
groups.set(key, g);
|
|
106373
106470
|
}
|
|
@@ -106377,15 +106474,27 @@ function buildRepetition(events, threshold) {
|
|
|
106377
106474
|
g.minTs = ts;
|
|
106378
106475
|
if (ts > g.maxTs)
|
|
106379
106476
|
g.maxTs = ts;
|
|
106477
|
+
if (e.durationMs > g.exampleDuration) {
|
|
106478
|
+
g.exampleDuration = e.durationMs;
|
|
106479
|
+
g.exampleSql = e.sql;
|
|
106480
|
+
}
|
|
106380
106481
|
}
|
|
106381
|
-
return [...groups.values()].filter((g) => g.count >= threshold).map((g) =>
|
|
106382
|
-
|
|
106383
|
-
|
|
106384
|
-
|
|
106385
|
-
|
|
106386
|
-
|
|
106387
|
-
|
|
106388
|
-
|
|
106482
|
+
return [...groups.values()].filter((g) => g.count >= threshold).sort((a, b) => b.count - a.count).map((g) => {
|
|
106483
|
+
const spanMs = g.maxTs - g.minTs;
|
|
106484
|
+
const hint = `N+1 suspect: the same query ran ${g.count}\xD7 within one session over ${spanMs}ms \u2014 ` + `collapse into a single query (JOIN / IN (...)) or cache the result`;
|
|
106485
|
+
return {
|
|
106486
|
+
fingerprint: g.fingerprint,
|
|
106487
|
+
sessionId: g.sessionId,
|
|
106488
|
+
count: g.count,
|
|
106489
|
+
spanMs,
|
|
106490
|
+
totalDurationMs: g.totalDurationMs,
|
|
106491
|
+
tables: g.tables,
|
|
106492
|
+
statement: g.statement,
|
|
106493
|
+
exampleSql: g.exampleSql,
|
|
106494
|
+
...g.statement === "SELECT" ? { suggestedCommands: explainSuggestions(g.exampleSql) } : {},
|
|
106495
|
+
hints: [hint]
|
|
106496
|
+
};
|
|
106497
|
+
});
|
|
106389
106498
|
}
|
|
106390
106499
|
function analyzeEvents(events, opts) {
|
|
106391
106500
|
const timestamps = events.map((e) => e.timestamp).filter(Boolean).sort();
|
|
@@ -106439,7 +106548,7 @@ function buildSummary(events, slowMs) {
|
|
|
106439
106548
|
}
|
|
106440
106549
|
};
|
|
106441
106550
|
}
|
|
106442
|
-
var isCompleted = (e) => e.type === "query_completed", isErrored = (e) => e.type === "query_errored";
|
|
106551
|
+
var isCompleted = (e) => e.type === "query_completed", isErrored = (e) => e.type === "query_errored", MAX_ERROR_SCHEMA_TABLES = 3;
|
|
106443
106552
|
var init_analyze = __esm(() => {
|
|
106444
106553
|
init_sql_metadata();
|
|
106445
106554
|
});
|
|
@@ -106479,12 +106588,29 @@ function renderAnalysisText(report, top) {
|
|
|
106479
106588
|
for (const r of report.repetition.slice(0, top)) {
|
|
106480
106589
|
L2.push(` ${r.count}x in session ${r.sessionId} (${r.spanMs}ms) ${r.fingerprint}`);
|
|
106481
106590
|
}
|
|
106482
|
-
const cmds = [
|
|
106591
|
+
const cmds = [
|
|
106592
|
+
...new Set([
|
|
106593
|
+
...report.byFingerprint.flatMap((f) => f.suggestedCommands ?? []),
|
|
106594
|
+
...report.errors.flatMap((e) => e.suggestedCommands ?? []),
|
|
106595
|
+
...report.repetition.flatMap((r) => r.suggestedCommands ?? [])
|
|
106596
|
+
])
|
|
106597
|
+
];
|
|
106483
106598
|
if (cmds.length) {
|
|
106484
106599
|
L2.push("", "SUGGESTED COMMANDS");
|
|
106485
106600
|
for (const c2 of cmds)
|
|
106486
106601
|
L2.push(` ${c2}`);
|
|
106487
106602
|
}
|
|
106603
|
+
const hints = [
|
|
106604
|
+
...new Set([
|
|
106605
|
+
...report.errors.flatMap((e) => e.hints ?? []),
|
|
106606
|
+
...report.repetition.flatMap((r) => r.hints ?? [])
|
|
106607
|
+
])
|
|
106608
|
+
];
|
|
106609
|
+
if (hints.length) {
|
|
106610
|
+
L2.push("", "HINTS");
|
|
106611
|
+
for (const h of hints)
|
|
106612
|
+
L2.push(` ${h}`);
|
|
106613
|
+
}
|
|
106488
106614
|
return L2.join(`
|
|
106489
106615
|
`);
|
|
106490
106616
|
}
|
package/dist/core.mjs
CHANGED
|
@@ -23219,17 +23219,37 @@ function projectRows(rows, selection) {
|
|
|
23219
23219
|
const columnNames = collectColumnNames(projectedRows);
|
|
23220
23220
|
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
23221
23221
|
}
|
|
23222
|
-
function hasFieldPath(row,
|
|
23223
|
-
return readPath(row,
|
|
23222
|
+
function hasFieldPath(row, segments) {
|
|
23223
|
+
return readPath(row, segments).found;
|
|
23224
23224
|
}
|
|
23225
23225
|
function omitFieldPaths(rows, paths) {
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23226
|
+
const topLevel = new Set(paths);
|
|
23227
|
+
const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: path.slice(0, path.indexOf(".")), segments: path.split(".") }));
|
|
23228
|
+
const maskRecord = (row) => {
|
|
23229
|
+
let projected = cloneRecord(row, topLevel);
|
|
23230
|
+
for (const { head, segments } of dotted) {
|
|
23231
|
+
const value = row[head];
|
|
23232
|
+
if (value !== null && typeof value === "object")
|
|
23233
|
+
projected = omitPath(projected, segments);
|
|
23234
|
+
}
|
|
23230
23235
|
return projected;
|
|
23236
|
+
};
|
|
23237
|
+
return rows.map((row) => {
|
|
23238
|
+
if (row === null || typeof row !== "object")
|
|
23239
|
+
return {};
|
|
23240
|
+
if (Array.isArray(row))
|
|
23241
|
+
return maskArrayRow(row, maskRecord);
|
|
23242
|
+
return maskRecord(row);
|
|
23231
23243
|
});
|
|
23232
23244
|
}
|
|
23245
|
+
function maskArrayRow(row, maskRecord) {
|
|
23246
|
+
const out = {};
|
|
23247
|
+
row.forEach((item, index) => {
|
|
23248
|
+
const masked = Array.isArray(item) ? maskArrayRow(item, maskRecord) : item !== null && typeof item === "object" ? maskRecord(item) : item;
|
|
23249
|
+
defineData(out, String(index), masked);
|
|
23250
|
+
});
|
|
23251
|
+
return out;
|
|
23252
|
+
}
|
|
23233
23253
|
function readPath(value, segments) {
|
|
23234
23254
|
if (segments.length === 0)
|
|
23235
23255
|
return { found: true, value };
|
|
@@ -23273,10 +23293,10 @@ function omitPath(value, segments) {
|
|
|
23273
23293
|
}
|
|
23274
23294
|
return out;
|
|
23275
23295
|
}
|
|
23276
|
-
function cloneRecord(value,
|
|
23296
|
+
function cloneRecord(value, omittedKeys) {
|
|
23277
23297
|
const out = {};
|
|
23278
23298
|
for (const [key, child] of Object.entries(value)) {
|
|
23279
|
-
if (key
|
|
23299
|
+
if (!omittedKeys.has(key))
|
|
23280
23300
|
defineData(out, key, child);
|
|
23281
23301
|
}
|
|
23282
23302
|
return out;
|
|
@@ -24116,6 +24136,16 @@ function matchesIndexGlob(pattern, name) {
|
|
|
24116
24136
|
}
|
|
24117
24137
|
|
|
24118
24138
|
// src/core/blacklist-validator.ts
|
|
24139
|
+
function flattenArrayRow(row) {
|
|
24140
|
+
const records = [];
|
|
24141
|
+
for (const item of row) {
|
|
24142
|
+
if (Array.isArray(item))
|
|
24143
|
+
records.push(...flattenArrayRow(item));
|
|
24144
|
+
else if (item !== null && typeof item === "object")
|
|
24145
|
+
records.push(item);
|
|
24146
|
+
}
|
|
24147
|
+
return records;
|
|
24148
|
+
}
|
|
24119
24149
|
function dedupe2(values) {
|
|
24120
24150
|
const seen = new Set;
|
|
24121
24151
|
const result = [];
|
|
@@ -24216,7 +24246,56 @@ class BlacklistValidator {
|
|
|
24216
24246
|
if (blacklistedColumns.length === 0) {
|
|
24217
24247
|
return { filteredRows: rows, omittedColumns: [] };
|
|
24218
24248
|
}
|
|
24219
|
-
const
|
|
24249
|
+
const probeNested = blacklistedColumns.some((path3) => path3.includes("."));
|
|
24250
|
+
const presentColumns = new Set(columnList);
|
|
24251
|
+
const nestedHeads = new Set;
|
|
24252
|
+
const collect = (record) => {
|
|
24253
|
+
for (const key of Object.getOwnPropertyNames(record)) {
|
|
24254
|
+
presentColumns.add(key);
|
|
24255
|
+
if (!probeNested)
|
|
24256
|
+
continue;
|
|
24257
|
+
const value = record[key];
|
|
24258
|
+
if (value !== null && typeof value === "object")
|
|
24259
|
+
nestedHeads.add(key);
|
|
24260
|
+
}
|
|
24261
|
+
};
|
|
24262
|
+
for (const row of rows) {
|
|
24263
|
+
if (row === null || typeof row !== "object")
|
|
24264
|
+
continue;
|
|
24265
|
+
if (Array.isArray(row)) {
|
|
24266
|
+
for (const item of flattenArrayRow(row))
|
|
24267
|
+
collect(item);
|
|
24268
|
+
continue;
|
|
24269
|
+
}
|
|
24270
|
+
collect(row);
|
|
24271
|
+
}
|
|
24272
|
+
const protectedPaths = new Set(blacklistedColumns);
|
|
24273
|
+
const omitted = new Set;
|
|
24274
|
+
for (const path3 of blacklistedColumns) {
|
|
24275
|
+
if (presentColumns.has(path3)) {
|
|
24276
|
+
omitted.add(path3);
|
|
24277
|
+
continue;
|
|
24278
|
+
}
|
|
24279
|
+
const dot = path3.indexOf(".");
|
|
24280
|
+
if (dot < 0)
|
|
24281
|
+
continue;
|
|
24282
|
+
if (!nestedHeads.has(path3.slice(0, dot)))
|
|
24283
|
+
continue;
|
|
24284
|
+
const segments = path3.split(".");
|
|
24285
|
+
if (rows.some((row) => hasFieldPath(row, segments)))
|
|
24286
|
+
omitted.add(path3);
|
|
24287
|
+
}
|
|
24288
|
+
for (const column of presentColumns) {
|
|
24289
|
+
let dot = column.indexOf(".");
|
|
24290
|
+
while (dot >= 0) {
|
|
24291
|
+
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
24292
|
+
omitted.add(column);
|
|
24293
|
+
break;
|
|
24294
|
+
}
|
|
24295
|
+
dot = column.indexOf(".", dot + 1);
|
|
24296
|
+
}
|
|
24297
|
+
}
|
|
24298
|
+
const omittedColumns = Array.from(omitted);
|
|
24220
24299
|
if (omittedColumns.length === 0) {
|
|
24221
24300
|
return { filteredRows: rows, omittedColumns: [] };
|
|
24222
24301
|
}
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carllee1983/dbcli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
4
4
|
"description": "Database CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -81,8 +81,9 @@
|
|
|
81
81
|
"typecheck": "tsc --noEmit --pretty false",
|
|
82
82
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
83
83
|
"lint": "eslint src tests scripts --ext .ts --max-warnings=0",
|
|
84
|
-
"
|
|
85
|
-
"format": "prettier --write
|
|
84
|
+
"format:check": "prettier --check .",
|
|
85
|
+
"format": "prettier --write .",
|
|
86
|
+
"lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0"
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
89
|
"cli-table3": "^0.6.5",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbcli-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
4
4
|
"description": "Database CLI skill and command reference for AI agents",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carl Lee",
|
|
@@ -26,11 +26,7 @@
|
|
|
26
26
|
"longDescription": "Installs the dbcli agent skill and reference guide so Codex can inspect schemas, query databases, respect blacklist boundaries, and recover from database errors through the dbcli command workflow.",
|
|
27
27
|
"developerName": "Carl Lee",
|
|
28
28
|
"category": "Productivity",
|
|
29
|
-
"capabilities": [
|
|
30
|
-
"Database",
|
|
31
|
-
"Local CLI",
|
|
32
|
-
"Agent Skill"
|
|
33
|
-
],
|
|
29
|
+
"capabilities": ["Database", "Local CLI", "Agent Skill"],
|
|
34
30
|
"defaultPrompt": [
|
|
35
31
|
"Inspect my database with dbcli.",
|
|
36
32
|
"Show schema safely with dbcli.",
|
|
@@ -363,7 +363,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
|
|
|
363
363
|
| `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>`. |
|
|
364
364
|
| `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. |
|
|
365
365
|
| `backfill artifact` | n/a | Build a bounded, reviewable source-to-SQL backfill artifact from JSON. Includes source/target identity, blacklist/schema preflight, read-back verification, and rollback hints; dry-run only and never executes writes. |
|
|
366
|
-
| `proxy` | n/a | **(v1.26)** MySQL/MariaDB/PostgreSQL only. Local-dev observability proxy — relays app traffic to the real DB and appends query/latency/byte/error events to `.dbcli/proxy/events.jsonl`. Subcommands: `mysql` \| `mariadb` \| `postgresql`. `--listen`, `--target`, `--events
|
|
366
|
+
| `proxy` | n/a | **(v1.26)** MySQL/MariaDB/PostgreSQL only. Local-dev observability proxy — relays app traffic to the real DB and appends query/latency/byte/error events to `.dbcli/proxy/events.jsonl`. Subcommands: `mysql` \| `mariadb` \| `postgresql`. `--listen`, `--target`, `--events` (default `.dbcli/proxy/events.jsonl`), `--slow-ms` (default `1000`), `--redact none\|literals` (default `none`). Observe-only. **(v1.27)** `proxy analyze` aggregates the event log offline into a JSON/text report (summary, byFingerprint, slowest, errors, hotTables, N+1) — `--format`, `--top`, `--slow-ms`, `--n-plus-one`; errors out if no events exist yet. Actionable blocks carry `suggestedCommands` + `hints` so an agent can act: SELECT hotspots/N+1 → `explain` / `guide missing-index-for`, errors → `schema <table>` (verify names, never guess), N+1 → batch (JOIN / `IN (...)`). After analyzing, run each finding's `suggestedCommands`, read its `hints`, then propose the fix. |
|
|
367
367
|
| `status` | query-only+ | Safe JSON/text summary (no credentials). |
|
|
368
368
|
| `inspect` | query-only+ | Read-only context snapshot (connection, permission, blacklist, objects, snippets, context-aware `suggestedCommands`, and **(v1.23)** human-readable `hints`). `--for-agent` / `--brief` / `--no-connect` / `--require-schema-cache`. Supports `--recovery`. |
|
|
369
369
|
| `report` | query-only+ | Diagnostic report built from `@diag/*` snippets. `--section <health\|capacity\|perf>` (comma-separated to combine), `--brief`, `--for-agent`, `--no-connect`. |
|
|
@@ -1202,6 +1202,15 @@ dbcli proxy analyze --slow-ms 200 --n-plus-one 5 # custom thresholds
|
|
|
1202
1202
|
|
|
1203
1203
|
**`proxy analyze`** — offline aggregation of the event log (no DB). Flags: `--events <path>` (default `.dbcli/proxy/events.jsonl`), `--format json|text` (default `json`), `--top <n>` (default 20; text rows + suggestedCommands depth), `--slow-ms <ms>` (default 1000; recomputes slowCount), `--n-plus-one <n>` (default 10), `--no-include-rotated`. JSON report blocks: `summary`, `byFingerprint` (sorted by total time; SELECT entries in the top-N carry `suggestedCommands` for `explain` / `guide missing-index-for`), `slowest`, `errors`, `hotTables`, `repetition` (N+1 suspects). Reads the current log plus the rotated `.1` segment by default.
|
|
1204
1204
|
|
|
1205
|
+
Every actionable block carries machine-readable next steps so an agent can move from "what is wrong" to "what to run":
|
|
1206
|
+
- `byFingerprint[]` — top-N SELECT entries get `suggestedCommands`: `dbcli explain "<sql>"` and `dbcli guide missing-index-for "<sql>"`.
|
|
1207
|
+
- `errors[]` — carries `tables`; emits `suggestedCommands` of `dbcli schema <table>` (capped at the first 3 tables) plus a `hints` note to verify table/column names before fixing (never guess column names). No tables known (e.g. a syntax error) → no `suggestedCommands`, but the hint still appears.
|
|
1208
|
+
- `repetition[]` — carries `statement` and a runnable `exampleSql` (the slowest occurrence). SELECT N+1 groups get `explain` / `guide missing-index-for` `suggestedCommands`; every group carries a `hints` note suggesting batching (JOIN / `IN (...)`) or caching.
|
|
1209
|
+
|
|
1210
|
+
`suggestedCommands` are emitted as strings only — `proxy analyze` never executes them. When the proxy ran with `--redact literals`, `exampleSql` (and therefore the suggested commands) contains `?` placeholders; fill in real values before running them.
|
|
1211
|
+
|
|
1212
|
+
**Acting on the report (agent loop):** after `proxy analyze`, for each block read `hints` for the diagnosis, run the entry's `suggestedCommands` to gather schema/plan/index evidence, then propose a concrete fix — add an index (`guide missing-index-for`), rewrite a slow SELECT (`explain`), batch an N+1 (`repetition`), or correct a column/table name (`errors` → `schema`). The text format mirrors this with aggregated `SUGGESTED COMMANDS` and `HINTS` sections; JSON keeps the suggestions attached per-finding.
|
|
1213
|
+
|
|
1205
1214
|
**Engines:** MySQL / MariaDB / PostgreSQL
|
|
1206
1215
|
**Permission:** n/a (acts as a TCP relay; does not use dbcli's SQL permission model)
|
|
1207
1216
|
|
package/skills/dbcli/SKILL.md
CHANGED
|
@@ -363,7 +363,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
|
|
|
363
363
|
| `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>`. |
|
|
364
364
|
| `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. |
|
|
365
365
|
| `backfill artifact` | n/a | Build a bounded, reviewable source-to-SQL backfill artifact from JSON. Includes source/target identity, blacklist/schema preflight, read-back verification, and rollback hints; dry-run only and never executes writes. |
|
|
366
|
-
| `proxy` | n/a | **(v1.26)** MySQL/MariaDB/PostgreSQL only. Local-dev observability proxy — relays app traffic to the real DB and appends query/latency/byte/error events to `.dbcli/proxy/events.jsonl`. Subcommands: `mysql` \| `mariadb` \| `postgresql`. `--listen`, `--target`, `--events
|
|
366
|
+
| `proxy` | n/a | **(v1.26)** MySQL/MariaDB/PostgreSQL only. Local-dev observability proxy — relays app traffic to the real DB and appends query/latency/byte/error events to `.dbcli/proxy/events.jsonl`. Subcommands: `mysql` \| `mariadb` \| `postgresql`. `--listen`, `--target`, `--events` (default `.dbcli/proxy/events.jsonl`), `--slow-ms` (default `1000`), `--redact none\|literals` (default `none`). Observe-only. **(v1.27)** `proxy analyze` aggregates the event log offline into a JSON/text report (summary, byFingerprint, slowest, errors, hotTables, N+1) — `--format`, `--top`, `--slow-ms`, `--n-plus-one`; errors out if no events exist yet. Actionable blocks carry `suggestedCommands` + `hints` so an agent can act: SELECT hotspots/N+1 → `explain` / `guide missing-index-for`, errors → `schema <table>` (verify names, never guess), N+1 → batch (JOIN / `IN (...)`). After analyzing, run each finding's `suggestedCommands`, read its `hints`, then propose the fix. |
|
|
367
367
|
| `status` | query-only+ | Safe JSON/text summary (no credentials). |
|
|
368
368
|
| `inspect` | query-only+ | Read-only context snapshot (connection, permission, blacklist, objects, snippets, context-aware `suggestedCommands`, and **(v1.23)** human-readable `hints`). `--for-agent` / `--brief` / `--no-connect` / `--require-schema-cache`. Supports `--recovery`. |
|
|
369
369
|
| `report` | query-only+ | Diagnostic report built from `@diag/*` snippets. `--section <health\|capacity\|perf>` (comma-separated to combine), `--brief`, `--for-agent`, `--no-connect`. |
|
|
@@ -1202,6 +1202,15 @@ dbcli proxy analyze --slow-ms 200 --n-plus-one 5 # custom thresholds
|
|
|
1202
1202
|
|
|
1203
1203
|
**`proxy analyze`** — offline aggregation of the event log (no DB). Flags: `--events <path>` (default `.dbcli/proxy/events.jsonl`), `--format json|text` (default `json`), `--top <n>` (default 20; text rows + suggestedCommands depth), `--slow-ms <ms>` (default 1000; recomputes slowCount), `--n-plus-one <n>` (default 10), `--no-include-rotated`. JSON report blocks: `summary`, `byFingerprint` (sorted by total time; SELECT entries in the top-N carry `suggestedCommands` for `explain` / `guide missing-index-for`), `slowest`, `errors`, `hotTables`, `repetition` (N+1 suspects). Reads the current log plus the rotated `.1` segment by default.
|
|
1204
1204
|
|
|
1205
|
+
Every actionable block carries machine-readable next steps so an agent can move from "what is wrong" to "what to run":
|
|
1206
|
+
- `byFingerprint[]` — top-N SELECT entries get `suggestedCommands`: `dbcli explain "<sql>"` and `dbcli guide missing-index-for "<sql>"`.
|
|
1207
|
+
- `errors[]` — carries `tables`; emits `suggestedCommands` of `dbcli schema <table>` (capped at the first 3 tables) plus a `hints` note to verify table/column names before fixing (never guess column names). No tables known (e.g. a syntax error) → no `suggestedCommands`, but the hint still appears.
|
|
1208
|
+
- `repetition[]` — carries `statement` and a runnable `exampleSql` (the slowest occurrence). SELECT N+1 groups get `explain` / `guide missing-index-for` `suggestedCommands`; every group carries a `hints` note suggesting batching (JOIN / `IN (...)`) or caching.
|
|
1209
|
+
|
|
1210
|
+
`suggestedCommands` are emitted as strings only — `proxy analyze` never executes them. When the proxy ran with `--redact literals`, `exampleSql` (and therefore the suggested commands) contains `?` placeholders; fill in real values before running them.
|
|
1211
|
+
|
|
1212
|
+
**Acting on the report (agent loop):** after `proxy analyze`, for each block read `hints` for the diagnosis, run the entry's `suggestedCommands` to gather schema/plan/index evidence, then propose a concrete fix — add an index (`guide missing-index-for`), rewrite a slow SELECT (`explain`), batch an N+1 (`repetition`), or correct a column/table name (`errors` → `schema`). The text format mirrors this with aggregated `SUGGESTED COMMANDS` and `HINTS` sections; JSON keeps the suggestions attached per-finding.
|
|
1213
|
+
|
|
1205
1214
|
**Engines:** MySQL / MariaDB / PostgreSQL
|
|
1206
1215
|
**Permission:** n/a (acts as a TCP relay; does not use dbcli's SQL permission model)
|
|
1207
1216
|
|