@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 +3 -1
- package/deepsql-phase1-lib.js +251 -0
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +7 -0
- package/src/auth/store.js +3 -3
- package/src/cli.js +30 -0
- package/src/commands/growth.js +458 -0
- package/src/commands/growth.test.js +439 -0
- package/src/commands/mcp.js +9 -9
- package/src/commands/slow-queries.js +116 -1
- package/src/user-home.js +29 -0
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
|
|
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. |
|
package/deepsql-phase1-lib.js
CHANGED
|
@@ -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
package/skills/SKILL_BODY.md
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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,
|