@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/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.48.0",
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
- "lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0",
137
- format: 'prettier --write "src/**/*.ts" "tests/**/*.ts"'
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, path5) {
30315
- return readPath(row, path5.split(".")).found;
30315
+ function hasFieldPath(row, segments) {
30316
+ return readPath(row, segments).found;
30316
30317
  }
30317
30318
  function omitFieldPaths(rows, paths) {
30318
- return rows.map((row) => {
30319
- let projected = cloneRecord(row);
30320
- for (const path5 of paths)
30321
- projected = omitPath(projected, path5.split("."));
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, omittedKey) {
30408
+ function cloneRecord(value, omittedKeys) {
30388
30409
  const out = {};
30389
30410
  for (const [key, child] of Object.entries(value)) {
30390
- if (key !== omittedKey)
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 omittedColumns = blacklistedColumns.filter((path5) => columnList.includes(path5) || rows.some((row) => hasFieldPath(row, path5)));
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
- const sql = shellEscapeDq(s.exampleSql);
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
- fingerprint: g.fingerprint,
106383
- sessionId: g.sessionId,
106384
- count: g.count,
106385
- spanMs: g.maxTs - g.minTs,
106386
- totalDurationMs: g.totalDurationMs,
106387
- tables: g.tables
106388
- })).sort((a, b) => b.count - a.count);
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 = [...new Set(report.byFingerprint.flatMap((f) => f.suggestedCommands ?? []))];
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, path) {
23223
- return readPath(row, path.split(".")).found;
23222
+ function hasFieldPath(row, segments) {
23223
+ return readPath(row, segments).found;
23224
23224
  }
23225
23225
  function omitFieldPaths(rows, paths) {
23226
- return rows.map((row) => {
23227
- let projected = cloneRecord(row);
23228
- for (const path of paths)
23229
- projected = omitPath(projected, path.split("."));
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, omittedKey) {
23296
+ function cloneRecord(value, omittedKeys) {
23277
23297
  const out = {};
23278
23298
  for (const [key, child] of Object.entries(value)) {
23279
- if (key !== omittedKey)
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 omittedColumns = blacklistedColumns.filter((path3) => columnList.includes(path3) || rows.some((row) => hasFieldPath(row, path3)));
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbcli-agent",
3
- "version": "1.48.0",
3
+ "version": "1.49.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.48.0",
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
- "lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0",
85
- "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\""
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.48.0",
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`, `--slow-ms` (default `1000`), `--redact none\|literals`. Observe-only. **(v1.27)** `proxy analyze` aggregates the event log offline into a JSON/text report (summary, byFingerprint with suggestedCommands, slowest, errors, hotTables, N+1) — errors out if no events exist yet. |
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
 
@@ -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`, `--slow-ms` (default `1000`), `--redact none\|literals`. Observe-only. **(v1.27)** `proxy analyze` aggregates the event log offline into a JSON/text report (summary, byFingerprint with suggestedCommands, slowest, errors, hotTables, N+1) — errors out if no events exist yet. |
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