@deepsql/mcp 0.16.0 → 0.17.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.md CHANGED
@@ -33,7 +33,7 @@ server, and the statement you ran.** Don't be sloppy.
33
33
 
34
34
  ## The tools you have
35
35
 
36
- The MCP server exposes 12 tools. They all take a `connectionId` (UUID
36
+ The MCP server exposes 14 tools. They all take a `connectionId` (UUID
37
37
  returned by `list_connections`) except `apply_index_recommendation`,
38
38
  which takes a server-resolved `recommendationId`.
39
39
 
@@ -47,6 +47,8 @@ which takes a server-resolved `recommendationId`.
47
47
  | `get_relationships` | Inferred + validated foreign keys with confidence scores. Many real DBs lack declared FKs; this fills the gap. |
48
48
  | `get_anti_patterns` | Schema-level (`kind=table`) or query-level (`kind=query`) anti-patterns. |
49
49
  | `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples. Read-only; doesn't trigger new work. |
50
+ | `get_slow_query_timeline` | Day-by-day timeline for one query from the 30-day analytics store — call count, mean/max time, regression factor per day. Identify the query by its fingerprint (the `queryId` from `analyze_slow_queries`). Answers "is this query getting slower". |
51
+ | `get_query_regressions` | Slow queries that regressed (got slower) on the latest daily analysis run, ranked by slowdown factor. Read-only. |
50
52
  | `get_index_recommendations` | **Workload-weighted DBA-grade index advisor.** Pre-computed top-N (default 5) recommendations ranked by net benefit (`Σ calls × mean_exec_time` − write-cost). Each result carries up to 5 contributing query fingerprints, the role each column played, and optional HypoPG cost-delta on Postgres. Covers both `CREATE_INDEX` and `DROP_INDEX` (unused + redundant-prefix) candidates. |
51
53
  | **`apply_index_recommendation`** | **The only write-capable MCP tool.** Apply (or dry-run) a recommendation against its target connection and measure the before/after benefit on contributing queries. `DRY_RUN` (default) uses HypoPG (Postgres-only) for zero-write cost-delta. `APPLY` runs real `CREATE/DROP INDEX CONCURRENTLY` (configurable via `concurrent`). `APPLY_AND_MEASURE` additionally runs `EXPLAIN ANALYZE` for wall-clock timings. Write modes require `confirm: true`. The DDL is server-generated from the recommendation row — clients never supply SQL. |
52
54
  | **`execute_sql`** | **Run any SQL statement.** Policy is server-enforced: developers can run SELECT/WITH/SHOW/EXPLAIN; admins can also run DML/DDL with a two-step confirmation. EXPLAIN and EXPLAIN ANALYZE are just SQL — no separate flag. |
@@ -217,6 +217,107 @@ const TOOL_DEFINITIONS = [
217
217
  additionalProperties: false,
218
218
  },
219
219
  },
220
+ {
221
+ name: "get_slow_query_timeline",
222
+ description:
223
+ "Get the day-by-day timeline for one slow query from DeepSQL's 30-day analytics store. "
224
+ + "The query is identified by its stable fingerprint (the MD5 of the normalized query — "
225
+ + "the `queryId` field returned by analyze_slow_queries). Returns one point per day with "
226
+ + "call count, mean/max execution time, and the regression factor versus the previous day. "
227
+ + "Use this to answer 'is this query getting slower over time'.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
232
+ fingerprint: {
233
+ type: "string",
234
+ description: "Stable query fingerprint (MD5 of the normalized query).",
235
+ },
236
+ },
237
+ required: ["connectionId", "fingerprint"],
238
+ additionalProperties: false,
239
+ },
240
+ },
241
+ {
242
+ name: "get_query_regressions",
243
+ description:
244
+ "List slow queries that regressed (got slower) on the most recent daily analysis run. "
245
+ + "Each result carries the fingerprint, normalized SQL, current mean execution time, and "
246
+ + "the regression factor (this period's mean / the previous period's mean). Read-only.",
247
+ inputSchema: {
248
+ type: "object",
249
+ properties: {
250
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
251
+ minFactor: {
252
+ type: "number",
253
+ minimum: 1,
254
+ description: "Minimum slowdown multiple to report. Defaults to 1.5 (≥50% slower).",
255
+ },
256
+ },
257
+ required: ["connectionId"],
258
+ additionalProperties: false,
259
+ },
260
+ },
261
+ {
262
+ name: "get_table_growth",
263
+ description:
264
+ "Get table size / row-count growth trends for a connection from DeepSQL's persistent stats history. "
265
+ + "Returns three parallel time series (sizeOverTime, growthOverTime, rowCountOverTime) plus per-table "
266
+ + "headline rollups suitable for answering questions like \"which tables are growing fastest?\" or "
267
+ + "\"how much has `orders` grown in the last month?\". Backed by snapshots stored in DeepSQL's "
268
+ + "`table_stats_history`, not live `pg_total_relation_size()` probes — so it can show growth velocity "
269
+ + "and bloat over time without re-scanning the customer's database.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {
273
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
274
+ tableName: {
275
+ type: "string",
276
+ description: "Optional table name to scope the trends to a single table. Omit for all tables.",
277
+ },
278
+ days: {
279
+ type: "integer",
280
+ minimum: 1,
281
+ maximum: 365,
282
+ description: "Lookback window in days. Defaults to 30.",
283
+ },
284
+ },
285
+ required: ["connectionId"],
286
+ additionalProperties: false,
287
+ },
288
+ },
289
+ {
290
+ name: "get_growth_anomalies",
291
+ description:
292
+ "Get growth anomalies DeepSQL flagged on a connection — sudden size or row spikes that exceeded the "
293
+ + "configured thresholds (percentage growth, absolute byte growth, statistical z-score). Each anomaly "
294
+ + "carries severity (CRITICAL / WARNING / INFO), the before/after sizes, an anomaly type "
295
+ + "(PERCENTAGE_GROWTH, ABSOLUTE_GROWTH, STATISTICAL_ANOMALY, ROW_SPIKE, NEW_TABLE), a human-readable "
296
+ + "description, and a confidence score. Use this BEFORE walking the user through a plan to optimize a "
297
+ + "table — the anomaly may be the root cause they should investigate first.",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
302
+ tableName: {
303
+ type: "string",
304
+ description: "Optional table name to scope to one table. Omit for the whole connection.",
305
+ },
306
+ unacknowledgedOnly: {
307
+ type: "boolean",
308
+ description: "When true, only return anomalies the operator hasn't acked yet. Defaults to false.",
309
+ },
310
+ days: {
311
+ type: "integer",
312
+ minimum: 1,
313
+ maximum: 365,
314
+ description: "Lookback window in days. Defaults to 30.",
315
+ },
316
+ },
317
+ required: ["connectionId"],
318
+ additionalProperties: false,
319
+ },
320
+ },
220
321
  {
221
322
  name: "execute_sql",
222
323
  description:
@@ -740,6 +841,90 @@ function summarizeSlowQueries(payload) {
740
841
  + parts.join("");
741
842
  }
742
843
 
844
+ function summarizeTableGrowth(payload) {
845
+ // Backend returns { success, trends: { sizeOverTime[], growthOverTime[],
846
+ // rowCountOverTime[] }, days }. We don't want to dump the raw arrays into
847
+ // the agent's context — collapse to a per-table headline and a top-3
848
+ // "growing fastest" list.
849
+ const trends = payload?.trends || {};
850
+ const sizeOverTime = Array.isArray(trends.sizeOverTime) ? trends.sizeOverTime : [];
851
+ if (sizeOverTime.length === 0) {
852
+ return "No table-growth history for this connection in the requested window. "
853
+ + "The customer may not have stats-snapshot collection enabled yet.";
854
+ }
855
+
856
+ // Roll up per-table: first vs last snapshot.
857
+ const byTable = new Map();
858
+ for (const point of sizeOverTime) {
859
+ const t = point.table || "(unknown)";
860
+ if (!byTable.has(t)) byTable.set(t, []);
861
+ byTable.get(t).push(point);
862
+ }
863
+ const rows = [];
864
+ for (const [table, points] of byTable.entries()) {
865
+ points.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
866
+ const first = points[0];
867
+ const last = points[points.length - 1];
868
+ const firstBytes = first?.sizeBytes ?? 0;
869
+ const lastBytes = last?.sizeBytes ?? 0;
870
+ const deltaBytes = lastBytes - firstBytes;
871
+ rows.push({ table, firstBytes, lastBytes, deltaBytes });
872
+ }
873
+ rows.sort((a, b) => Math.abs(b.deltaBytes) - Math.abs(a.deltaBytes));
874
+
875
+ const days = payload?.days != null ? `${payload.days}d` : "window";
876
+ const top = rows.slice(0, 3).map((r) => {
877
+ const arrow = r.deltaBytes >= 0 ? "↑" : "↓";
878
+ const pct = r.firstBytes > 0
879
+ ? ` (${r.deltaBytes >= 0 ? "+" : ""}${((r.deltaBytes / r.firstBytes) * 100).toFixed(1)}%)`
880
+ : "";
881
+ return `${r.table} ${arrow} ${formatBytesHumanLib(Math.abs(r.deltaBytes))}${pct}`;
882
+ });
883
+ return `${rows.length} table(s) with growth data over ${days}. `
884
+ + `Most-changed: ${top.join("; ")}.`;
885
+ }
886
+
887
+ function summarizeGrowthAnomalies(payload) {
888
+ // Backend returns { success, anomalies[], statistics: { total, warning,
889
+ // critical, info, acknowledged, unacknowledged } }. Agents should know
890
+ // BEFORE drilling into a slow query whether a recent growth anomaly is
891
+ // the real root cause.
892
+ const list = Array.isArray(payload?.anomalies) ? payload.anomalies : [];
893
+ if (list.length === 0) {
894
+ return "No growth anomalies detected in the requested window.";
895
+ }
896
+ const stats = payload?.statistics || {};
897
+ const total = stats.total ?? list.length;
898
+ const crit = stats.critical ?? 0;
899
+ const warn = stats.warning ?? 0;
900
+ const unack = stats.unacknowledged ?? 0;
901
+
902
+ // Surface the worst recent one so the agent has something concrete to
903
+ // reference without having to walk the structured payload.
904
+ const worst = list.find((a) => a && a.severity === "CRITICAL")
905
+ || list.find((a) => a && a.severity === "WARNING")
906
+ || list[0];
907
+ const worstLine = worst
908
+ ? ` Top: [${worst.severity || "INFO"}] ${worst.tableName || "?"} — `
909
+ + `${worst.anomalyType || "growth"}`
910
+ + (worst.sizeGrowthPercent != null
911
+ ? ` ${worst.sizeGrowthPercent > 0 ? "+" : ""}${worst.sizeGrowthPercent.toFixed(1)}%`
912
+ : "")
913
+ : "";
914
+ return `${total} growth anomal${total === 1 ? "y" : "ies"} `
915
+ + `(${crit} critical, ${warn} warning, ${unack} unacknowledged).${worstLine}`;
916
+ }
917
+
918
+ function formatBytesHumanLib(bytes) {
919
+ if (bytes == null) return "?";
920
+ const abs = Math.abs(bytes);
921
+ if (abs < 1024) return `${bytes} B`;
922
+ if (abs < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
923
+ if (abs < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
924
+ if (abs < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
925
+ return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
926
+ }
927
+
743
928
  function summarizeQueryResult(payload) {
744
929
  const result = payload?.result || payload?.data || payload;
745
930
  const rowCount = result?.rowCount ?? 0;
@@ -785,6 +970,12 @@ function buildToolResult(name, payload, extra = {}) {
785
970
  case "analyze_slow_queries":
786
971
  summary = summarizeSlowQueries(payload);
787
972
  break;
973
+ case "get_table_growth":
974
+ summary = summarizeTableGrowth(payload);
975
+ break;
976
+ case "get_growth_anomalies":
977
+ summary = summarizeGrowthAnomalies(payload);
978
+ break;
788
979
  case "get_index_recommendations":
789
980
  summary = summarizeIndexRecommendations(payload);
790
981
  break;
@@ -969,6 +1160,64 @@ async function handleToolCall(config, name, args = {}) {
969
1160
  return buildToolResult(name, payload);
970
1161
  }
971
1162
 
1163
+ case "get_slow_query_timeline": {
1164
+ const connectionId = String(args.connectionId || "").trim();
1165
+ if (!connectionId) return buildToolError("connectionId is required.");
1166
+ const fingerprint = String(args.fingerprint || "").trim();
1167
+ if (!fingerprint) return buildToolError("fingerprint is required.");
1168
+ const payload = await callDeepSqlApi(
1169
+ config,
1170
+ `/slow-query-analytics/${encodeURIComponent(connectionId)}/timeline/${encodeURIComponent(fingerprint)}`,
1171
+ );
1172
+ return buildToolResult(name, payload);
1173
+ }
1174
+
1175
+ case "get_query_regressions": {
1176
+ const connectionId = String(args.connectionId || "").trim();
1177
+ if (!connectionId) return buildToolError("connectionId is required.");
1178
+ const minFactor = args.minFactor != null ? Number(args.minFactor) : 1.5;
1179
+ const payload = await callDeepSqlApi(
1180
+ config,
1181
+ `/slow-query-analytics/${encodeURIComponent(connectionId)}/regressions?minFactor=${minFactor}`,
1182
+ );
1183
+ return buildToolResult(name, payload);
1184
+ }
1185
+
1186
+ case "get_table_growth": {
1187
+ const connectionId = String(args.connectionId || "").trim();
1188
+ if (!connectionId) return buildToolError("connectionId is required.");
1189
+ const params = [];
1190
+ const days = clampInteger(args.days, 1, 365, 30);
1191
+ params.push(`days=${days}`);
1192
+ if (args.tableName) {
1193
+ params.push(`tableName=${encodeURIComponent(String(args.tableName))}`);
1194
+ }
1195
+ const payload = await callDeepSqlApi(
1196
+ config,
1197
+ `/growth-monitoring/trends/${encodeURIComponent(connectionId)}?${params.join("&")}`,
1198
+ );
1199
+ return buildToolResult(name, payload);
1200
+ }
1201
+
1202
+ case "get_growth_anomalies": {
1203
+ const connectionId = String(args.connectionId || "").trim();
1204
+ if (!connectionId) return buildToolError("connectionId is required.");
1205
+ const params = [];
1206
+ const days = clampInteger(args.days, 1, 365, 30);
1207
+ params.push(`days=${days}`);
1208
+ if (args.tableName) {
1209
+ params.push(`tableName=${encodeURIComponent(String(args.tableName))}`);
1210
+ }
1211
+ if (args.unacknowledgedOnly === true) {
1212
+ params.push("unacknowledgedOnly=true");
1213
+ }
1214
+ const payload = await callDeepSqlApi(
1215
+ config,
1216
+ `/growth-monitoring/anomalies/${encodeURIComponent(connectionId)}?${params.join("&")}`,
1217
+ );
1218
+ return buildToolResult(name, payload);
1219
+ }
1220
+
972
1221
  case "execute_sql": {
973
1222
  const connectionId = String(args.connectionId || "").trim();
974
1223
  const query = String(args.query || "").trim();
@@ -1075,6 +1324,8 @@ module.exports = {
1075
1324
  stripSqlComments,
1076
1325
  stripSqlStringLiterals,
1077
1326
  summarizeApplyResult,
1327
+ summarizeGrowthAnomalies,
1078
1328
  summarizeIndexRecommendations,
1329
+ summarizeTableGrowth,
1079
1330
  validateReadOnlySql,
1080
1331
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepsql/mcp",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
5
5
  "bin": {
6
6
  "deepsql": "bin/deepsql.js",
@@ -87,6 +87,8 @@ usually doesn't know about either; that's exactly why DeepSQL exists.
87
87
  | `get_relationships(connectionId)` | Foreign keys (declared + inferred-with-confidence). |
88
88
  | `get_anti_patterns(connectionId, kind="table"\|"query")` | Patterns to avoid in this DB. |
89
89
  | `analyze_slow_queries(connectionId, thresholdMs?, limit?)` | Snapshot of slow queries from live stats. |
90
+ | `get_table_growth(connectionId, tableName?, days?)` | Persistent stats history: per-table size/row time series + headline rollups. Use to answer "which tables are growing fastest?" or "how much has X grown in the last month?" without scanning the live DB. |
91
+ | `get_growth_anomalies(connectionId, tableName?, unacknowledgedOnly?, days?)` | DeepSQL-flagged sudden growth spikes with severity (CRITICAL/WARNING/INFO), anomaly type, before/after sizes, confidence score. Check this BEFORE walking the user through a slow-query plan — a recent growth anomaly is often the real root cause. |
90
92
  | `execute_sql(connectionId, query, ...)` | Run any SQL — SELECT for everyone, DML/DDL for admins (two-step confirm). |
91
93
  | `analyze_query_plan(connectionId, query, useAnalyze=false)` | AI-enriched plan analysis (issues + index recs + summary). |
92
94
 
@@ -128,6 +130,7 @@ deepsql query "SELECT 1" --connection prod-pg --caller-agent claude-code --json
128
130
  | `deepsql relationships --connection <c>` | Inferred + validated FKs | `get_relationships` |
129
131
  | `deepsql anti-patterns --connection <c> [--kind table\|query]` | Anti-patterns | `get_anti_patterns` |
130
132
  | `deepsql digest [N]\|list\|show <id>` | **CLI-only**: daily digest of slow queries + AI commentary | none |
133
+ | `deepsql growth trends\|history\|anomalies\|ack\|capture\|config` | Table growth analytics (size/row trends, detected anomalies, alert thresholds) | partial: `get_table_growth`, `get_growth_anomalies` |
131
134
  | `deepsql indexes list\|missing\|health\|unused\|duplicates\|usage <table>` | **CLI-only**: index recommendations and usage stats | none |
132
135
  | `deepsql slow-queries latest\|history\|analyze\|optimize\|delete` | Slow-query analyses; `optimize` streams AI optimization steps live (SSE) | partial: `analyze_slow_queries` |
133
136
  | `deepsql users list\|get\|add\|set-role\|lock\|unlock\|disable\|resend-invite\|reset-password\|delete` | **Admin-only, CLI-only**: workspace user management | none |
@@ -147,6 +150,10 @@ shell-out (with `--caller-agent`) or tell the user the exact command:
147
150
  | User asks for | Run / suggest |
148
151
  |---|---|
149
152
  | "What changed in the database recently?" / "Today's report" | `deepsql digest` (most recent) or `deepsql digest 7` (last week) |
153
+ | "Which tables are growing fastest?" / "How big is `<table>` now vs a month ago?" | MCP: `get_table_growth(connectionId, days=30)`. CLI: `deepsql growth trends --connection <c>` (or `--table <name> --days 30`) |
154
+ | "Did any table spike in size recently?" / "Anything weird in growth?" | MCP: `get_growth_anomalies(connectionId, unacknowledgedOnly=true)`. CLI: `deepsql growth anomalies --connection <c> --unack` |
155
+ | "Force a fresh stats snapshot" (admin) | `deepsql growth capture --connection <c>` |
156
+ | "Set / view growth alert thresholds" (admin) | `deepsql growth config show --connection <c>` / `set --file <p>` |
150
157
  | "What indexes are we missing?" / "Index advice" | `deepsql indexes missing --connection <c>` |
151
158
  | "Are any indexes unused?" / "Index bloat" | `deepsql indexes unused --connection <c>` |
152
159
  | "Duplicate indexes?" | `deepsql indexes duplicates --connection <c>` |
package/src/auth/store.js CHANGED
@@ -30,18 +30,18 @@
30
30
 
31
31
  const fs = require("node:fs");
32
32
  const path = require("node:path");
33
- const os = require("node:os");
33
+ const { userHome } = require("../user-home");
34
34
 
35
35
  function configDir() {
36
36
  const override = process.env.DEEPSQL_CONFIG_DIR;
37
37
  if (override) return override;
38
38
  if (process.platform === "win32") {
39
- const base = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
39
+ const base = process.env.APPDATA || path.join(userHome(), "AppData", "Roaming");
40
40
  return path.join(base, "deepsql");
41
41
  }
42
42
  const xdg = process.env.XDG_CONFIG_HOME;
43
43
  if (xdg) return path.join(xdg, "deepsql");
44
- return path.join(os.homedir(), ".config", "deepsql");
44
+ return path.join(userHome(), ".config", "deepsql");
45
45
  }
46
46
 
47
47
  function authFilePath() {
package/src/cli.js CHANGED
@@ -31,6 +31,7 @@ const COMMANDS = {
31
31
  relationships: () => require("./commands/relationships"),
32
32
  "anti-patterns": () => require("./commands/anti-patterns"),
33
33
  digest: () => require("./commands/digest"),
34
+ growth: () => require("./commands/growth"),
34
35
  indexes: () => require("./commands/indexes"),
35
36
  users: () => require("./commands/users"),
36
37
  access: () => require("./commands/access"),
@@ -55,6 +56,7 @@ const COMMAND_LIST = [
55
56
  ["analyze", false, "AI-enriched query plan analysis (use --analyze for EXPLAIN ANALYZE)"],
56
57
  ["schema", false, "Dump connection schema or DB objects as JSON"],
57
58
  ["digest", true, "Show DeepSQL daily digests"],
59
+ ["growth", true, "Table growth analytics — trends, history, anomalies, alert config"],
58
60
  ["brain-context", false, "Retrieve embedding-ranked context for a question"],
59
61
  ["business-rules", false, "List active business rules and SQL guardrails"],
60
62
  ["relationships", false, "List inferred and validated FK relationships"],
@@ -224,6 +226,29 @@ const COMMAND_HELP = {
224
226
  ],
225
227
  },
226
228
 
229
+ growth: {
230
+ description: "Table growth analytics — size/row trends, detected anomalies, alert thresholds.",
231
+ usage: "deepsql growth <subcommand> [options]",
232
+ subcommands: [
233
+ ["trends [--table <t>] [--days N=30]", "Per-table growth headline over the last N days (default)"],
234
+ ["history [--table <t>] [--days N=7]", "Raw snapshot history rows"],
235
+ ["anomalies [--table <t>] [--unack] [--days N=30]", "Sudden growth spikes flagged by severity"],
236
+ ["ack <anomalyId>", "Acknowledge an anomaly"],
237
+ ["capture", "Trigger a fresh stats snapshot (admin; async)"],
238
+ ["config show [--table <t>]", "Show alert thresholds for the connection"],
239
+ ["config set --file <path>", "POST a GrowthAlertConfiguration JSON (admin)"],
240
+ ],
241
+ options: [
242
+ ["--connection <name>", "Connection to inspect"],
243
+ ["--table <name>", "Filter to one table"],
244
+ ["--days <N>", "Lookback window in days (max 365)"],
245
+ ["--unack", "anomalies: only unacknowledged ones"],
246
+ ["--file <path>", "config set: path to GrowthAlertConfiguration JSON"],
247
+ ["--json", "Raw JSON output"],
248
+ ],
249
+ notes: "`capture` and `config set` are the only mutations; both are admin-level but neither writes user data. Trends + anomalies are the agent-facing subcommands and are also exposed via the MCP tools `get_table_growth` and `get_growth_anomalies`.",
250
+ },
251
+
227
252
  "brain-context": {
228
253
  description: "Retrieve embedding-ranked tables/columns/FKs, training docs, and business rules for a question.",
229
254
  usage: 'deepsql brain-context "<question>" --connection <name> [options]',
@@ -565,6 +590,11 @@ function buildOpts(parsed) {
565
590
  // Indexes
566
591
  all: !!f.all,
567
592
  status: f.status || null,
593
+ // Growth analytics (also re-used wherever a per-table / lookback filter
594
+ // is useful — kept generic on purpose)
595
+ table: f.table || null,
596
+ days: f.days || null,
597
+ unack: !!f.unack,
568
598
  // mcp config installer
569
599
  install: !!f.install,
570
600
  print: !!f.print,